Find & Replace Text of Bookmarked Content Controls (SDT) in Word DOCX Document using C# .NET

I want to populate all content controls matching the passed in criteria. However it is only populating the first content control, even though there are other content controls matching same passed in criteria.

I had this working previously but since adding further parameters it no longer works.

Attached is the Main Doc that I’m using for testing, along with the Desired Results and the Actual Results. Test Docs.zip (44.4 KB)

This is the code:

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Reflection;
using Aspose.Words;
using Aspose.Words.Markup;
using OutSystems.HubEdition.RuntimePlatform;
using OutSystems.HubEdition.RuntimePlatform.Log;
using System.Diagnostics;
using Aspose.Words.Saving;
using Aspose.Words.Replacing;
using System.Text.RegularExpressions;
using System.Collections;

namespace OutSystems.NssEXT_AsposeWordIntegration
{

    public class CssEXT_AsposeWordIntegration : IssEXT_AsposeWordIntegration
    {

        static void Main(string[] args)
        {
            CssEXT_AsposeWordIntegration asposeExt = new CssEXT_AsposeWordIntegration();
            asposeExt.MssWriteSingleFieldValue(@"D:\sdavids\Test Data\Working\Main Doc.docx", "B1", "Name", "Category 1", "Ben");
            asposeExt.MssWriteSingleFieldValue(@"D:\sdavids\Test Data\Working\Main Doc.docx", "B1", "Name", "Category 2", "Adam");

        }


        /// <summary>
        /// Populates a single Content Control.
        /// </summary>
        /// <param name="ssFilenameAndPath"></param>
        /// <param name="ssBookmarkName"></param>
        /// <param name="ssContentControlTag"></param>
        /// <param name="ssSearchText">Text to be searched for and if found, the first Content Control with the Tag passed following the specified search text is populated.  If an empty string is passed (the default), then no search text is used and the first empty Content Control with the passed Tag that's found following the specified Bookmark Name is populated. A Content Control is considered empty if populated/prompt text is an empty string, or the Tag, or Tag_Title.</param>
		/// <param name="ssInsertionText">The text to write to the Content Control.</param>
		public void MssWriteSingleFieldValue(string ssFilenameAndPath, string ssBookmarkName, string ssContentControlTag, string ssSearchText, string ssInsertionText) {

            LoadLicense();

            if (ssSearchText == "")
            {
                // Populates the first empty instance of Content Control 
                // with the Tag passed under the specified Bookmark.
                WriteSingleFieldValue_NoSearch(ssFilenameAndPath, ssBookmarkName, ssContentControlTag, ssInsertionText);
            }

            else
            {
                // Populates the Content Control with the specified Tag that is 
                // found under the Bookmark and after the specified search text.
                WriteSingleFieldValue_WithSearch(ssFilenameAndPath, ssBookmarkName, ssSearchText, ssContentControlTag, ssInsertionText);
            }

        } // MssWriteSingleFieldValue


        private static void WriteSingleFieldValue_WithSearch(string filenameAndPath, string bookmarkName, string searchText, string contentControlTag, string insertionText)
        {
            // Populate the Content Control with the specified Tag that is 
            // found under the Bookmark and after the specified search text.

            Document doc = new Document(filenameAndPath);
            FindReplaceOptions options = new FindReplaceOptions();

            FindAndReplace findAndReplace = new FindAndReplace
            {
                BookmarkName = bookmarkName,
                InsertionText = insertionText,
                ContentControlTag = contentControlTag,
                TableTitle = ""
            };

            options.ReplacingCallback = findAndReplace;
            doc.Range.Replace(new Regex(searchText), searchText, options);

            doc.Save(filenameAndPath);

        }


        private static void WriteSingleFieldValue_NoSearch(string filenameAndPath, string bookmarkName, string contentControlTag, string insertionText)
        {
            // Populates the first empty instance of Content Control 
            // with the Tag passed under the specified Bookmark.

            Document doc = new Document(filenameAndPath);
            Bookmark bm = doc.Range.Bookmarks[bookmarkName];
            Paragraph start = (Paragraph)bm.BookmarkStart.GetAncestor(NodeType.Paragraph);
            StructuredDocumentTag targetSdt = null;
            CompositeNode node = (CompositeNode)start;
            bool flag = true;

            while (node != null && flag)
            {
                foreach (StructuredDocumentTag sdt in node.GetChildNodes(NodeType.StructuredDocumentTag, true))
                {
                    if ((sdt.Tag.Equals(contentControlTag))
                        && ((sdt.ToString(SaveFormat.Text).Trim().Equals("Click or tap here to enter text."))
                        || (sdt.ToString(SaveFormat.Text).Trim().Equals(""))
                        || (sdt.ToString(SaveFormat.Text).Trim().Equals(contentControlTag))
                        || sdt.ToString(SaveFormat.Text).Trim().Equals(contentControlTag + "_" + contentControlTag)))
                    {
                        targetSdt = sdt;
                        flag = false;
                        break;
                    }
                }
                node = (CompositeNode)node.NextSibling;
            }

            if (targetSdt != null)
            {

                if (targetSdt.Level == MarkupLevel.Cell && targetSdt.FirstChild.NodeType == NodeType.Cell)
                {
                    Paragraph paragraph = ((Aspose.Words.Tables.Cell)targetSdt.FirstChild).FirstParagraph;

                    targetSdt.IsShowingPlaceholderText = false;
                    paragraph.RemoveAllChildren();

                    Run newTextRun = new Run(start.Document, insertionText);
                    newTextRun.Font.Name = "Arial";
                    newTextRun.Font.Size = 10;

                    paragraph.AppendChild(newTextRun);
                }

            }

            doc.Save(filenameAndPath);

        }


        private class FindAndReplace : IReplacingCallback
        {
            public object BookmarkName { get; internal set; }
            public string TableTitle { get; internal set; }
            public string SearchText { get; internal set; }
            public string InsertionText { get; internal set; }
            public string ContentControlTag { get; internal set; }
            public Aspose.Words.Tables.Table TheTable { get; set; }

            ReplaceAction IReplacingCallback.Replacing(ReplacingArgs e)
            {
                // This is a Run node that contains either the beginning or the complete match.
                Node currentNode = e.MatchNode;

                // The first (and may be the only) run can contain text before the match,
                // in this case it is necessary to split the run.
                if (e.MatchOffset > 0)
                    currentNode = SplitRun((Run)currentNode, e.MatchOffset);

                // This array is used to store all nodes of the match for further removing.
                ArrayList runs = new ArrayList();

                // Find all runs that contain parts of the match string.
                int remainingLength = e.Match.Value.Length;

                while ((remainingLength > 0) &&
                    (currentNode != null) &&
                    (currentNode.GetText().Length <= remainingLength))
                {
                    runs.Add(currentNode);
                    remainingLength = remainingLength - currentNode.GetText().Length;

                    // Select the next Run node.
                    // Have to loop because there could be other nodes such as BookmarkStart etc.
                    do
                    {
                        currentNode = currentNode.NextSibling;
                    }

                    while ((currentNode != null) && (currentNode.NodeType != NodeType.Run));
                }

                // Split the last run that contains the match if there is any text left.
                if ((currentNode != null) && (remainingLength > 0))
                {
                    SplitRun((Run)currentNode, remainingLength);
                    runs.Add(currentNode);
                }

                Document doc = ((Document)e.MatchNode.Document);
                Bookmark bm = doc.Range.Bookmarks[BookmarkName.ToString()];
                Paragraph bmPara = (Paragraph)bm.BookmarkStart.GetAncestor(NodeType.Paragraph);

                if (TableTitle != "")
                {
                    //We're adding a row to a table

                    if (bmPara != null)
                    {
                        int bmParaIndex = doc.GetChildNodes(NodeType.Paragraph, true).IndexOf(bmPara);
                        Paragraph start = ((Run)runs[0]).ParentParagraph;
                        int searchTextParaIndex = doc.GetChildNodes(NodeType.Paragraph, true).IndexOf(start);

                        if (searchTextParaIndex > bmParaIndex)
                        {
                            Aspose.Words.Tables.Table targetTable = null;
                            CompositeNode node = (CompositeNode)start;
                            bool flag = true;

                            while (node != null && flag)
                            {
                                if (node.NodeType == NodeType.Table)
                                {
                                    Aspose.Words.Tables.Table table = (Aspose.Words.Tables.Table)node;

                                    if (table.Title == TableTitle)
                                    {
                                        targetTable = table;
                                        flag = false;
                                        break;
                                    }
                                }

                                node = (CompositeNode)node.NextSibling;
                            }

                            if (targetTable != null)
                            {
                                TheTable = targetTable;
                            }
                        }
                    }
                    return ReplaceAction.Skip;
                }

                else
                {
                    // We're populating a content control

                    if (bmPara != null)
                    {
                        int bmParaIndex = doc.GetChildNodes(NodeType.Paragraph, true).IndexOf(bmPara);
                        Paragraph start = ((Run)runs[0]).ParentParagraph;
                        int searchTextParaIndex = doc.GetChildNodes(NodeType.Paragraph, true).IndexOf(start);

                        if (searchTextParaIndex > bmParaIndex)
                        {
                            StructuredDocumentTag targetSdt = null;
                            CompositeNode node = (CompositeNode)start;
                            bool flag = true;

                            while (node != null && flag)
                            {
                                foreach (StructuredDocumentTag sdt in node.GetChildNodes(NodeType.StructuredDocumentTag, true))
                                {
                                    if (sdt.Tag.Equals(ContentControlTag))
                                    {
                                        targetSdt = sdt;
                                        flag = false;

                                        if (targetSdt != null)
                                        {
                                            targetSdt.IsShowingPlaceholderText = false;
                                            Run newTextRun = new Run(start.Document, InsertionText);
                                            newTextRun.Font.Name = "Arial";
                                            newTextRun.Font.Size = 10;

                                            if (targetSdt.Level == MarkupLevel.Inline)
                                            {
                                                targetSdt.RemoveAllChildren();
                                                targetSdt.AppendChild(newTextRun);
                                            }

                                            if (targetSdt.Level == MarkupLevel.Block)
                                            {
                                                targetSdt.RemoveAllChildren();
                                                Paragraph paraInsert = new Paragraph(doc);
                                                paraInsert.ParagraphFormat.Alignment = ParagraphAlignment.Left;
                                                paraInsert.AppendChild(newTextRun);
                                                targetSdt.AppendChild(paraInsert);
                                            }

                                            if (targetSdt.Level == MarkupLevel.Cell)
                                            {
                                                Paragraph paragraph = ((Aspose.Words.Tables.Cell)targetSdt.FirstChild).FirstParagraph;
                                                targetSdt.IsShowingPlaceholderText = false;
                                                paragraph.RemoveAllChildren();
                                                paragraph.AppendChild(newTextRun);
                                            }

                                            //return ReplaceAction.Stop;
                                        }
                                    }
                                }

                                node = (CompositeNode)node.NextSibling;
                            }

                        }
                    }
                    return ReplaceAction.Skip;
                }
            }


            private static Run SplitRun(Run run, int position)
            {
                Run afterRun = (Run)run.Clone(true);
                afterRun.Text = run.Text.Substring(position);
                run.Text = run.Text.Substring((0), (0) + (position));
                run.ParentNode.InsertAfter(afterRun, run);
                return afterRun;
            }
        }


        private ParagraphAlignment GetAlignment(RCAsposeAlignmentRecord alignment)
        {
            
            if (alignment.ssSTAsposeAlignment.ssLeft == true)
            {
                return ParagraphAlignment.Left;
            }
            else
            {
                if (alignment.ssSTAsposeAlignment.ssCentre == true)
                {
                    return ParagraphAlignment.Center;
                }
                else
                {
                    if (alignment.ssSTAsposeAlignment.ssRight == true)
                    {
                        return ParagraphAlignment.Right;
                    }
                    else
                    {
                        return ParagraphAlignment.Left;
                    }
                }
            }
        }


        private void LoadLicense()
        {
            ////GenericExtendedActions.LogMessage(AppInfo.GetAppInfo().OsContext, "Inside LoadLicense", "LoadLicense"); //DEBUGGING - DELETE THIS LINE

            // Loads the embedded Aspose license
            Assembly assembly = Assembly.GetExecutingAssembly();
            string resourceName = assembly.GetName().Name + ".Aspose.Words.lic";
            using (Stream resourceStream = assembly.GetManifestResourceStream(resourceName))

            {
                Aspose.Words.License license = new Aspose.Words.License();
                license.SetLicense(resourceStream);
            }

                //GenericExtendedActions.LogMessage(AppInfo.GetAppInfo().OsContext, "End", "Load License"); // DEBUGGING - DELETE ME
        }


    } // CssEXT_AsposeWordIntegration

} // OutSystems.NssEXT_AsposeWordIntegration

@SCDGLC,

We are working on your query and will get back to you soon.

Many thanks!!

Any joy?

Sorry don’t mean to harrass you but I have a client waiting on this.

@SCDGLC,

I have modified code of FindAndReplace class a bit (see Find And Replace.zip (1.7 KB)). I just commented the flag = false; statement inside foreach loop and added the following lines right after the foreach loop:

node = (CompositeNode)node.NextSibling;
if (node.GetChildNodes(NodeType.StructuredDocumentTag, true).Count == 0)
    flag = false;

Many thanks, however that doesn’t work as it adds ‘Ben’ under the wrong bookmark. The parameters passed are ‘B1’ bookmark under the search text of ‘Category 1’ but it’s appearing under ‘B2’ bookmark under the search text of ‘Category 1’. However ‘Adam’ is appearing in the correct place.

@SCDGLC,

I have made a few more changes to FindAndReplace class. Please check: Find And Replace v2.zip (1.7 KB)

Great - thank you!

Although the test data works ok, in practice it does not work because it never reaches the code to enter text into the content control.

In first example it’s because the sdt markup level is inline and not cell and the sdt node type is run and not cell. I am unclear on what constitutes markup levels and node types for each element, but should the break above that code should be one level up as it seems that may be why it’s not the correct types?

These are the 2 lines of calling code:

        asposeExt.MssWriteSingleFieldValue(@"D:\sdavids\Test Data\Working\Test Slip2.docx", "Insured", "Dummy CC1", "", "qqqqq");
        asposeExt.MssWriteSingleFieldValue(@"D:\sdavids\Test Data\Working\Test Slip2.docx", "LimitOfLiability", "DummyCC3", "Delay in Start Up", "ccccc");

And here is the new test document I’m using:
Test Slip2.zip (96.2 KB)

I expect the Dummy CC1 and DummyCC3 content controls to be populated as per the calling code, however they are not.

@SCDGLC,

You can use the following code to determine the Markup Level of content controls:

Document doc = new Document("C:\\Temp\\Test Slip2\\Test Slip2.docx");
foreach (StructuredDocumentTag sdt in doc.FirstSection.Body.GetChildNodes(NodeType.StructuredDocumentTag, true))
    Console.WriteLine(sdt.Tag + " -> " + sdt.Level);

You also need to adjust/modify the existing code to deal with content controls of different markup levels. Can you please also provide your expected document showing the desired output here for our reference. You can create this document manually by using MS Word.

The expected output is simply that the Dummy CC1 is populated with “qqqqq” and DummyCC3 is populated with “ccccc” as per the calling code.

Tbh I do not understand why I need to check for the markup level at all? This piece of code was written by yourselves, but it could be any level of markup and I don’t believe it matters and I don’t actually understand why it was added (and no questions were asked of me at the time the code was written as to what it might be).

I’m unsure about node type as there are a lot of options. I only want to search under a given bookmark and therefore not within the header and footer for example so it may be that it will suffice just to exclude header/footer type.

@SCDGLC,

For this document (Test Slip2.docx), please check the following code will process the content controls within the scope of specified bookmark:

MssWriteSingleFieldValue(@"C:\Temp\Test Slip2\Test Slip2.docx", "Insured", "Dummy CC1", "", "qqqqq");
MssWriteSingleFieldValue(@"C:\Temp\Test Slip2\Test Slip2.docx", "LimitOfLiability", "DummyCC3", "Delay in Start Up", "ccccc");

public static void MssWriteSingleFieldValue(string ssFilenameAndPath, string ssBookmarkName, string ssContentControlTag, string ssSearchText, string ssInsertionText)
{
    Document doc = new Document(ssFilenameAndPath);

    Bookmark bookmark = doc.Range.Bookmarks[ssBookmarkName];
    Node targetNode = bookmark.BookmarkStart;
    // Lets find the node containing the search string
    while (ssSearchText != "" && targetNode != null)
    {
        if (targetNode == bookmark.BookmarkEnd)
            break;

        if (targetNode.Range.Replace(ssSearchText, ssSearchText) > 0)
            break;

        Node nextNode = targetNode.NextPreOrder(targetNode.Document);
        targetNode = nextNode;
    }

    // Process only SDTs inside a Bookmark that appear after the search string Node
    Node currentNode = targetNode;
    while (currentNode != null)
    {
        if (currentNode == bookmark.BookmarkEnd)
            break;

        if (currentNode.NodeType == NodeType.StructuredDocumentTag)
        {
            StructuredDocumentTag targetSdt = (StructuredDocumentTag)currentNode;
            if (targetSdt.Tag.Equals(ssContentControlTag))
            {
                Run newTextRun = new Run(doc, ssInsertionText);
                newTextRun.Font.Name = "Arial";
                newTextRun.Font.Size = 10;

                if (targetSdt.Level == MarkupLevel.Inline)
                {
                    targetSdt.IsShowingPlaceholderText = false;
                    targetSdt.RemoveAllChildren();
                    targetSdt.AppendChild(newTextRun);
                }
                else if (targetSdt.Level == MarkupLevel.Block)
                {
                    targetSdt.IsShowingPlaceholderText = false;
                    targetSdt.RemoveAllChildren();
                    Paragraph paraInsert = new Paragraph(doc);
                    paraInsert.ParagraphFormat.Alignment = ParagraphAlignment.Left;
                    paraInsert.AppendChild(newTextRun);
                    targetSdt.AppendChild(paraInsert);
                }
                else if (targetSdt.Level == MarkupLevel.Cell) { }
                else if (targetSdt.Level == MarkupLevel.Row) { }
                else { }
            }
        }

        Node nextNode = currentNode.NextPreOrder(currentNode.Document);
        currentNode = nextNode;
    }

    doc.Save(@"C:\Temp\Test Slip2\20.8.docx");
}

Please let us know if you have any troubles and we will be glad to look into this further for you.