Evaluate Complex IF Conditions | Replace Merge Field with RTF Word Document using C# .NET | Mail Merge Region | System.ArgumentException

One of our customers has a template with complex and nested IF conditions. These are not evaluated correctly.
In the appendix you will find a test program and the template for simulating the problem.

Aspose.Word.Testanwendung.zip (50.9 KB)

Comparison of the output MS Word and Aspose.Words: Output-Comparison.zip (78.5 KB)

@Niebelschutz,

We are checking this scenario and will get back to you soon.

@Niebelschutz,

For the sake of any correction in Aspose.Words API, we have logged this problem in our issue tracking system with ID WORDSNET-20995. We will further look into the details of this problem and will keep you updated here on the status of the linked issue.

@Niebelschutz,

It is to inform you that we have completed the work on WORDSNET-20995 and concluded to close this issue with “Not a Bug” status. We have identified a few problems with your solution:

MergeFieldQuoteEnsurer:
Our previous workaround does not feet any cases as comment inside states:

And it does not work properly with sample document.

MergeFieldHandler:
_builder.MoveToMergeField(e.FieldName);

The MergeFieldHandler tries to merge VSTG_ZF_2_RTF field in own way. But the sample document contains multiple VSTG_ZF_2_RTF mergefields and the DocumentBuilder.MoveToMergeField navigate to the first one which is not actually merged.

_builder.InsertDocument(rtfDoc, ImportFormatMode.KeepSourceFormatting);

The MergeFieldHandler inserts RTF document in-place of VSTG_ZF_2_RTF field inside IF field. The RTF document contains several quotes. As result parent IF field arguments become rearranged.

Program.IfThenCondition:

builder.MoveToField(field, false);
builder.Write(field.Result);
field.Remove();

This code snippet replaces field result with its plain text version.
All these problems cause unexpected result.
Here is updated solution without these problems:

public class MergeFieldQuoteEnsurer : DocumentVisitor
{
    public override VisitorAction VisitRun(Run run)
    {
        if (!IsInIfField)
            return VisitorAction.Continue;


        bool isEscaped = false;

        foreach (char c in run.Text)
        {
            switch (c)
            {
                case '\\':
                    isEscaped = !isEscaped;
                    break;
                case '"':
                    if (!isEscaped)
                        CurrentQuotesCounter.Increment();
                    break;
            }

            if (c != '\\' && isEscaped)
                isEscaped = false;
        }

        return VisitorAction.Continue;
    }

    public override VisitorAction VisitFieldStart(FieldStart fieldStart)
    {
        switch (fieldStart.FieldType)
        {
            case FieldType.FieldIf:
                mQuotesCounters.Push(new QuotesCounter());
                break;
            case FieldType.FieldMergeField:
                if (IsInIfField && !CurrentQuotesCounter.IsInQuotes)
                    mFieldsToProcess.Add(fieldStart.GetField());
                break;
        }

        mFieldTypes.Push(fieldStart.FieldType);

        return VisitorAction.Continue;
    }

    public override VisitorAction VisitFieldSeparator(FieldSeparator fieldSeparator)
    {
        CompleteField(fieldSeparator.FieldType);

        return VisitorAction.Continue;
    }

    public override VisitorAction VisitFieldEnd(FieldEnd fieldEnd)
    {
        if (fieldEnd.GetField().Separator == null)
            CompleteField(fieldEnd.FieldType);

        return VisitorAction.Continue;
    }

    private void CompleteField(FieldType fieldType)
    {
        mFieldTypes.Pop();
        if (fieldType == FieldType.FieldIf)
            mQuotesCounters.Pop();
    }

    public override VisitorAction VisitDocumentEnd(Document doc)
    {
        InsertQuotes();
        return VisitorAction.Continue;
    }

    private void InsertQuotes()
    {
        foreach (Field field in mFieldsToProcess)
        {
            FieldStart fieldStart = field.Start;
            FieldEnd fieldEnd = field.End;

            fieldStart.ParentNode.InsertBefore(new Run(fieldStart.Document, "\""), fieldStart);
            fieldEnd.ParentNode.InsertAfter(new Run(fieldEnd.Document, "\""), fieldEnd);
        }
    }

    private bool IsInIfField
    {
        get { return mFieldTypes.Count > 0 && mFieldTypes.Peek() == FieldType.FieldIf; }
    }

    private QuotesCounter CurrentQuotesCounter
    {
        get { return mQuotesCounters.Peek(); }
    }

    private readonly Stack<FieldType> mFieldTypes = new Stack<FieldType>();
    private readonly Stack<QuotesCounter> mQuotesCounters = new Stack<QuotesCounter>();
    private readonly List<Field> mFieldsToProcess = new List<Field>();

    private class QuotesCounter
    {
        public void Increment()
        {
            mQuoteCount++;
        }

        public bool IsInQuotes
        {
            get { return (mQuoteCount & 1) == 1; }
        }

        private int mQuoteCount;
    }
}

public class MergeFieldHandler : IFieldMergingCallback
{
    private const string PatternRtfCode = @"^\{\\rtf.+";

    public void FieldMerging(FieldMergingArgs e)
    {

        if (_builder == null)
        {
            _builder = new DocumentBuilder(e.Document);
        }

        if (e.FieldName == null || string.IsNullOrWhiteSpace(e.FieldName) ||
            e.FieldValue == null || string.IsNullOrWhiteSpace(e.FieldValue.ToString()))
        {
            return;
        }

        if (TryHandleRtfField(e.FieldValue, out var rtfCode))
        {
            if (IsNestedFieldOfIfField(e))
            {
                return;
            }

            var stream = new MemoryStream(Encoding.UTF8.GetBytes(rtfCode));

            var info = FileFormatUtil.DetectFileFormat(stream);
            if (info.LoadFormat == LoadFormat.Rtf)
            {
                var rtfDoc = new Document(stream);

                rtfDoc.Range.Replace(
                    new Regex("([\"«“„»”‟])"),
                    string.Empty,
                    new FindReplaceOptions(new ReplacingCallback(rtfDoc)));

                _builder.MoveTo(e.Field.Remove());
                _builder.InsertDocument(rtfDoc, ImportFormatMode.KeepSourceFormatting);
            }
        }
    }

    private bool IsNestedFieldOfIfField(FieldMergingArgsBase e)
    {
        Node node = e.Field.Start;
        node = node.PreviousSibling;
        while (node != null)
        {
            if (node.NodeType == NodeType.FieldStart
                && ((FieldStart)node).GetField().Type == FieldType.FieldIf)
            {
                return true;
            }
            node = node.PreviousSibling;
        }

        return false;
    }

    public void ImageFieldMerging(ImageFieldMergingArgs args)
    {
    }

    private DocumentBuilder _builder;

    private bool TryHandleRtfField(object fieldValue, out string rtfCode)
    {
        if (!(fieldValue is string))
        {
            rtfCode = null;
            return false;
        }

        rtfCode = (string)fieldValue;

        var regex = new Regex(PatternRtfCode, RegexOptions.IgnoreCase);
        if (regex.IsMatch(rtfCode))
        {
            return true;
        }

        rtfCode = null;
        return false;
    }

    private class ReplacingCallback : IReplacingCallback
    {
        public ReplacingCallback(Document document)
        {
            _mBuilder = new DocumentBuilder(document);
        }

        public ReplaceAction Replacing(ReplacingArgs args)
        {
            var run = SplitAndRemove((Run)args.MatchNode, args.MatchOffset);
            _mBuilder.MoveTo(run);
            _mBuilder.InsertField($@"QUOTE \{args.Match.Value}");

            return ReplaceAction.Skip;
        }

        private readonly DocumentBuilder _mBuilder;

        private static Run SplitAndRemove(Run run, int index)
        {
            var text = run.Text;

            run.Text = text.Substring(0, index);
            var nextRun = (Run) run.Clone(true);
            nextRun.Text = text.Substring(index + 1);

            run.ParentNode.InsertAfter(nextRun, run);

            return nextRun;
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        var template = @"Template.docx";
        var fileOut = "Output 20.8.docx";

        Document doc = new Document(template);

        doc.Accept(new MergeFieldQuoteEnsurer());

        doc.FieldOptions.FieldUpdateCultureSource = FieldUpdateCultureSource.FieldCode;
        doc.MailMerge.FieldMergingCallback = new MergeFieldHandler();
        doc.MailMerge.CleanupOptions =
            MailMergeCleanupOptions.RemoveUnusedFields |
            MailMergeCleanupOptions.RemoveEmptyParagraphs |
            MailMergeCleanupOptions.RemoveUnusedRegions;

        using (var reader = GetDataReader())
            doc.MailMerge.Execute(reader);

        doc.Save(fileOut);
    }
}

Thank you for your solution. I built it into my test project and tested it.
It also works with the data I gave you.
I then tested more extensively with more data. Unfortunately then I get one or two errors.

I will once again make the revised test project with the test data available to you. I would be very grateful if you could help me solve the errors.

Aspose.Word.Testanwendung.20200923.zip (68.2 KB)

There are 2 methods in the test project: RunFormLetters and RunCatalog

Unfortunately, when I run the RunFormLetters method, I get the following error:

System.ArgumentOutOfRangeException:
The index and the length must refer to a position in the string.
Parametername: length

It would work with the following adjustment in the MergeFieldHandler:

rtfDoc.Range.Replace(
		new Regex("([\"«“„»”?])"),
		string.Empty,
		new FindReplaceOptions(FindReplaceDirection.Backward, new ReplacingCallback(rtfDoc)));

But I’m not sure that this is really the solution.

When I run the RunCatalog method, I get the same error as the RunFormLetters method.
With the adjustment in the MergeFieldHandler mentioned above, I get another error:

The reference node is not a child of this node.

@Niebelschutz,

I am afraid, the code in Visual Studio project that you attached is quite complex. We request you to please simplify this code and attach a minimal new project that still reproduces the same problem. Thanks for your cooperation.

I am a little confused.
The test project consists of only 2 methods, in which in principle only the merge is called. The rest comes from Aspose.
Can you tell me what is too complex for me to change?
Should I remove a method?

@Niebelschutz,

We have managed to observe these problems on our end. For the sake of any corrections in Aspose.Words API, we have logged them in our issue tracking system. Your ticket number is WORDSNET-21141. We will further look into the details of these problems and will keep you updated on the status of WORDSNET-21141. We apologize for your inconvenience.

The issues you have found earlier (filed as WORDSNET-21141) have been fixed in this Aspose.Words for .NET 20.11 update and this Aspose.Words for Java 20.11 update.