Conditionally hide text block via Mail Merge

Hello,

I’m new to GemBox.Document and I’m trying to create my invoices via the Mail Merge solutions in GemBox.Document.

I can’t find how I can conditionally show or hide a part of my word document. Eg. I want to show payment info when my document is of type invoice but I want to hide the payment info when the document is of type creditnote. It is nog just one line of text, it is a block with multiple lines of text including some formatting.

I’ve tried to put extra tags around the payment info block, but I can’t figure out how I can hide the text block.

Could someone suggest a solution to achieve this?

Thanks in advance!

image

Hi Joeri,

You could consider creating a merge range that would conditionally end up being removed (using the MailMergeClearOptions.RemoveEmptyRanges).

For example, let’s say you have this:

input

Now if you execute the following mail merge process:

var wireTransferSource = new
{
    DueDate = DateTime.Today,
    Message = "Sample",
    Amount = 123.45,
    AccountNumber = 1000
};

var document = DocumentModel.Load("input.docx");
document.MailMerge.ClearOptions = MailMergeClearOptions.RemoveEmptyRanges;
document.MailMerge.Execute(wireTransferSource, "WireTransfer");
document.Save("output.docx");

You’ll end up with something like this:
output

But if you execute that same mail merge process with an empty source, for example:

var wireTransferSource = new
{
    DueDate = "",
    Message = "",
    Amount = "",
    AccountNumber = ""
};

Then this “WireTransfer” merge range will be removed because none of the fields was merged inside of it and we specified the usage of MailMergeClearOptions.RemoveEmptyRanges.

Alternatively, you could have another value in the source that would indicate if the merge range should be removed. Then you could use the FieldMerging event to remove any field merging for that range when the conditional value indicates that the range needs to be removed.

This will basically result in the same behavior, none of the fields inside the range will be merged and thus the whole range will be removed because of the MailMergeClearOptions.RemoveEmptyRanges.

var wireTransferSource = new
{
    DueDate = DateTime.Today,
    Message = "Sample",
    Amount = 123.45,
    AccountNumber = 1000,
    Hidden = true
};

var document = DocumentModel.Load("input.docx");

document.MailMerge.FieldMerging += (sender, e) =>
{
    if (e.MergeContext.RangeName != "WireTransfer")
        return;

    var source = (dynamic)e.MergeContext.Record;
    if (source.Hidden)
        e.Inline = null;
};

document.MailMerge.ClearOptions = MailMergeClearOptions.RemoveEmptyRanges;
document.MailMerge.Execute(wireTransferSource, "WireTransfer");
document.Save("output.docx");

Hello Mario,

Thank you for the response, it is working. But unfortunately, it is not 100% the solution for my case.

In fact the payment info block is only a small part of my invoicing document, so I have several parts on the document I want to show/hide depending on the type of document.

Using RangeStart obliges me to use a nested object with the payment properties “in” the “WireTransfer” object. It would make it very complex to create a nested object for each dynamic template block.

I rather expect that it would be possible to link the show/hide logica to a specific property on my object, link the screenshow below.
Do you have any idea if that is possible somehow?

image

Thanks in advance!

Hi Joeri,

In that case, you could add some custom text using those “ShowIfInvoice” that would indicate that the content within needs to be removed.

For example, something like the following, it will add the “%%DELETE%%” tags when the ShowIfInvoice is false and then after the mail merge you can search for those tags and delete them together with anything that is within:

const string DeleteTag = "%%DELETE%%";
var document = DocumentModel.Load("input.docx");

var source = new
{
    TotalAmountIncl = 1234.56,
    DueDate = DateTime.Today,
    // ...
    ShowIfInvoice = false
};

document.MailMerge.FieldMerging += (sender, e) =>
{
    if (e.FieldName != "ShowIfInvoice")
        return;

    if (e.IsValueFound && !(bool)e.Value)
        e.Inline = new Run(e.Document, DeleteTag);
    else
    {
        e.Field.Parent.Content.Delete();
        e.Cancel = true;
    }
};

document.MailMerge.Execute(source);

var deletePositions = document.Content.Find(DeleteTag).ToList();
if (deletePositions.Count % 2 != 0)
    throw new InvalidOperationException($"The '{DeleteTag}' tags should come in pair, indicating start and end of the content that should to be removed");

deletePositions.Reverse();
for (int i = 0; i < deletePositions.Count; i += 2)
{
    var deleteRange = new ContentRange(deletePositions[i + 1].Start, deletePositions[i].End);
    deleteRange.Delete();
}

document.Save("output.docx");

You could go even further with this and define something like a custom merge field type that you would use for this purpose. For example, here is how you would define that merge fields that start with “Conditional:” are used for this.

input.docx:

const string DeleteTag = "%%DELETE%%";
var document = DocumentModel.Load("input.docx");

var source = new
{
    TotalAmountIncl = 1234.56,
    DueDate = DateTime.Today,
    // ...
    IsInvoice = false,
    HasNotes = true
};

// Map merge fields with prefix to data source without prefix.
// E.g. "Conditional:IsInvoice" field's name to "IsInvoice" data source name.
foreach (string fieldName in document.MailMerge.GetMergeFieldNames())
{
    if (!fieldName.StartsWith("Conditional:"))
        continue;

    string sourceName = fieldName.Substring("Conditional:".Length);
    document.MailMerge.FieldMappings[fieldName] = sourceName;
}

document.MailMerge.FieldMerging += (sender, e) =>
{
    if (!e.FieldName.StartsWith("Conditional:"))
        return;

    if (e.IsValueFound && !(bool)e.Value)
        e.Inline = new Run(e.Document, DeleteTag);
    else
    {
        e.Field.Parent.Content.Delete();
        e.Cancel = true;
    }
};

document.MailMerge.Execute(source);

var deletePositions = document.Content.Find(DeleteTag).ToList();
if (deletePositions.Count % 2 != 0)
    throw new InvalidOperationException($"The '{DeleteTag}' tags should come in pair, indicating start and end of the content that should to be removed");

deletePositions.Reverse();
for (int i = 0; i < deletePositions.Count; i += 2)
{
    var deleteRange = new ContentRange(deletePositions[i + 1].Start, deletePositions[i].End);
    deleteRange.Delete();
}

document.Save("output.docx");

output.docx:

I hope this helps.

Regards,
Mario

Hello Mario,

Thank you for your response!

It took some time to fit your code snippet in my own code. But I can confirm it is now working as expected. I just had to tweak the code in the FieldMerging function.

Regards,
Joeri

image