21
votes

I have created a docx file from a word template, now I am accessing the copied docx file and want to replace certain text with some other data.

I am unable to get the hint as to how to access the text from the doument main part?

Any help would be appreciable.

Below is my code till now.

private void CreateSampleWordDocument()
    {
        //string sourceFile = Path.Combine("D:\\GeneralLetter.dot");
        //string destinationFile = Path.Combine("D:\\New.doc");
        string sourceFile = Path.Combine("D:\\GeneralWelcomeLetter.docx");
        string destinationFile = Path.Combine("D:\\New.docx");
        try
        {
            // Create a copy of the template file and open the copy
            File.Copy(sourceFile, destinationFile, true);
            using (WordprocessingDocument document = WordprocessingDocument.Open(destinationFile, true))
            {
                // Change the document type to Document
                document.ChangeDocumentType(DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
                //Get the Main Part of the document
                MainDocumentPart mainPart = document.MainDocumentPart;
                mainPart.Document.Save();
            }
        }
        catch
        {
        }
    }

Now how to find certain text and replace the same? I am unable to get via Link, so some code hint would be appreciable.

10

10 Answers

24
votes

Just to give you the idea of how to do it, please try:

  using ( WordprocessingDocument doc =
                    WordprocessingDocument.Open(@"yourpath\testdocument.docx", true))
            {
                var body = doc.MainDocumentPart.Document.Body;
                var paras = body.Elements<Paragraph>();

                foreach (var para in paras)
                {
                    foreach (var run in para.Elements<Run>())
                    {
                        foreach (var text in run.Elements<Text>())
                        {
                            if (text.Text.Contains("text-to-replace"))
                            {
                                text.Text = text.Text.Replace("text-to-replace", "replaced-text");
                            }
                        }
                    }
                }
            }
        }

Please note the text is case sensitive. The text formatting won't be changed after the replace. Hope this helps you.

19
votes

In addition to Flowerking's answer:

When your Word file has textboxes in it, his solution would not work. Because textbox has TextBoxContent element so it will not appear at foreach loop of Runs.

But when writing

using ( WordprocessingDocument doc =
                    WordprocessingDocument.Open(@"yourpath\testdocument.docx", true))
{
    var document = doc.MainDocumentPart.Document

    foreach (var text in document.Descendants<Text>()) // <<< Here
    {
        if (text.Text.Contains("text-to-replace"))
        {
            text.Text = text.Text.Replace("text-to-replace", "replaced-text");
        }
    } 
}
        

it will loop all the texts in document(whether it is in textbox or not) so it will replace the texts.

Note that if the text is split between Runs or Textboxes, this also won't work. You need a better solution for those cases. One solution to split texts could be fixing the "template", sometimes, simply deleting the placeholder and re-creating it works wonders.

4
votes

Maybe this solution is easier:
1. a StreamReader reads all the text,
2. using a Regex you case-insensitively replace the new text instead of the old tex
3. a StreamWriter writes again the modified text into the document.

 using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(document, true))
{
    string docText = null;
    using (StreamReader sr = new StreamReader(wordDoc.MainDocumentPart.GetStream()))
        docText = sr.ReadToEnd();

    foreach (var t in findesReplaces)
        docText = new Regex(findText, RegexOptions.IgnoreCase).Replace(docText, replaceText);

    using (StreamWriter sw = new StreamWriter(wordDoc.MainDocumentPart.GetStream(FileMode.Create)))
        sw.Write(docText);
}
3
votes

Here is a solution that can find and replace tags in an open xml (word) document across text runs (including text boxes)

namespace Demo
{
    using System;
    using System.Collections.Generic;
    using System.IO;
    using System.Linq;
    using System.Text.RegularExpressions;
    using DocumentFormat.OpenXml.Packaging;
    using DocumentFormat.OpenXml.Wordprocessing;

    public class WordDocumentHelper
    {
        class DocumentTag
        {
            public DocumentTag()
            {
                ReplacementText = "";
            }

            public string Tag { get; set; }
            public string Table { get; set; }
            public string Column { get; set; }
            public string ReplacementText { get; set; }

            public override string ToString()
            {
                return ReplacementText ?? (Tag ?? "");
            }
        }

        private const string TAG_PATTERN = @"\[(.*?)[\.|\:](.*?)\]";
        private const string TAG_START = @"[";
        private const string TAG_END = @"]";

        /// <summary>
        /// Clones a document template into the temp folder and returns the newly created clone temp filename and path.
        /// </summary>
        /// <param name="templatePath"></param>
        /// <returns></returns>
        public string CloneTemplateForEditing(string templatePath)
        {
            var tempFile = Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()) + Path.GetExtension(templatePath);
            File.Copy(templatePath, tempFile);
            return tempFile;
        }

        /// <summary>
        /// Opens a given filename, replaces tags, and saves. 
        /// </summary>
        /// <param name="filename"></param>
        /// <returns>Number of tags found</returns>
        public int FindAndReplaceTags(string filename)
        {
            var allTags = new List<DocumentTag>();

            using (WordprocessingDocument doc = WordprocessingDocument.Open(path: filename, isEditable: true))
            {
                var document = doc.MainDocumentPart.Document;

                // text may be split across multiple text runs so keep a collection of text objects
                List<Text> tagParts = new List<Text>();

                foreach (var text in document.Descendants<Text>())
                {
                    // search for any fully formed tags in this text run
                    var fullTags = GetTags(text.Text);

                    // replace values for fully formed tags
                    fullTags.ForEach(t => {
                        t = GetTagReplacementValue(t);
                        text.Text = text.Text.Replace(t.Tag, t.ReplacementText);
                        allTags.Add(t);
                    });

                    // continue working on current partial tag
                    if (tagParts.Count > 0)
                    {
                        // working on a tag
                        var joinText = string.Join("", tagParts.Select(x => x.Text)) + text.Text;

                        // see if tag ends with this block
                        if (joinText.Contains(TAG_END))
                        {
                            var joinTag = GetTags(joinText).FirstOrDefault(); // should be just one tag (or none)
                            if (joinTag == null)
                            {
                                throw new Exception($"Misformed document tag in block '{string.Join("", tagParts.Select(x => x.Text)) + text.Text}' ");
                            }

                            joinTag = GetTagReplacementValue(joinTag);
                            allTags.Add(joinTag);

                            // replace first text run in the tagParts set with the replacement value. 
                            // (This means the formatting used on the first character of the tag will be used)
                            var firstRun = tagParts.First();
                            firstRun.Text = firstRun.Text.Substring(0, firstRun.Text.LastIndexOf(TAG_START));
                            firstRun.Text += joinTag.ReplacementText;

                            // replace trailing text runs with empty strings
                            tagParts.Skip(1).ToList().ForEach(x => x.Text = "");

                            // replace all text up to and including the first index of TAG_END
                            text.Text = text.Text.Substring(text.Text.IndexOf(TAG_END) + 1);

                            // empty the tagParts list so we can start on a new tag
                            tagParts.Clear();
                        }
                        else
                        {
                            // no tag end so keep getting text runs
                            tagParts.Add(text);
                        }
                    }

                    // search for new partial tags
                    if (text.Text.Contains("["))
                    {
                        if (tagParts.Any())
                        {
                            throw new Exception($"Misformed document tag in block '{string.Join("", tagParts.Select(x => x.Text)) + text.Text}' ");
                        }
                        tagParts.Add(text);
                        continue;
                    }

                }

                // save the temp doc before closing
                doc.Save();
            }

            return allTags.Count;
        }

        /// <summary>
        /// Gets a unique set of document tags found in the passed fileText using Regex
        /// </summary>
        /// <param name="fileText"></param>
        /// <returns></returns>
        private List<DocumentTag> GetTags(string fileText)
        {
            List<DocumentTag> tags = new List<DocumentTag>();

            if (string.IsNullOrWhiteSpace(fileText))
            {
                return tags;
            }

            // TODO: custom regex for tag matching 
            // this example looks for tags in the formation "[table.column]" or "[table:column]" and captures the full tag, "table", and "column" into match Groups
            MatchCollection matches = Regex.Matches(fileText, TAG_PATTERN);
            foreach (Match match in matches)
            {
                try
                {

                    if (match.Groups.Count < 3
                        || string.IsNullOrWhiteSpace(match.Groups[0].Value)
                        || string.IsNullOrWhiteSpace(match.Groups[1].Value)
                        || string.IsNullOrWhiteSpace(match.Groups[2].Value))
                    {
                        continue;
                    }

                    tags.Add(new DocumentTag
                    {
                        Tag = match.Groups[0].Value,
                        Table = match.Groups[1].Value,
                        Column = match.Groups[2].Value
                    });
                }
                catch
                {

                }
            }

            return tags;
        }

        /// <summary>
        /// Set the Tag replacement value of the pasted tag
        /// </summary>
        /// <returns></returns>
        private DocumentTag GetTagReplacementValue(DocumentTag tag)
        {
            // TODO: custom routine to update tag Replacement Value

            tag.ReplacementText = "foobar";

            return tag;
        }
    }
}
3
votes

The easiest and most accurate way I have found so far is to use Open-Xml-PowerTools. Personally, i'm with dotnet core, so I use this nuget package.

using OpenXmlPowerTools;
// ...

protected byte[] SearchAndReplace(byte[] file, IDictionary<string, string> translations)
{
    WmlDocument doc = new WmlDocument(file.Length.ToString(), file);

    foreach (var translation in translations)
        doc = doc.SearchAndReplace(translation.Key, translation.Value, true);

    return doc.DocumentByteArray;
}

Example of use:

var templateDoc = File.ReadAllBytes("templateDoc.docx");
var generatedDoc = SearchAndReplace(templateDoc, new Dictionary<string, string>(){
    {"text-to-replace-1", "replaced-text-1"},
    {"text-to-replace-2", "replaced-text-2"},
});
File.WriteAllBytes("generatedDoc.docx", generatedDoc);

For more information, see Search and Replace Text in an Open XML WordprocessingML Document

1
votes

My class for replacing long pharses in word document, that word split in different text blocks:

class itself:

using System.Collections.Generic;
using System.Text;
using DocumentFormat.OpenXml.Packaging;
using DocumentFormat.OpenXml.Wordprocessing;

namespace WebBackLibrary.Service
{
    public class WordDocumentService
    {
        private class WordMatchedPhrase
        {
            public int charStartInFirstPar { get; set; }
            public int charEndInLastPar { get; set; }

            public int firstCharParOccurance { get; set; }
            public int lastCharParOccurance { get; set; }
        }

        public WordprocessingDocument ReplaceStringInWordDocumennt(WordprocessingDocument wordprocessingDocument, string replaceWhat, string replaceFor)
        {
            List<WordMatchedPhrase> matchedPhrases = FindWordMatchedPhrases(wordprocessingDocument, replaceWhat);

            Document document = wordprocessingDocument.MainDocumentPart.Document;
            int i = 0;
            bool isInPhrase = false;
            bool isInEndOfPhrase = false;
            foreach (Text text in document.Descendants<Text>()) // <<< Here
            {
                char[] textChars = text.Text.ToCharArray();
                List<WordMatchedPhrase> curParPhrases = matchedPhrases.FindAll(a => (a.firstCharParOccurance.Equals(i) || a.lastCharParOccurance.Equals(i)));
                StringBuilder outStringBuilder = new StringBuilder();
                
                for (int c = 0; c < textChars.Length; c++)
                {
                    if (isInEndOfPhrase)
                    {
                        isInPhrase = false;
                        isInEndOfPhrase = false;
                    }

                    foreach (var parPhrase in curParPhrases)
                    {
                        if (c == parPhrase.charStartInFirstPar && i == parPhrase.firstCharParOccurance)
                        {
                            outStringBuilder.Append(replaceFor);
                            isInPhrase = true;
                        }
                        if (c == parPhrase.charEndInLastPar && i == parPhrase.lastCharParOccurance)
                        {
                            isInEndOfPhrase = true;
                        }

                    }
                    if (isInPhrase == false && isInEndOfPhrase == false)
                    {
                        outStringBuilder.Append(textChars[c]);
                    }
                }
                text.Text = outStringBuilder.ToString();
                i = i + 1;
            }

            return wordprocessingDocument;
        }

        private List<WordMatchedPhrase> FindWordMatchedPhrases(WordprocessingDocument wordprocessingDocument, string replaceWhat)
        {
            char[] replaceWhatChars = replaceWhat.ToCharArray();
            int overlapsRequired = replaceWhatChars.Length;
            int overlapsFound = 0;
            int currentChar = 0;
            int firstCharParOccurance = 0;
            int lastCharParOccurance = 0;
            int startChar = 0;
            int endChar = 0;
            List<WordMatchedPhrase> wordMatchedPhrases = new List<WordMatchedPhrase>();
            //
            Document document = wordprocessingDocument.MainDocumentPart.Document;
            int i = 0;
            foreach (Text text in document.Descendants<Text>()) // <<< Here
            {
                char[] textChars = text.Text.ToCharArray();
                for (int c = 0; c < textChars.Length; c++)
                {
                    char compareToChar = replaceWhatChars[currentChar];
                    if (textChars[c] == compareToChar)
                    {
                        currentChar = currentChar + 1;
                        if (currentChar == 1)
                        {
                            startChar = c;
                            firstCharParOccurance = i;
                        }
                        if (currentChar == overlapsRequired)
                        {
                            endChar = c;
                            lastCharParOccurance = i;
                            WordMatchedPhrase matchedPhrase = new WordMatchedPhrase
                            {
                                firstCharParOccurance = firstCharParOccurance,
                                lastCharParOccurance = lastCharParOccurance,
                                charEndInLastPar = endChar,
                                charStartInFirstPar = startChar
                            };
                            wordMatchedPhrases.Add(matchedPhrase);
                            currentChar = 0;
                        }
                    }
                    else
                    {
                        currentChar = 0;

                    }
                }
                i = i + 1;
            }

            return wordMatchedPhrases;

        }

    }
}

And example of easy-to-use:

public void EditWordDocument(UserContents userContents)
        {
            string filePath = Path.Combine(userContents.PathOnDisk, userContents.FileName);
            WordDocumentService wordDocumentService = new WordDocumentService();
            if (userContents.ContentType.Contains("word") && File.Exists(filePath))
            {
                string saveAs = "modifiedTechWord.docx";
                //
                using (WordprocessingDocument doc = WordprocessingDocument.Open(filePath, true)) //open source word file
                {
                    Document document = doc.MainDocumentPart.Document;
                    OpenXmlPackage res = doc.SaveAs(Path.Combine(userContents.PathOnDisk, saveAs)); // copy it
                    res.Close();
                }
                using (WordprocessingDocument doc = WordprocessingDocument.Open(Path.Combine(userContents.PathOnDisk, saveAs), true)) // open copy
                {
                    string replaceWhat = "{transform:CandidateFio}";
                    string replaceFor = "ReplaceToFio";
                    var result = wordDocumentService.ReplaceStringInWordDocumennt(doc, replaceWhat, replaceFor); //replace words in copy
                }
            }
        }
1
votes

I'm testing this out for doc generation, but my placeholders were split across run and text nodes. I didn't want to load the whole doc as a single string for regex find/replace, so I worked with the OpenXml api. My idea is to:

  1. clean up the placeholder nodes as a one time operation on the document
  2. find/replace by node value each time it's generated, now that the source is clean.

Testing showed that placeholders were split across runs and text nodes, but not paragraphs. I also found that subsequent placeholders didn't share text nodes, so I didn't handle that. Placeholders follow the pattern {{placeholder_name}}.

First, I needed to get all the text nodes in the paragraph (per @sertsedat):

    var nodes = paragraph.Descendants<Text>();

Testing showed that this function preserves order, which was perfect for my use case since I could iterate through the collection looking for start/stop indicators, and group those nodes that were part of my placeholders.

The grouping function looked for {{ and }} in the text node values to identify nodes that were part of the placeholder and should be deleted, and other nodes, which should be ignored.

Once the start of a node was found, all subsequent nodes, up to and including the termination, would need to be deleted (marked by adding to the TextNodes list), the value of those nodes included in the placeholder StringBuilder, and any text part of the first/last node that was not a part of the placeholder would also need to be saved (thus the string properties). Any incomplete groups when a new placeholder was found or at the end of the sequence should throw an error.

Finally, I used the grouping to update the original doc

foreach (var placeholder in GroupPlaceholders(paragraph.Descendants<Text>()))
{
    var firstTextNode = placeholder.TextNodes[0];
    if (placeholder.PrecedingText != null)
    {
        firstTextNode.Parent.InsertBefore(new Text(placeholder.PrecedingText), firstTextNode);
    }
    firstTextNode.Parent.InsertBefore(placeholder.PlaceholderText, firstTextNode);
    if (placeholder.SubsequentText != null)
    {
        firstTextNode.Parent.InsertBefore(new Text(placeholder.SubsequentText), firstTextNode);
    }
    foreach (var textNode in placeholder.TextNodes) {
        textNode.Remove();                      
    }
}
0
votes

If the text you are looking for is placed between brackets and Word Splits your text in multiple runs...;

Search the text (ienumerable(of text))

for (int i = 0; i <= SearchIn.Count - 1; i++) {

    if (!(i + 2 > SearchIn.Count - 1)) {
        Text TXT = SearchIn(i);
        Text TXT1 = SearchIn(i + 1);
        Text TXT2 = SearchIn(i + 2);

        if (Strings.Trim(TXT.Text) == "[" & Strings.Trim(TXT2.Text) == "]") {
            TXT1.Text = TXT.Text + TXT1.Text + TXT2.Text;

            TXT.Text = "";
            TXT2.Text = "";
        }
    }
}
0
votes
Dim doc As WordprocessingDocument = WordprocessingDocument.Open("Chemin", True, New OpenSettings With {.AutoSave = True})

Dim d As Document = doc.MainDocumentPart.Document

Dim txt As Text = d.Descendants(Of Text).Where(Function(t) t.Text = "txtNom").FirstOrDefault

If txt IsNot Nothing Then
 txt.Text = txt.Text.Replace("txtNom", "YASSINE OULARBI")
End If

doc.Close()
-1
votes

here is solution from msdn.

Example from there:

public static void SearchAndReplace(string document)
{
    using (WordprocessingDocument wordDoc = WordprocessingDocument.Open(document, true))
    {
        string docText = null;
        using (StreamReader sr = new StreamReader(wordDoc.MainDocumentPart.GetStream()))
        {
            docText = sr.ReadToEnd();
        }

        Regex regexText = new Regex("Hello world!");
        docText = regexText.Replace(docText, "Hi Everyone!");

        using (StreamWriter sw = new StreamWriter(wordDoc.MainDocumentPart.GetStream(FileMode.Create)))
        {
            sw.Write(docText);
        }
    }
}