Objects and Collections
WPF applications often have their data in classes already, for example the models a window binds to. MailMerge merges such objects without conversion: public properties become merge fields, and properties that hold collections fill merge blocks. You can merge collections and single objects, rename fields with attributes, format text in ToString, offer the classes in the Reporting tab and merge dictionaries whose keys are known only at run time.
The examples extend the invoice classes Invoice and InvoiceItem of the Getting Started tutorial Add Repeating Merge Blocks. They assume a window with a TextControl named textControl1 and a RibbonReportingTab named ribbonReportingTab1, connected through the RibbonReportingTab property of the TextControl, as the tutorials set them up.
How MailMerge Reads Objects
MergeObjects takes any IEnumerable, and MergeObject one object. Both read the public properties through .NET reflection:
| Property | Becomes |
|---|---|
A simple type, such as string, int, DateTime or an enumeration | A column: the merge field of the same name receives the text of its ToString method |
A type with public properties of its own, such as Customer | A column merged with ToString, or a related table with one row whose properties are addressed with dot notation, such as Customer.City |
A collection, a type that implements IEnumerable, such as List<InvoiceItem> | A related table that fills the merge block of the same name, also in nested blocks |
A read-only calculated property, such as Total => Quantity * Price | A column like any other property |
The type of the first object in a collection defines the structure; objects of other types in the same collection still merge, but some of their fields stay empty. Each object of the collection produces one copy of the template, and with append set to true, the default, the copies form one document.
View models of an MVVM application are objects like any other. Merge them, or classes built from them, as long as their public properties have the names the template uses.
In the following class diagram of a Text Control sample, the Products property of Invoice is a list of Product objects and fills a merge block Products with the fields Name and Price:

Control Field Names and Merged Text
Two attributes of System.ComponentModel affect the merge:
DisplayNamegives a property a different field name: with[DisplayName("Date")],InvoiceDatefills the fieldDate.Descriptionon an enumeration value sets the text merged for it.
The invoice classes with a date, a status and a calculated item total:
using System;
using System.Collections.Generic;
using System.ComponentModel;
public class Invoice
{
public string Company { get; set; }
public string Name { get; set; }
public string InvoiceNumber { get; set; }
// Merged into a field named "Date"
[]
public DateTime InvoiceDate { get; set; }
public InvoiceStatus Status { get; set; }
public List<InvoiceItem> Items { get; set; } = new List<InvoiceItem>();
}
public class InvoiceItem
{
public string Description { get; set; }
public int Quantity { get; set; }
public decimal Price { get; set; }
// Calculated by the object, merged like any other property
public decimal Total => Quantity * Price;
}
public enum InvoiceStatus
{
[]
Open,
Paid
}
For an open invoice, the field Status shows "Awaiting payment", and a field Total in the Items block shows each item's total. Calculations like this belong in the objects rather than in the template; the blog article Merging Self-Calculating Business Objects with TX Text Control MailMerge in C# builds a complete invoice on this idea.
Merge a Collection Into the Editor
Add these methods to MainWindow.xaml.cs. The second one is the Click handler of a button named mergeObjectsButton, connected in XAML with Click="mergeObjectsButton_Click":
using System;
using System.Collections.Generic;
using System.Windows;
using TXTextControl.DocumentServer;
private List<Invoice> GetInvoices()
{
Invoice invoice = new Invoice
{
Company = "Contoso Ltd.",
Name = "Jane Miller",
InvoiceNumber = "INV-1001",
InvoiceDate = new DateTime(2026, 9, 1),
Status = InvoiceStatus.Open
};
invoice.Items.Add(new InvoiceItem { Description = "Consulting", Quantity = 2, Price = 150.00m });
invoice.Items.Add(new InvoiceItem { Description = "Training", Quantity = 1, Price = 480.00m });
return new List<Invoice> { invoice };
}
private void mergeObjectsButton_Click(object sender, RoutedEventArgs e)
{
using (MailMerge mailMerge = new MailMerge { TextComponent = textControl1 })
{
// Merge the template in the editor; the result replaces it
mailMerge.MergeObjects(GetInvoices());
}
}
The code works as follows:
TextComponent = textControl1connectsMailMergeto the WPFTextControl, so the document in the editor is the template and the merged invoice replaces it. AServerTextControlkeeps the template untouched, as Overview shows.MergeObjectsmerges eachInvoiceof the list: the fields receiveCompany,Name,InvoiceNumber,DateandStatus, and theItemsblock one row perInvoiceItem.
Data from a database can take the same route: query it, for example with Entity Framework, into classes shaped like the document, and merge those.
Offer the Classes in the Reporting Tab
To design a template, users need the names of the fields and blocks. LoadObjects of the DataSourceManager that the Reporting tab provides reads a collection like MergeObjects and fills Insert Merge Field and Insert Merge Block with its properties. Call it when the window has loaded, for example in the Loaded event handler of the TextControl, connected with Loaded="textControl1_Loaded" on the TextControl element:
private void textControl1_Loaded(object sender, RoutedEventArgs e)
{
// Offer the properties of Invoice in the drop-downs of the Reporting tab
ribbonReportingTab1.DataSourceManager.LoadObjects(GetInvoices());
}
LoadSingleObject loads a single object. LoadAssembly loads a compiled assembly and deduces tables, columns and relations from the public properties of all its public classes; users do the same with Load Assembly in the Select Data Source drop-down.
Format Text in ToString
A property whose type has properties of its own can also fill one field as a whole. A merge field with the property's name receives the text of ToString, so an override can return a formatted block, such as an address on two lines:
using System;
public class Customer
{
public string Name { get; set; }
public string City { get; set; }
// The text a merge field named "Customer" shows
public override string ToString() => Name + Environment.NewLine + City;
}
With a property Customer of this type, the field Customer shows the name and the city on two lines, and fields with dot notation, such as Customer.City, address the single values instead.
The Customer class of the same sample overrides ToString this way:

Merge Dictionaries and Anonymous Objects
When the field names are known only at run time, for example because users define them, merge dictionaries with string keys: MergeObjects accepts a collection of Dictionary instances, and the keys are the field names. MergeObject accepts an object of any type, an anonymous object included:
Dictionary<string, object> invoice = new Dictionary<string, object>
{
[] = "Contoso Ltd.",
[] = "Jane Miller",
[] = "INV-1001"
};
mailMerge.MergeObjects(new List<Dictionary<string, object>> { invoice });
// or, with an anonymous object
mailMerge.MergeObject(new { Company = "Contoso Ltd.", Name = "Jane Miller", InvoiceNumber = "INV-1001" });
The key names and value types of the first dictionary define the structure.
Next Steps
- DataSets and DataTables: merge tables with relations.
- JSON and XML: merge the same invoice as JSON.
- Merge Blocks: the blocks that collections fill.
- Nested Blocks and Master-Detail Reports: collections inside collections.