The concept of Text Control Reporting is to accept pre-shaped data. That means that Text Control's MailMerge component is merging the data rows 'as-is' and data shaping, sorting or queries are done in the application business layer.

Sometimes, it is required to define the order of data rows in the template. This sample shows how to realize this concept based on additional parameters in the merge block name and LINQ to sort the data rows.

Let's have a look at the XML data source. It consists of 2 simple tables: company and product and a relation between those tables.

Data source

The template simply consists of a single merge block with the name product. Without any sorting, the results would look like this:

Merge results without sorting

In order to enable sorting, we simply introduce a new parameter to the merge block name:

[BlockName];orderby,[ColumnName],ASC|DESC

For example:

product;orderby,name,ASC

The sample code shows how to detect and parse these merge blocks in the template and how to replace the rows of the relevant DataTables with sorted DataRows.

The class DataSelector is parsing the template in order to sort the given data source. The property Template returns the changed template which is loaded into a MailMerge instance using LoadTemplateFromMemory. Finally, the document is merged with the sorted DataSet:

private string sDataSource = "data.xml";
private string sTemplateFile = "template.tx";
private void mergeToolStripMenuItem_Click(object sender, EventArgs e)
{
// convert the XML file to a .NET DataSet
DataSet ds = new DataSet();
ds.ReadXml(sDataSource, XmlReadMode.Auto);
// create a new DataSelector instance
DataSelector selector = new DataSelector(ds,
File.ReadAllBytes(sTemplateFile));
// load the modified template
mailMerge1.LoadTemplateFromMemory(selector.Template,
TXTextControl.DocumentServer.FileFormat.InternalUnicodeFormat);
// merge the template with the new, sorted data source
mailMerge1.Merge(ds.Tables[0]);
// copy the merged document into a visible TextControl object
byte[] data = null;
mailMerge1.SaveDocumentToMemory(out data,
TXTextControl.BinaryStreamType.InternalUnicodeFormat, null);
textControl1.Load(data,
TXTextControl.BinaryStreamType.InternalUnicodeFormat);
}
view raw gistfile1.cs hosted with ❤ by GitHub

The DataSelector class loops through all merge blocks in a given template to check for specific keywords. A temporary ServerTextControl instance is used to recognize blocks with the sorting keyword 'ORDERBY'. If such tagged merge blocks are found, the parameters are parsed, stored and the block name is reset to the original block name.

Finally, LINQ is used to sort the data rows by the given column name. The resulting data rows are replaced in the specific DataTable. The DataTable won't be replaced completely to keep existing data relations and constraints.

// DataSelector
// description: This class loops through all merge blocks in a given template to check
// for sort keywords. The given referenced DataSet will be sorted.
//
// Parameters: dataSet of type DataSet, template as a byte[] array in the InternalUnicodeFormat
public class DataSelector
{
public byte[] Template { get; set; }
public DataSelector(DataSet dataSet, byte[] template)
{
// use a temporary ServerTextControl instance to recognize blocks with
// the sorting keyword 'ORDERBY'
using (TXTextControl.ServerTextControl tx =
new TXTextControl.ServerTextControl())
{
tx.Create();
// load the template
tx.Load(template, TXTextControl.BinaryStreamType.InternalUnicodeFormat);
// create a list of merge blocks
List<MergeBlock> lMergeBlocks = MergeBlock.GetMergeBlocks(MergeBlock.GetBlockMarkersOrdered(tx), tx);
// loop through all merge blocks to check whether they
// have a sorting parameter
foreach (MergeBlock mergeBlock in lMergeBlocks)
{
string sBlockName = mergeBlock.StartMarker.TargetName;
// check for the unique sorting parameter
if (sBlockName.ToUpper().Contains("ORDERBY") == false)
continue;
// create a new SortedBlock object to store parameter
SortedBlock block = new SortedBlock(sBlockName);
// remove the sorting parameter from the block name
// so that MailMerge can find the matching data in the data source
mergeBlock.StartMarker.TargetName = block.Name;
mergeBlock.EndMarker.TargetName = "BlockEnd_" + mergeBlock.Name;
if (dataSet.Tables.Contains(mergeBlock.Name) == false)
continue;
// get all DataRows using LINQ
var query = from product in dataSet.Tables[mergeBlock.Name].AsEnumerable()
select product;
// create a new DataTable with the sorted DataRows
DataTable dtBoundTable = (block.SortType == SortType.ASC) ?
query.OrderBy(product => product.Field<String>(block.ColumnName)).CopyToDataTable() :
query.OrderByDescending(product => product.Field<String>(block.ColumnName)).CopyToDataTable();
// remove original rows and replace with sorted rows
dataSet.Tables[mergeBlock.Name].Rows.Clear();
dataSet.Tables[mergeBlock.Name].Merge(dtBoundTable);
dtBoundTable.Dispose();
}
// save the template
byte[] data = null;
tx.Save(out data, TXTextControl.BinaryStreamType.InternalUnicodeFormat);
this.Template = data;
}
}
}
view raw gistfile1.cs hosted with ❤ by GitHub

Let's do two examples. The merge block name contains the following string:

product;orderby,name,ASC

The ascending sorted results are shown below:

Results with ASC sorting

Descending sorting results:

Results with DESC sorting

LINQ is used to sort the results to show the flexibility. You can customize this process with your own selectors. This shows the flexibility and the power of the MailMerge class. Give it a try to download the sample project directly from GitHub.

Download the sample from GitHub and test it on your own.