4

I'm developing a web application with asp.net and I have a file called Template.docx that works like a template to generate other reports. Inside this Template.docx I have some MergeFields (Title, CustomerName, Content, Footer, etc) to replace for some dynamic content in C#.

I would like to know, how can I put a content in a mergefield in docx ?

I don't know if MergeFields is the right way to do this or if there is another way. If you can suggest me, I appreciate!

PS: I have openxml referenced in my web application.

Edits:

private MemoryStream LoadFileIntoStream(string fileName)
{
    MemoryStream memoryStream = new MemoryStream();
    using (FileStream fileStream = File.OpenRead(fileName))
    {
        memoryStream.SetLength(fileStream.Length);
        fileStream.Read(memoryStream.GetBuffer(), 0, (int) fileStream.Length);

        memoryStream.Flush();
        fileStream.Close();
    }
    return memoryStream;
}

public MemoryStream GenerateWord()
{
    string templateDoc = "C:\\temp\\template.docx"; 
    string reportFileName = "C:\\temp\\result.docx";

    var reportStream = LoadFileIntoStream(templateDoc);

    // Copy a new file name from template file
    //File.Copy(templateDoc, reportFileName, true);

    // Open the new Package
    Package pkg = Package.Open(reportStream, FileMode.Open, FileAccess.ReadWrite);

    // Specify the URI of the part to be read
    Uri uri = new Uri("/word/document.xml", UriKind.Relative);
    PackagePart part = pkg.GetPart(uri);

    XmlDocument xmlMainXMLDoc = new XmlDocument();
    xmlMainXMLDoc.Load(part.GetStream(FileMode.Open, FileAccess.Read));

    // replace some keys inside xml (it will come from database, it's just a test)
    xmlMainXMLDoc.InnerXml = xmlMainXMLDoc.InnerXml.Replace("field_customer", "My Customer Name");
    xmlMainXMLDoc.InnerXml = xmlMainXMLDoc.InnerXml.Replace("field_title", "Report of Documents");
    xmlMainXMLDoc.InnerXml = xmlMainXMLDoc.InnerXml.Replace("field_content", "Content of Document");

    // Open the stream to write document
    StreamWriter partWrt = new StreamWriter(part.GetStream(FileMode.Open, FileAccess.Write));
    //doc.Save(partWrt);
    xmlMainXMLDoc.Save(partWrt);

    partWrt.Flush();
    partWrt.Close();
    reportStream.Flush();
    pkg.Close();

    return reportStream;
}

PS: When I convert MemoryStream to a file, I got a corrupted file. Thanks!

Felipe Oriani
  • 37,948
  • 19
  • 131
  • 194
  • I am presuming that using Word to perfrom a mail merge is not an option here ? – user957902 Dec 06 '11 at 15:39
  • No, Actually, I need to fill these fields, I would like to replace it to my dynamic content. I don't know if MergeFields is the right way to do this. Thank you! – Felipe Oriani Dec 06 '11 at 16:09
  • Typically the way you would put content in the MergeField is by peforming a mail merge with a datasource that contians you dynamic content. What libaray are you using to manipulate the docx file from the asp.net code ? – user957902 Dec 06 '11 at 16:57
  • Well, I have DocumentFormat.OpenXml referenced in my project, but it is not necessary to be used. I found a code that does what I need and I got it working :) ... but it uses a FileStream, so, I was wondering if is there any way to generate a docx in MemoryStream and send it to client-side? Look my edits! Thanks @user957902 – Felipe Oriani Dec 06 '11 at 18:26
  • Take a look at the DocumentFormat.OpenXml.Wordprocessing.MailMerge class. You might also considering using DocumentVariables instead, though that is somewhat deprecated. – user957902 Dec 06 '11 at 18:57
  • do you have any sample code I can see? Thank you! – Felipe Oriani Dec 06 '11 at 19:29

3 Answers3

7

I know this is an old post, but I could not get the accepted answer to work for me. The project linked would not even compile (which someone has already commented in that link). Also, it seems to use other Nuget packages like WPFToolkit.

So I'm adding my answer here in case someone finds it useful. This only uses the OpenXML SDK 2.5 and also the WindowsBase v4. This works on MS Word 2010 and later.

string sourceFile = @"C:\Template.docx";
string targetFile = @"C:\Result.docx";
File.Copy(sourceFile, targetFile, true);
using (WordprocessingDocument document = WordprocessingDocument.Open(targetFile, true))
{
    // If your sourceFile is a different type (e.g., .DOTX), you will need to change the target type like so:
    document.ChangeDocumentType(WordprocessingDocumentType.Document);

    // Get the MainPart of the document
    MainDocumentPart mainPart = document.MainDocumentPart;
    var mergeFields = mainPart.RootElement.Descendants<FieldCode>();

    var mergeFieldName = "SenderFullName";
    var replacementText = "John Smith";

    ReplaceMergeFieldWithText(mergeFields, mergeFieldName, replacementText);                   

    // Save the document
    mainPart.Document.Save();

}


private void ReplaceMergeFieldWithText(IEnumerable<FieldCode> fields, string mergeFieldName, string replacementText)
{
    var field = fields
        .Where(f => f.InnerText.Contains(mergeFieldName))
        .FirstOrDefault();

    if (field != null)
    {
        // Get the Run that contains our FieldCode
        // Then get the parent container of this Run
        Run rFldCode = (Run)field.Parent; 

        // Get the three (3) other Runs that make up our merge field
        Run rBegin = rFldCode.PreviousSibling<Run>();
        Run rSep = rFldCode.NextSibling<Run>();
        Run rText = rSep.NextSibling<Run>();
        Run rEnd = rText.NextSibling<Run>();

        // Get the Run that holds the Text element for our merge field
        // Get the Text element and replace the text content 
        Text t = rText.GetFirstChild<Text>();
        t.Text = replacementText;

        // Remove all the four (4) Runs for our merge field
        rFldCode.Remove();
        rBegin.Remove();
        rSep.Remove();
        rEnd.Remove();
    }

}

What the code above does is basically this:

  • Identify the 4 Runs that make up the merge field named "SenderFullName".
  • Identify the Run that contains the Text element for our merge field.
  • Remove the 4 Runs.
  • Update the text property of the Text element for our merge field.

UPDATE

For anyone interested, here is a simple static class I used to help me with replacing merge fields.

Frank Fajardo
  • 7,034
  • 1
  • 29
  • 47
  • Thank you very much @Frank Fajardo. Minor changes you migt like to make - 1) var container is never used & declaration can be deleted from the code. 2) declaration of rText should come between rSep and rEnd (otherwise using an undeclared variable). – Brent Sep 19 '16 at 21:42
  • @Brent, you're right. I did copy and paste and modified to kinda make it clear but did not do it correctly. Thanks! – Frank Fajardo Sep 19 '16 at 22:46
  • @Brent, I added a link to the code I eventually used, in case you are interested. – Frank Fajardo Sep 19 '16 at 23:55
  • Much appreciated Frank - not sure why it took me so long to find this sample but its perfect for understanding OpenXML merge fields – Bassie Nov 19 '18 at 10:04
  • Such code ignores the `FieldCode` second in one line (can be a paragraph). I don't know why. Can you help me? –  Jul 24 '19 at 07:40
  • I found a solution: [link](https://stackoverflow.com/a/57180496/11563179) –  Jul 24 '19 at 10:14
4

Frank Fajardo's answer was 99% of the way there for me, but it is important to note that MERGEFIELDS can be SimpleFields or FieldCodes.

In the case of SimpleFields, the text runs displayed to the user in the document are children of the SimpleField.

In the case of FieldCodes, the text runs shown to the user are between the runs containing FieldChars with the Separate and the End FieldCharValues. Occasionally, several text containing runs exist between the Separate and End Elements.

The code below deals with these problems. Further details of how to get all the MERGEFIELDS from the document, including the header and footer is available in a GitHub repository at https://github.com/mcshaz/SimPlanner/blob/master/SP.DTOs/Utilities/OpenXmlExtensions.cs

private static Run CreateSimpleTextRun(string text)
{
    Run returnVar = new Run();
    RunProperties runProp = new RunProperties();
    runProp.Append(new NoProof());
    returnVar.Append(runProp);
    returnVar.Append(new Text() { Text = text });
    return returnVar;
}

private static void InsertMergeFieldText(OpenXmlElement field, string replacementText)
{
    var sf = field as SimpleField;
    if (sf != null)
    {
        var textChildren = sf.Descendants<Text>();
        textChildren.First().Text = replacementText;
        foreach (var others in textChildren.Skip(1))
        {
            others.Remove();
        }
    }
    else
    {
        var runs = GetAssociatedRuns((FieldCode)field);
        var rEnd = runs[runs.Count - 1];
        foreach (var r in runs
            .SkipWhile(r => !r.ContainsCharType(FieldCharValues.Separate))
            .Skip(1)
            .TakeWhile(r=>r!= rEnd))
        {
            r.Remove();
        }
        rEnd.InsertBeforeSelf(CreateSimpleTextRun(replacementText));
    }
}

private static IList<Run> GetAssociatedRuns(FieldCode fieldCode)
{
    Run rFieldCode = (Run)fieldCode.Parent;
    Run rBegin = rFieldCode.PreviousSibling<Run>();
    Run rCurrent = rFieldCode.NextSibling<Run>();

    var runs = new List<Run>(new[] { rBegin, rCurrent });

    while (!rCurrent.ContainsCharType(FieldCharValues.End))
    {
        rCurrent = rCurrent.NextSibling<Run>();
        runs.Add(rCurrent);
    };

    return runs;
}

private static bool ContainsCharType(this Run run, FieldCharValues fieldCharType)
{
    var fc = run.GetFirstChild<FieldChar>();
    return fc == null
        ? false
        : fc.FieldCharType.Value == fieldCharType;
}
Brent
  • 4,611
  • 4
  • 38
  • 55
3

You could try http://www.codeproject.com/KB/office/Fill_Mergefields.aspx which uses the Open XML SDK to do this.

JasonPlutext
  • 15,352
  • 4
  • 44
  • 84