Consider the following comma separated list that is created dynamically using a merge block:

MailMerge event order

Pay attention to the last circle where the comma is intentionally missing. In you define the comma as part of the TextAfter TX Text Control .NET Server for ASP.NET
DocumentServer.Fields Namespace
MergeField Class
TextAfter Property
Gets and sets the text of the field that is displayed after the field's text.
property of a MergeField TX Text Control .NET Server for ASP.NET
DocumentServer.Fields Namespace
MergeField Class
The MergeField class implements the MS Word specific MERGEFIELD field.
, the comma would be added to the last entry in the merge block as well.

MailMerge event order

As a result, an unwanted comma is added at the end of the list.

MailMerge event order

In order to solve that problem, the flexible events of MailMerge 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.
are used:

For merge blocks, these events are fired in the following order:

MailMerge event order

To define conditional text after processing, a unique keyword can be defined that is part of the TextAfter property:

MailMerge event order

The TextBefore property value starts with %REMOVELAST%: followed by the actual string that should be rendered. In our case the comma (",").

When merging the template with data, the above events are attached before calling one of the merge methods:

using (MailMerge mm = new MailMerge()) {
mm.TextComponent = textControl1;
// attach events
mm.BlockMerging += Mm_BlockMerging;
mm.FieldMerged += Mm_FieldMerged;
mm.BlockRowMerged += Mm_BlockRowMerged;
// merge template
view raw merge.cs hosted with ❤ by GitHub

The flag bLastBlockRow that indicates that the last row of a block is processed is reset for each new merge block:

bool bLastBlockRow = false;
private void Mm_BlockMerging(object sender, MailMerge.BlockMergingEventArgs e) {
// reset counter for new block
bLastBlockRow = false;
view raw test.cs hosted with ❤ by GitHub

bLastBlockRow is updated in the BlockRowMerged event:

private void Mm_BlockRowMerged(object sender, MailMerge.BlockRowMergedEventArgs e) {
// before last row gets merged
if (e.DataRowNumber == e.DataRowCount - 2) {
bLastBlockRow = true;
view raw test.cs hosted with ❤ by GitHub

The actual processing is done in the FieldMerged event. If the field contains the unique keyword and the last row of the merge block is being processed, the complete text after is removed. Otherwise, it is rendered.

private void Mm_FieldMerged(object sender, MailMerge.FieldMergedEventArgs e) {
var keyword = "%REMOVELAST%:";
// return if field is outside block
if (e.MergeBlockName == "" || e.MailMergeFieldAdapter.TypeName != "MERGEFIELD")
// convert to the MergeField
MergeField field = ((MergeField)e.MailMergeFieldAdapter);
// if "text after" contains keyword
if (field.TextAfter.StartsWith(keyword)) {
if (bLastBlockRow == true) { // remove "text after" when last row
field.TextAfter = "";
else { // else keep "text after" string
field.TextAfter = field.TextAfter.Substring(
field.TextAfter.Length - keyword.Length);
view raw test.cs hosted with ❤ by GitHub