Hierarchical tables can be easily merged using repeating merge blocks with the Mail ╰ TX Text Control .NET Server for ASP.NET
╰ DocumentServer Namespace
╰ MailMerge Class
The MailMerge class is a .NET component that can be used to effortlessly merge template documents with database content in .NET projects, such as ASP.NET web applications, web services or Windows services. class. But what if the tables are very dynamic with a variable number of columns?
In this case, tables can be dynamically generated using the TX Text Control API by adding nested tables. This method can also be mixed with the MailMerge process by adding a placeholder in your template that will be replaced with the dynamically generated table.
JSON Data
Consider the following JSON data that should be used to generate the table.
[ | |
{ | |
"Row": 1, | |
"UniqueID": "900-0315918-000", | |
"Name": "Tim Typer", | |
"Address": "1 Text Control Dr.", | |
"City": "Charlotte", | |
"Zip": "28209", | |
"State": "NC", | |
"Country": "US", | |
"line_items": [ | |
{ | |
"Name": "TX Text Control", | |
"Description": "Awesome Document Processor", | |
"Price": 2998.00, | |
"Quantity": 1, | |
"additional_items": [ | |
{ | |
"Name": "TX Barcode .NET for Windows Forms", | |
"Description": "Barcode Generator for Windows Forms", | |
"Price": 499.00, | |
"Quantity": 1 | |
}, | |
{ | |
"Name": "TX Spell .NET for Windows Forms", | |
"Description": "Spell Checking for Windows Forms", | |
"Price": 499.00, | |
"Quantity": 1 | |
} | |
] | |
} | |
] | |
}, | |
{ | |
"Row": 2, | |
"UniqueID": "800-0315918-000", | |
"Name": "Kathrina Keyboard", | |
"Address": "1 Text Control Dr.", | |
"City": "Charlotte", | |
"Zip": "28209", | |
"State": "NC", | |
"Country": "US", | |
"line_items": [ | |
{ | |
"Name": "TX Text Control", | |
"Description": "Awesome Document Processor", | |
"Price": 2998.00, | |
"Quantity": 1, | |
"additional_items": [ | |
{ | |
"Name": "TX Barcode .NET for Windows Forms", | |
"Description": "Barcode Generator for Windows Forms", | |
"Price": 499.00, | |
"Quantity": 1 | |
} | |
] | |
} | |
] | |
} | |
] |
The data contains three hierarchical levels with two data rows on the first level. The JSON string is loaded and converted into a list of JObject elements returned by the DeserializeObject method of the JsonConvert class (Newtonsoft.Json). The CreateTable method is then called with the created list object.
string json = File.ReadAllText(@"data.json"); | |
var data = JsonConvert.DeserializeObject<List<JObject>>(json); | |
CreateTable(data); |
Recursively Adding Tables
The CreateTable method takes the hierarchical data and creates the tables. It is called recursively based on the current hierarchy level. A new table is added based on the number of columns in the current hierarchy level. For each row of data, a new table row is added and the values are added to the table cell text.
In the case where the data value is an array, a new nested table will be added by making a recursive call to the CreateTable method.
// Create a hierarchical table from a list of JSON objects | |
private void CreateTable(List<JObject> dataObjects) { | |
// Get the number of columns of the first object | |
int columns = dataObjects[0].Values().Count(); | |
// Check if the first object contains an array and reduce the number of columns | |
columns -= dataObjects[0].Values().Count(value => value.Type == JTokenType.Array); | |
// Add a table with the number of columns | |
Table table = AddTableAtInputPosition(1, columns); | |
int row = 1; | |
// Loop through all objects | |
foreach (JObject dataObject in dataObjects) { | |
// Check if the row is not the first row and add a new row | |
if (row != 1) { | |
table.Cells.GetItem(row - 1, 1).Select(); | |
textControl1.Selection.Start += textControl1.Selection.Length; | |
textControl1.Selection.Length = 0; | |
textControl1.Selection.Start -= 1; | |
// Add a new row after the current row | |
table.Rows.Add(TXTextControl.TableAddPosition.After, 1); | |
// Set the left text distance to 0 to remove the indent | |
table.Rows.GetItem().CellFormat.LeftTextDistance = 0; | |
// Split all cells | |
table.SplitCells(); | |
} | |
int col = 1; | |
// Loop through all properties of the object | |
foreach (var info in dataObject) { | |
// Check if the property is an array | |
if (dataObject[info.Key].Type == JTokenType.Array) { | |
textControl1.Selection.Start = table.Cells.GetItem(row, 1).Start; | |
table.Rows.Add(TXTextControl.TableAddPosition.After, 1); | |
// Set the left text distance to indent the nested table | |
table.Rows.GetItem().CellFormat.LeftTextDistance = 600; | |
// Select the new row and merge all cells | |
table.Select(row + 1, 1, row + 1, table.Columns.Count); | |
table.MergeCells(); | |
// Create a new table for the array | |
List<JObject> subObject = | |
dataObject[info.Key]?.Select(x => x as JObject).ToList(); | |
// recursively call the method to create a new nested table | |
if (subObject != null) { | |
CreateTable(subObject); | |
row++; | |
} | |
} | |
else { // Add the value to the table cell | |
table.Cells.GetItem(row, col).Text = info.Value?.ToString(); | |
col++; | |
} | |
} | |
row++; | |
} | |
} | |
// Add a table at the current input position and return the table object | |
private Table AddTableAtInputPosition(int rows, int columns) { | |
textControl1.Tables.Add(rows, columns); | |
textControl1.Selection.Start -= 1; | |
return textControl1.Tables.GetItem(); | |
} |
Sample Data Results
When this method is called with the sample data, the following table structure will be created.