How do you mail merge a word document in c#
Asked Answered
E

3

15

What I'm trying to achieve

In my c# application I would like to generate a report (word document) from data in my application, I figured that the best way to do this would be to perform something like a mail merge using the data source from my application.

What I've tried

  1. I tried following this Mail Merge into word however this uses GemBox which you need to pay for
  2. I have tried using Microsoft.Office.Interop.Word however I fell short when I didn't know how to reference the saved template document:

    Dictionary<string, string> MailMerge = new Dictionary<string, string>()
        {
            { "ID", "123" },
            { "Name", "Test" },
            { "Address1", "Test" },
            { "Address2", "Test" },
            { "Address3", "Test" },
            { "Address4", "Test" },
            { "PostCode", "Test" },
            { "Year End", "Test" },
            { "SicCode", "123" },
        };
    
        Document doc = new Document();
        doc.MailMerge.Execute(MailMerge);
    

Summary

I'm looking for some guidance as to what to research further as I believe there must be a 'standard' way of doing this.

Extinguisher answered 7/8, 2015 at 9:29 Comment(2)
So what exactly do you want to do? Create a document from your template and replace the Merge Fields with your Dictionary? Is this right?Grecian
Correct, just like a mail mergeExtinguisher
G
11

This is quite simple by using Microsoft.Office.Interop.Word. Here is a simple step by step tutorial on how to do this.

The code to replace a mergefield with a string is like this:

public static void TextToWord(string pWordDoc, string pMergeField, string pValue)
{
    Object oMissing = System.Reflection.Missing.Value;
    Object oTrue = true;
    Object oFalse = false;
    Word.Application oWord = new Word.Application();
    Word.Document oWordDoc = new Word.Document();
    oWord.Visible = true;
    Object oTemplatePath = pWordDoc;
    oWordDoc = oWord.Documents.Add(ref oTemplatePath, ref oMissing, ref oMissing, ref oMissing);
    foreach (Word.Field myMergeField in oWordDoc.Fields)
    {
        Word.Range rngFieldCode = myMergeField.Code;
        String fieldText = rngFieldCode.Text;
        if (fieldText.StartsWith(" MERGEFIELD"))
        {
            Int32 endMerge = fieldText.IndexOf("\\");
            Int32 fieldNameLength = fieldText.Length - endMerge;
            String fieldName = fieldText.Substring(11, endMerge - 11);
            fieldName = fieldName.Trim();
            if (fieldName == pMergeField)
            {
                myMergeField.Select();
                oWord.Selection.TypeText(pValue);
            }
        }
    }
}

originally posted here and here

In case you wish to use a dictionary to replace many fields at once use the code below:

public static void TextToWord(string pWordDoc, Dictionary<string, string> pDictionaryMerge)
    {
        Object oMissing = System.Reflection.Missing.Value;
        Object oTrue = true;
        Object oFalse = false;
        Microsoft.Office.Interop.Word.Application oWord = new Microsoft.Office.Interop.Word.Application();
        Microsoft.Office.Interop.Word.Document oWordDoc = new Microsoft.Office.Interop.Word.Document();
        oWord.Visible = true;
        Object oTemplatePath = pWordDoc;
        oWordDoc = oWord.Documents.Add(ref oTemplatePath, ref oMissing, ref oMissing, ref oMissing);

        foreach (Microsoft.Office.Interop.Word.Field myMergeField in oWordDoc.Fields)
        {
            Microsoft.Office.Interop.Word.Range rngFieldCode = myMergeField.Code;
            String fieldText = rngFieldCode.Text;
            if (fieldText.StartsWith(" MERGEFIELD"))
            {
                Int32 endMerge = fieldText.IndexOf("\\");
                Int32 fieldNameLength = fieldText.Length - endMerge;
                String fieldName = fieldText.Substring(11, endMerge - 11);
                fieldName = fieldName.Trim();
                foreach (var item in pDictionaryMerge)
                {
                    if (fieldName == item.Key)
                    {
                        myMergeField.Select();
                        oWord.Selection.TypeText(item.Value);
                    }
                }
            }
        }
    }
Grecian answered 7/8, 2015 at 13:55 Comment(1)
Thanks for the code, I had to modify it for my purpose as I needed to change more that just one field. I will paste code when it's complete.Extinguisher
M
18

Can't believe third party software charge thousands for interface functions with Word. I have perfectly solved this mail merge thing in my project -- no third party, no particular demands on IIS, just use OpenXML. So, add these 4 functions to your project:

public static void dotx2docx(string sourceFile, string targetFile)
    {
        MemoryStream documentStream;
        using (Stream tplStream = File.OpenRead(sourceFile))
        {
            documentStream = new MemoryStream((int)tplStream.Length);
            CopyStream(tplStream, documentStream);
            documentStream.Position = 0L;
        }

        using (WordprocessingDocument template = WordprocessingDocument.Open(documentStream, true))
        {
            template.ChangeDocumentType(DocumentFormat.OpenXml.WordprocessingDocumentType.Document);
            MainDocumentPart mainPart = template.MainDocumentPart;
            mainPart.DocumentSettingsPart.AddExternalRelationship("http://schemas.openxmlformats.org/officeDocument/2006/relationships/attachedTemplate",
               new Uri(targetFile, UriKind.Absolute));

            mainPart.Document.Save();
        }
        File.WriteAllBytes(targetFile, documentStream.ToArray());
    }
    public static void CopyStream(Stream source, Stream target)
    {
        if (source != null)
        {
            MemoryStream mstream = source as MemoryStream;
            if (mstream != null) mstream.WriteTo(target);
            else
            {
                byte[] buffer = new byte[2048];
                int length = buffer.Length, size;
                while ((size = source.Read(buffer, 0, length)) != 0)
                    target.Write(buffer, 0, size);
            }
        }
    }
    public static void Mailmerge(string templatePath, string DestinatePath, DataRow dr, DataColumnCollection columns)
    {
        try
        {
            dotx2docx(templatePath, DestinatePath);
        }
        catch //incase the server does not support MS Office Word 2003 / 2007 / 2010
        {
            File.Copy(templatePath, DestinatePath, true);
        }
        using (WordprocessingDocument doc = WordprocessingDocument.Open(DestinatePath, true))
        {
            var allParas = doc.MainDocumentPart.Document.Descendants<DocumentFormat.OpenXml.Wordprocessing.Text>();
            Text PreItem = null;
            string PreItemConstant = null;
            bool FindSingleAnglebrackets = false;
            bool breakFlag = false;
            List<Text> breakedFiled = new List<Text>();
            foreach (Text item in allParas)
            {
                foreach (DataColumn cl in columns)
                {
                    //<Today>
                    if (item.Text.Contains("«" + cl.ColumnName + "»") || item.Text.Contains("<" + cl.ColumnName + ">"))
                    {
                        item.Text = item.Text.Replace("<" + cl.ColumnName + ">", dr[cl.ColumnName].ToString())
                                             .Replace("«" + cl.ColumnName + "»", dr[cl.ColumnName].ToString());
                        FindSingleAnglebrackets = false;
                        breakFlag = false;
                        breakedFiled.Clear();
                    }
                    else if //<Today
                    (item.Text != null
                        && (
                                (item.Text.Contains("<") && !item.Text.Contains(">"))
                                || (item.Text.Contains("«") && !item.Text.Contains("»"))
                            )
                        && (item.Text.Contains(cl.ColumnName))
                    )
                    {
                        FindSingleAnglebrackets = true;
                        item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"\<" + cl.ColumnName + @"(?!\w)", dr[cl.ColumnName].ToString());
                        item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"\«" + cl.ColumnName + @"(?!\w)", dr[cl.ColumnName].ToString());
                    }
                    else if //Today> or Today
                    (
                        PreItemConstant != null
                        && (
                                (PreItemConstant.Contains("<") && !PreItemConstant.Contains(">"))
                                || (PreItemConstant.Contains("«") && !PreItemConstant.Contains("»"))
                            )
                        && (item.Text.Contains(cl.ColumnName))
                    )
                    {
                        if (item.Text.Contains(">") || item.Text.Contains("»"))
                        {
                            FindSingleAnglebrackets = false;
                            breakFlag = false;
                            breakedFiled.Clear();
                        }
                        else
                        {
                            FindSingleAnglebrackets = true;
                        }
                        if (PreItemConstant == "<" || PreItemConstant == "«")
                        {
                            PreItem.Text = "";
                        }
                        else
                        {
                            PreItem.Text = global::System.Text.RegularExpressions.Regex.Replace(PreItemConstant, @"\<" + cl.ColumnName + @"(?!\w)", dr[cl.ColumnName].ToString());
                            PreItem.Text = global::System.Text.RegularExpressions.Regex.Replace(PreItemConstant, @"\«" + cl.ColumnName + @"(?!\w)", dr[cl.ColumnName].ToString());
                        }
                        if (PreItemConstant.Contains("<") || PreItemConstant.Contains("«")) // pre item is like '[blank]«'
                        {
                            PreItem.Text = PreItem.Text.Replace("<", "");
                            PreItem.Text = PreItem.Text.Replace("«", "");
                        }
                        if (item.Text.Contains(cl.ColumnName + ">") || item.Text.Contains(cl.ColumnName + "»"))
                        {
                            item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"(?<!\w)" + cl.ColumnName + @"\>", dr[cl.ColumnName].ToString());
                            item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"(?<!\w)" + cl.ColumnName + @"\»", dr[cl.ColumnName].ToString());

                        }
                        else
                        {
                            item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"(?<!\w)" + cl.ColumnName + @"(?!\w)", dr[cl.ColumnName].ToString());
                        }
                    }
                    else if (FindSingleAnglebrackets && (item.Text.Contains("»") || item.Text.Contains(">")))
                    {
                        item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"(?<!\w)" + cl.ColumnName + @"\>", dr[cl.ColumnName].ToString());
                        item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"(?<!\w)" + cl.ColumnName + @"\»", dr[cl.ColumnName].ToString());
                        item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"^\s*\>", "");
                        item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"^\s*\»", "");
                        FindSingleAnglebrackets = false;
                        breakFlag = false;
                        breakedFiled.Clear();
                    }
                    else if (item.Text.Contains("<") || item.Text.Contains("«")) // no ColumnName
                    {

                    }
                } //end of each columns
                PreItem = item;
                PreItemConstant = item.Text;
                if (breakFlag
                    || (item.Text.Contains("<") && !item.Text.Contains(">"))
                    || (item.Text.Contains("«") && !item.Text.Contains("»"))
                   )
                {
                    breakFlag = true;
                    breakedFiled.Add(item);
                    string combinedfiled = "";
                    foreach (Text t in breakedFiled)
                    {
                        combinedfiled += t.Text;
                    }
                    foreach (DataColumn cl in columns)
                    {
                        //<Today>
                        if (combinedfiled.Contains("«" + cl.ColumnName + "»") || combinedfiled.Contains("<" + cl.ColumnName + ">"))
                        {
                            //for the first part, remove the last '<' and tailing content
                            breakedFiled[0].Text = global::System.Text.RegularExpressions.Regex.Replace(breakedFiled[0].Text, @"<\w*$", "");
                            breakedFiled[0].Text = global::System.Text.RegularExpressions.Regex.Replace(breakedFiled[0].Text, @"<\w*$", "");

                            //remove middle parts
                            foreach (Text t in breakedFiled)
                            {
                                if (!t.Text.Contains("<") && !t.Text.Contains("«") && !t.Text.Contains(">") && !t.Text.Contains("»"))
                                {
                                    t.Text = "";
                                }
                            }

                            //for the last part(as current item), remove leading content till the first '>' 
                            item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"^\s*\>", dr[cl.ColumnName].ToString());
                            item.Text = global::System.Text.RegularExpressions.Regex.Replace(item.Text, @"^\s*\»", dr[cl.ColumnName].ToString());

                            FindSingleAnglebrackets = false;
                            breakFlag = false;
                            breakedFiled.Clear();
                            break;
                        }
                    }
                }
            }//end of each item
            #region go through footer
            MainDocumentPart mainPart = doc.MainDocumentPart;
            foreach (FooterPart footerPart in mainPart.FooterParts)
            {
                Footer footer = footerPart.Footer;
                var allFooterParas = footer.Descendants<Text>();
                foreach (Text item in allFooterParas)
                {
                    foreach (DataColumn cl in columns)
                    {
                        if (item.Text.Contains("«" + cl.ColumnName + "»") || item.Text.Contains("<" + cl.ColumnName + ">"))
                        {
                            item.Text = (string.IsNullOrEmpty(dr[cl.ColumnName].ToString()) ? " " : dr[cl.ColumnName].ToString());
                            FindSingleAnglebrackets = false;
                        }
                        else if (PreItem != null && (PreItem.Text == "<" || PreItem.Text == "«") && (item.Text.Trim() == cl.ColumnName))
                        {
                            FindSingleAnglebrackets = true;
                            PreItem.Text = "";
                            item.Text = (string.IsNullOrEmpty(dr[cl.ColumnName].ToString()) ? " " : dr[cl.ColumnName].ToString());
                        }
                        else if (FindSingleAnglebrackets && (item.Text == "»" || item.Text == ">"))
                        {
                            item.Text = "";
                            FindSingleAnglebrackets = false;
                        }
                    }
                    PreItem = item;
                }
            }
            #endregion

            #region replace \v to new Break()
            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("MS_Doc_New_Line"))
                        {
                            string[] ss = text.Text.Split(new string[] { "MS_Doc_New_Line" }, StringSplitOptions.None);
                            text.Text = text.Text = "";
                            int n = 0;
                            foreach (string s in ss)
                            {
                                n++;
                                run.AppendChild(new Text(s));
                                if (n != ss.Length)
                                {
                                    run.AppendChild(new Break());
                                }
                            }
                        }
                    }
                }
            }
            #endregion

            doc.MainDocumentPart.Document.Save();
        }
    }
    public static void MergeDocuments(params string[] filepaths)
    {

        //filepaths = new[] { "D:\\one.docx", "D:\\two.docx", "D:\\three.docx", "D:\\four.docx", "D:\\five.docx" };
        if (filepaths != null && filepaths.Length > 1)

            using (WordprocessingDocument myDoc = WordprocessingDocument.Open(@filepaths[0], true))
            {
                MainDocumentPart mainPart = myDoc.MainDocumentPart;

                for (int i = 1; i < filepaths.Length; i++)
                {
                    string altChunkId = "AltChunkId" + i;
                    AlternativeFormatImportPart chunk = mainPart.AddAlternativeFormatImportPart(
                        AlternativeFormatImportPartType.WordprocessingML, altChunkId);
                    using (FileStream fileStream = File.Open(@filepaths[i], FileMode.Open))
                    {
                        chunk.FeedData(fileStream);
                    }
                    DocumentFormat.OpenXml.Wordprocessing.AltChunk altChunk = new DocumentFormat.OpenXml.Wordprocessing.AltChunk();
                    altChunk.Id = altChunkId;
                    //new page, if you like it...
                    mainPart.Document.Body.AppendChild(new Paragraph(new Run(new Break() { Type = BreakValues.Page })));
                    //next document
                    mainPart.Document.Body.InsertAfter(altChunk, mainPart.Document.Body.Elements<Paragraph>().Last());
                }
                mainPart.Document.Save();
                myDoc.Close();
            }
    }

And use them like this:

DataTable dt = new DataTable();
dt.Columns.Add("Date");
dt.Columns.Add("Today");
dt.Columns.Add("Addr1");
dt.Columns.Add("Addr2");
dt.Columns.Add("PreferContact");
dt.Columns.Add("TenantName");
//......

DataRow nr = dt.NewRow();
nr["Date"] = DateTime.Now.ToString("dd/MM/yyyy");
                nr["Today"] = DateTime.Now.ToString("dd/MM/yyyy");
//......
dt.Rows.Add(nr);


string sourceFile = "c:\my_template.docx"; //this is where you store your template
string filePath = "c:\final.docx"; //this is where your result file locate
Mailmerge(sourceFile, filePath, nr, dt.Columns);

Your template(c:\my_template.docx) will be just like normal .docx file, and you need to specify your fields in it:

<field>

So, your template(c:\my_template.docx) should be like:

<Today> 

<DebtorName>
<DebtorADDR>
<DebtorEmail>


Dear <Dear>,

Congratulations on yourr property <PlanNo>  <BuildAddress>. Your unit number is <LotNo> ...............

In addition, if some of your fields contain line breaks, use this:

nr["Address"] = my_address_text_contains_line_breaks.Replace(Environment.NewLine, "MS_Doc_New_Line");
Mockup answered 22/1, 2019 at 9:1 Comment(4)
I use DataTable instead of Dictionary because in my project DataTable is easy to deal with data directly from DB.Mockup
how to print the document without savingGarland
Does this handle the mail merge custom formatting?Rhona
@FrankelyDiaz Yes, save your formatting in the template c:\my_template.docxMockup
G
11

This is quite simple by using Microsoft.Office.Interop.Word. Here is a simple step by step tutorial on how to do this.

The code to replace a mergefield with a string is like this:

public static void TextToWord(string pWordDoc, string pMergeField, string pValue)
{
    Object oMissing = System.Reflection.Missing.Value;
    Object oTrue = true;
    Object oFalse = false;
    Word.Application oWord = new Word.Application();
    Word.Document oWordDoc = new Word.Document();
    oWord.Visible = true;
    Object oTemplatePath = pWordDoc;
    oWordDoc = oWord.Documents.Add(ref oTemplatePath, ref oMissing, ref oMissing, ref oMissing);
    foreach (Word.Field myMergeField in oWordDoc.Fields)
    {
        Word.Range rngFieldCode = myMergeField.Code;
        String fieldText = rngFieldCode.Text;
        if (fieldText.StartsWith(" MERGEFIELD"))
        {
            Int32 endMerge = fieldText.IndexOf("\\");
            Int32 fieldNameLength = fieldText.Length - endMerge;
            String fieldName = fieldText.Substring(11, endMerge - 11);
            fieldName = fieldName.Trim();
            if (fieldName == pMergeField)
            {
                myMergeField.Select();
                oWord.Selection.TypeText(pValue);
            }
        }
    }
}

originally posted here and here

In case you wish to use a dictionary to replace many fields at once use the code below:

public static void TextToWord(string pWordDoc, Dictionary<string, string> pDictionaryMerge)
    {
        Object oMissing = System.Reflection.Missing.Value;
        Object oTrue = true;
        Object oFalse = false;
        Microsoft.Office.Interop.Word.Application oWord = new Microsoft.Office.Interop.Word.Application();
        Microsoft.Office.Interop.Word.Document oWordDoc = new Microsoft.Office.Interop.Word.Document();
        oWord.Visible = true;
        Object oTemplatePath = pWordDoc;
        oWordDoc = oWord.Documents.Add(ref oTemplatePath, ref oMissing, ref oMissing, ref oMissing);

        foreach (Microsoft.Office.Interop.Word.Field myMergeField in oWordDoc.Fields)
        {
            Microsoft.Office.Interop.Word.Range rngFieldCode = myMergeField.Code;
            String fieldText = rngFieldCode.Text;
            if (fieldText.StartsWith(" MERGEFIELD"))
            {
                Int32 endMerge = fieldText.IndexOf("\\");
                Int32 fieldNameLength = fieldText.Length - endMerge;
                String fieldName = fieldText.Substring(11, endMerge - 11);
                fieldName = fieldName.Trim();
                foreach (var item in pDictionaryMerge)
                {
                    if (fieldName == item.Key)
                    {
                        myMergeField.Select();
                        oWord.Selection.TypeText(item.Value);
                    }
                }
            }
        }
    }
Grecian answered 7/8, 2015 at 13:55 Comment(1)
Thanks for the code, I had to modify it for my purpose as I needed to change more that just one field. I will paste code when it's complete.Extinguisher
R
0

I am also using the same thing but I have more complexity. I have to check the if condition also. In word template file { IF «Installment» = Monthly "then the table will appear" "nothing to show" }

when I use the above code shared in the answer. and for if condition in c# I have written

Range rngFieldCode = myMergeField.Code;
            String fieldText = rngFieldCode.Text.Trim();
            if (fieldText.ToUpper().StartsWith("IF"))
            {
                myMergeField.UpdateSource();}

so the output is like

{ IF Monthly = Monthly "then table will appear" "nothing to show" }

but the desired output is only "then the table will appear".

Rachelrachele answered 29/10, 2017 at 9:49 Comment(1)
Is this a new question related to the starting question? If so you should create a new question and possibly reffer to the question, but not post as an answer a new question.Gouge

© 2022 - 2024 — McMap. All rights reserved.