Pastebin launched a little side project called VERYVIRAL.com, check it out ;-) Want more features on Pastebin? Sign Up, it's FREE!
Guest

Fill Mergefields in .docx Documents without Microsoft Word

By: Warlock_29A on Jul 31st, 2013  |  syntax: C#  |  size: 36.96 KB  |  views: 139  |  expires: Never
download  |  raw  |  embed  |  report abuse  |  print
Text below is selected. Please press Ctrl+C to copy to your clipboard. (⌘+C on Mac)
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Globalization;
  4. using System.Linq;
  5. using System.Text;
  6. using System.Text.RegularExpressions;
  7. using System.IO;
  8. using System.Data;
  9. using System.Xml.Linq;
  10. using DocumentFormat.OpenXml.Packaging;
  11. using DocumentFormat.OpenXml.Wordprocessing;
  12. using DocumentFormat.OpenXml;
  13.  
  14. namespace Kacit.EBoard.CommonControls.Reporting
  15. {
  16.     /// <summary>
  17.     /// Helper class for filling in data forms based on Word 2007 documents.
  18.     /// </summary>
  19.     public static class FormFiller
  20.     {
  21.         /// <summary>
  22.         /// Regex used to parse MERGEFIELDs in the provided document.
  23.         /// </summary>
  24.         private static readonly Regex instructionRegEx =
  25.             new Regex(
  26.                         @"^[\s]*MERGEFIELD[\s]+(?<name>[#\w]*){1}               # This retrieves the field's name (Named Capture Group -> name)
  27.                            [\s]*(\\\*[\s]+(?<Format>[\w]*){1})?                # Retrieves field's format flag (Named Capture Group -> Format)
  28.                            [\s]*(\\b[\s]+[""]?(?<PreText>[^\\]*){1})?         # Retrieves text to display before field data (Named Capture Group -> PreText)
  29.                                                                                # Retrieves text to display after field data (Named Capture Group -> PostText)
  30.                            [\s]*(\\f[\s]+[""]?(?<PostText>[^\\]*){1})?",
  31.                         RegexOptions.Compiled | RegexOptions.CultureInvariant | RegexOptions.ExplicitCapture | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace | RegexOptions.Singleline);
  32.  
  33.         /// <summary>
  34.         /// Fills in a .docx file with the provided data.
  35.         /// </summary>
  36.         /// <param name="filename">Path to the template that must be used.</param>
  37.         /// <param name="dataset">Dataset with the datatables to use to fill the document tables with.  Table names in the dataset should match the table names in the document.</param>
  38.         /// <param name="values">Values to fill the document.  Keys should match the MERGEFIELD names.</param>
  39.         /// <returns>The filled-in document.</returns>
  40.         public static byte[] GetWordReport(string filename, DataSet dataset, Dictionary<string, string> values)
  41.         {
  42.             // first read document in as stream
  43.             byte[] original = File.ReadAllBytes(filename);
  44.             string[] switches = null;
  45.  
  46.             using (var stream = new MemoryStream())
  47.             {
  48.                 stream.Write(original, 0, original.Length);
  49.  
  50.                 // Create a Wordprocessing document object.
  51.                 using (var docx = WordprocessingDocument.Open(stream, true))
  52.                 {
  53.                     //  2010/08/01: addition
  54.                     ConvertFieldCodes(docx.MainDocumentPart.Document);
  55.  
  56.                     // first: process all tables
  57.                     foreach (var field in docx.MainDocumentPart.Document.Descendants<SimpleField>())
  58.                     {
  59.                         var fieldname = GetFieldName(field, out switches);
  60.                         if (!string.IsNullOrEmpty(fieldname) &&
  61.                             fieldname.StartsWith("TBL_"))
  62.                         {
  63.                             var wrow = GetFirstParent<TableRow>(field);
  64.                             if (wrow == null)
  65.                             {
  66.                                 continue;   // can happen: is because table contains multiple fields, and after 1 pass, the initial row is already deleted
  67.                             }
  68.  
  69.                             var wtable = GetFirstParent<Table>(wrow);
  70.                             if (wtable == null)
  71.                             {
  72.                                 continue;   // can happen: is because table contains multiple fields, and after 1 pass, the initial row is already deleted
  73.                             }
  74.  
  75.                             var tablename = GetTableNameFromFieldName(fieldname);
  76.                             if (dataset == null ||
  77.                                 !dataset.Tables.Contains(tablename) ||
  78.                                 dataset.Tables[tablename].Rows.Count == 0)
  79.                             {
  80.                                 continue;   // don't remove table here: will be done in next pass
  81.                             }
  82.  
  83.                             var table = dataset.Tables[tablename];
  84.  
  85.                             var props = new List<TableCellProperties>();
  86.                             var cellcolumnnames =  new List<Dictionary<string, List<string>>>();
  87.                             var paragraphInfo = new List<string>();
  88.                             var cellfields = new List<List<SimpleField>>();
  89.  
  90.                             foreach (var cell in wrow.Descendants<TableCell>())
  91.                             {
  92.                                 props.Add(cell.GetFirstChild<TableCellProperties>());
  93.                                 var p = cell.GetFirstChild<Paragraph>();
  94.  
  95.                                 if (p != null)
  96.                                 {
  97.                                     var pp = p.GetFirstChild<ParagraphProperties>();
  98.                                     paragraphInfo.Add(pp != null ? pp.OuterXml : null);
  99.                                 }
  100.                                 else
  101.                                 {
  102.                                     paragraphInfo.Add(null);
  103.                                 }
  104.  
  105.                                 var colname = string.Empty;
  106.                                 SimpleField colfield = null;
  107.  
  108.                                 var subCellFields = new List<SimpleField>();
  109.                                 var subColumnNames = new List<string>();
  110.  
  111.                                 foreach (var cellfield in cell.Descendants<SimpleField>())
  112.                                 {
  113.                                     colfield = cellfield;
  114.                                     colname = GetColumnNameFromFieldName(GetFieldName(cellfield, out switches));
  115.  
  116.                                     subColumnNames.Add(colname);
  117.                                     subCellFields.Add(colfield);
  118.                                 }
  119.  
  120.                                 cellfields.Add(subCellFields.Count == 0 ? new List<SimpleField>() : subCellFields);
  121.  
  122.                                 if (subColumnNames.Count == 0)
  123.                                 {
  124.                                     cellcolumnnames.Add(null);
  125.                                 }
  126.                                 else
  127.                                 {
  128.                                     cellcolumnnames.Add(new Dictionary<string, List<string>>
  129.                                         {
  130.                                             {subColumnNames.First(), subColumnNames}
  131.                                         });    
  132.                                 }
  133.                             }
  134.  
  135.                             // keep reference to row properties
  136.                             var rprops = wrow.GetFirstChild<TableRowProperties>();
  137.  
  138.                             foreach (DataRow row in table.Rows)
  139.                             {
  140.                                 var nrow = new TableRow();
  141.  
  142.                                 if (rprops != null)
  143.                                 {
  144.                                     nrow.Append(new TableRowProperties(rprops.OuterXml));
  145.                                 }
  146.  
  147.                                 for (var i = 0; i < props.Count; i++)
  148.                                 {
  149.                                     var cellproperties = new TableCellProperties(props[i].OuterXml);
  150.  
  151.                                     var cell = new TableCell();
  152.                                     cell.Append(cellproperties);
  153.                                    
  154.  
  155.                                     if (cellcolumnnames[i] != null)
  156.                                     {
  157.                                         var cellColumnNameAsDict = cellcolumnnames[i];
  158.                                         var cellColumnName = cellColumnNameAsDict.First().Key;
  159.  
  160.                                         if (!table.Columns.Contains(cellColumnName))
  161.                                         {
  162.                                             throw new Exception(
  163.                                                 string.Format(
  164.                                                     "Unable to complete template: column name '{0}' is unknown in parameter tables !",
  165.                                                     cellcolumnnames[i]));
  166.                                         }
  167.  
  168.                                         foreach (var cn in cellColumnNameAsDict[cellColumnName])
  169.                                         {
  170.                                             var val = row[cn].ToString();
  171.  
  172.                                             foreach (var cellfield in cellfields[i])
  173.                                             {
  174.                                                 if (!cellfield.Instruction.Value.Contains(string.Format("TBL_{0}_{1}",
  175.                                                                                                         table.TableName,
  176.                                                                                                         cn)))
  177.                                                 {
  178.                                                     continue;
  179.                                                 }
  180.  
  181.                                                 var p = new Paragraph(new ParagraphProperties(paragraphInfo[i]));
  182.                                                 p.Append(GetRunElementForText(val, cellfield));
  183.                                                 cell.Append(p);
  184.                                             }
  185.                                         }
  186.                                     }
  187.                                     else
  188.                                     {
  189.                                         var p = new Paragraph(new ParagraphProperties(paragraphInfo[i]));
  190.                                         cell.Append(p);   // cell must contain at minimum a paragraph !
  191.                                     }
  192.  
  193.                                     nrow.Append(cell);
  194.                                 }
  195.  
  196.                                 wtable.Append(nrow);
  197.                             }
  198.  
  199.                             // finally : delete template-row (and thus also the mergefields in the table)
  200.                             wrow.Remove();
  201.                         }
  202.                     }
  203.  
  204.                     // clean empty tables
  205.                     foreach (var field in docx.MainDocumentPart.Document.Descendants<SimpleField>())
  206.                     {
  207.                         var fieldname = GetFieldName(field, out switches);
  208.  
  209.                         if (string.IsNullOrEmpty(fieldname) || !fieldname.StartsWith("TBL_")) continue;
  210.  
  211.                         var wrow = GetFirstParent<TableRow>(field);
  212.                         if (wrow == null)
  213.                         {
  214.                             continue;   // can happen: is because table contains multiple fields, and after 1 pass, the initial row is already deleted
  215.                         }
  216.  
  217.                         var wtable = GetFirstParent<Table>(wrow);
  218.                         if (wtable == null)
  219.                         {
  220.                             continue;   // can happen: is because table contains multiple fields, and after 1 pass, the initial row is already deleted
  221.                         }
  222.  
  223.                         var tablename = GetTableNameFromFieldName(fieldname);
  224.                         if (dataset == null ||
  225.                             !dataset.Tables.Contains(tablename) ||
  226.                             dataset.Tables[tablename].Rows.Count == 0)
  227.                         {
  228.                             // if there's a 'dt' switch: delete Word-table
  229.                             if (switches.Contains("dt"))
  230.                             {
  231.                                 wtable.Remove();
  232.                             }
  233.                         }
  234.                     }
  235.  
  236.                     // next : process all remaining fields in the main document
  237.                     FillWordFieldsInElement(values, docx.MainDocumentPart.Document);
  238.  
  239.                     docx.MainDocumentPart.Document.Save();  // save main document back in package
  240.  
  241.                     // process header(s)
  242.                     foreach (HeaderPart hpart in docx.MainDocumentPart.HeaderParts)
  243.                     {
  244.                         //  2010/08/01: addition
  245.                         ConvertFieldCodes(hpart.Header);
  246.  
  247.                         FillWordFieldsInElement(values, hpart.Header);
  248.                         hpart.Header.Save();    // save header back in package
  249.                     }
  250.  
  251.                     // process footer(s)
  252.                     foreach (FooterPart fpart in docx.MainDocumentPart.FooterParts)
  253.                     {
  254.                         //  2010/08/01: addition
  255.                         ConvertFieldCodes(fpart.Footer);
  256.  
  257.                         FillWordFieldsInElement(values, fpart.Footer);
  258.                         fpart.Footer.Save();    // save footer back in package
  259.                     }
  260.                 }
  261.  
  262.                 // get package bytes
  263.                 stream.Seek(0, SeekOrigin.Begin);
  264.                 byte[] data = stream.ToArray();
  265.  
  266.                 return data;
  267.             }
  268.         }
  269.  
  270.         /// <summary>
  271.         /// Applies any formatting specified to the pre and post text as
  272.         /// well as to fieldValue.
  273.         /// </summary>
  274.         /// <param name="format">The format flag to apply.</param>
  275.         /// <param name="fieldValue">The data value being inserted.</param>
  276.         /// <param name="preText">The text to appear before fieldValue, if any.</param>
  277.         /// <param name="postText">The text to appear after fieldValue, if any.</param>
  278.         /// <returns>The formatted text; [0] = fieldValue, [1] = preText, [2] = postText.</returns>
  279.         /// <exception cref="">Throw if fieldValue, preText, or postText are null.</exception>
  280.         internal static string[] ApplyFormatting(string format, string fieldValue, string preText, string postText)
  281.         {
  282.             string[] valuesToReturn = new string[3];
  283.  
  284.             if ("UPPER".Equals(format))
  285.             {
  286.                 // Convert everything to uppercase.
  287.                 valuesToReturn[0] = fieldValue.ToUpper(CultureInfo.CurrentCulture);
  288.                 valuesToReturn[1] = preText.ToUpper(CultureInfo.CurrentCulture);
  289.                 valuesToReturn[2] = postText.ToUpper(CultureInfo.CurrentCulture);
  290.             }
  291.             else if ("LOWER".Equals(format))
  292.             {
  293.                 // Convert everything to lowercase.
  294.                 valuesToReturn[0] = fieldValue.ToLower(CultureInfo.CurrentCulture);
  295.                 valuesToReturn[1] = preText.ToLower(CultureInfo.CurrentCulture);
  296.                 valuesToReturn[2] = postText.ToLower(CultureInfo.CurrentCulture);
  297.             }
  298.             else if ("FirstCap".Equals(format))
  299.             {
  300.                 // Capitalize the first letter, everything else is lowercase.
  301.                 if (!string.IsNullOrEmpty(fieldValue))
  302.                 {
  303.                     valuesToReturn[0] = fieldValue.Substring(0, 1).ToUpper(CultureInfo.CurrentCulture);
  304.                     if (fieldValue.Length > 1)
  305.                     {
  306.                         valuesToReturn[0] = valuesToReturn[0] + fieldValue.Substring(1).ToLower(CultureInfo.CurrentCulture);
  307.                     }
  308.                 }
  309.  
  310.                 if (!string.IsNullOrEmpty(preText))
  311.                 {
  312.                     valuesToReturn[1] = preText.Substring(0, 1).ToUpper(CultureInfo.CurrentCulture);
  313.                     if (fieldValue.Length > 1)
  314.                     {
  315.                         valuesToReturn[1] = valuesToReturn[1] + preText.Substring(1).ToLower(CultureInfo.CurrentCulture);
  316.                     }
  317.                 }
  318.  
  319.                 if (!string.IsNullOrEmpty(postText))
  320.                 {
  321.                     valuesToReturn[2] = postText.Substring(0, 1).ToUpper(CultureInfo.CurrentCulture);
  322.                     if (fieldValue.Length > 1)
  323.                     {
  324.                         valuesToReturn[2] = valuesToReturn[2] + postText.Substring(1).ToLower(CultureInfo.CurrentCulture);
  325.                     }
  326.                 }
  327.             }
  328.             else if ("Caps".Equals(format))
  329.             {
  330.                 // Title casing: the first letter of every word should be capitalized.
  331.                 valuesToReturn[0] = ToTitleCase(fieldValue);
  332.                 valuesToReturn[1] = ToTitleCase(preText);
  333.                 valuesToReturn[2] = ToTitleCase(postText);
  334.             }
  335.             else
  336.             {
  337.                 valuesToReturn[0] = fieldValue;
  338.                 valuesToReturn[1] = preText;
  339.                 valuesToReturn[2] = postText;
  340.             }
  341.  
  342.             return valuesToReturn;
  343.         }
  344.  
  345.         /// <summary>
  346.         /// Executes the field switches on a given element.
  347.         /// The possible switches are:
  348.         /// <list>
  349.         /// <li>dt : delete table</li>
  350.         /// <li>dr : delete row</li>
  351.         /// <li>dp : delete paragraph</li>
  352.         /// </list>
  353.         /// </summary>
  354.         /// <param name="element">The element being operated on.</param>
  355.         /// <param name="switches">The switched to be executed.</param>
  356.         internal static void ExecuteSwitches(OpenXmlElement element, string[] switches)
  357.         {
  358.             if (switches == null || switches.Count() == 0)
  359.             {
  360.                 return;
  361.             }
  362.  
  363.             // check switches (switches are always lowercase)
  364.             if (switches.Contains("dp"))
  365.             {
  366.                 Paragraph p = GetFirstParent<Paragraph>(element);
  367.                 if (p != null)
  368.                 {
  369.                     p.Remove();
  370.                 }
  371.             }
  372.             else if (switches.Contains("dr"))
  373.             {
  374.                 TableRow row = GetFirstParent<TableRow>(element);
  375.                 if (row != null)
  376.                 {
  377.                     row.Remove();
  378.                 }
  379.             }
  380.             else if (switches.Contains("dt"))
  381.             {
  382.                 Table table = GetFirstParent<Table>(element);
  383.                 if (table != null)
  384.                 {
  385.                     table.Remove();
  386.                 }
  387.             }
  388.         }
  389.  
  390.         /// <summary>
  391.         /// Fills all the <see cref="SimpleFields"/> that are found in a given <see cref="OpenXmlElement"/>.
  392.         /// </summary>
  393.         /// <param name="values">The values to insert; keys should match the placeholder names, values are the data to insert.</param>
  394.         /// <param name="element">The document element taht will contain the new values.</param>
  395.         internal static void FillWordFieldsInElement(Dictionary<string, string> values, OpenXmlElement element)
  396.         {
  397.             string[] switches;
  398.             string[] options;
  399.             string[] formattedText;
  400.  
  401.             Dictionary<SimpleField, string[]> emptyfields = new Dictionary<SimpleField, string[]>();
  402.  
  403.             // First pass: fill in data, but do not delete empty fields.  Deletions silently break the loop.
  404.             var list = element.Descendants<SimpleField>().ToArray();
  405.             foreach (var field in list)
  406.             {
  407.                 string fieldname = GetFieldNameWithOptions(field, out switches, out options);
  408.                 if (!string.IsNullOrEmpty(fieldname))
  409.                 {
  410.                     if (values.ContainsKey(fieldname)
  411.                         && !string.IsNullOrEmpty(values[fieldname]))
  412.                     {
  413.                         formattedText = ApplyFormatting(options[0], values[fieldname], options[1], options[2]);
  414.  
  415.                         // Prepend any text specified to appear before the data in the MergeField
  416.                         if (!string.IsNullOrEmpty(options[1]))
  417.                         {
  418.                             field.Parent.InsertBeforeSelf<Paragraph>(GetPreOrPostParagraphToInsert(formattedText[1], field));
  419.                         }
  420.  
  421.                         // Append any text specified to appear after the data in the MergeField
  422.                         if (!string.IsNullOrEmpty(options[2]))
  423.                         {
  424.                             field.Parent.InsertAfterSelf<Paragraph>(GetPreOrPostParagraphToInsert(formattedText[2], field));
  425.                         }
  426.  
  427.                         // replace mergefield with text
  428.                         field.Parent.ReplaceChild<SimpleField>(GetRunElementForText(formattedText[0], field), field);
  429.                     }
  430.                     else
  431.                     {
  432.                         // keep track of unknown or empty fields
  433.                         emptyfields[field] = switches;
  434.                     }
  435.                 }
  436.             }
  437.  
  438.             // second pass : clear empty fields
  439.             foreach (KeyValuePair<SimpleField, string[]> kvp in emptyfields)
  440.             {
  441.                 // if field is unknown or empty: execute switches and remove it from document !
  442.                 ExecuteSwitches(kvp.Key, kvp.Value);
  443.                 kvp.Key.Remove();
  444.             }
  445.         }
  446.  
  447.         /// <summary>
  448.         /// Returns the columnname from a given fieldname from a Mergefield
  449.         /// The instruction of a table-Mergefield is formatted as TBL_tablename_columnname
  450.         /// </summary>
  451.         /// <param name="fieldname">The field name.</param>
  452.         /// <returns>The column name.</returns>
  453.         /// <exception cref="ArgumentException">Thrown when fieldname is not formatted as TBL_tablename_columname.</exception>
  454.         internal static string GetColumnNameFromFieldName(string fieldname)
  455.         {
  456.             // Column name is after the second underscore.
  457.             int pos1 = fieldname.IndexOf('_');
  458.             if (pos1 <= 0)
  459.             {
  460.                 throw new ArgumentException("Error: table-MERGEFIELD should be formatted as follows: TBL_tablename_columnname.");
  461.             }
  462.  
  463.             int pos2 = fieldname.IndexOf('_', pos1 + 1);
  464.             if (pos2 <= 0)
  465.             {
  466.                 throw new ArgumentException("Error: table-MERGEFIELD should be formatted as follows: TBL_tablename_columnname.");
  467.             }
  468.  
  469.             return fieldname.Substring(pos2 + 1);
  470.         }
  471.  
  472.         /// <summary>
  473.         /// Returns the fieldname and switches from the given mergefield-instruction
  474.         /// Note: the switches are always returned lowercase !
  475.         /// </summary>
  476.         /// <param name="field">The field being examined.</param>
  477.         /// <param name="switches">An array of switches to apply to the field.</param>
  478.         /// <returns>The name of the field.</returns>
  479.         internal static string GetFieldName(SimpleField field, out string[] switches)
  480.         {
  481.             var a = field.GetAttribute("instr", "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
  482.             switches = new string[0];
  483.             string fieldname = string.Empty;
  484.             string instruction = a.Value;
  485.  
  486.             if (!string.IsNullOrEmpty(instruction))
  487.             {
  488.                 Match m = instructionRegEx.Match(instruction);
  489.                 if (m.Success)
  490.                 {
  491.                     fieldname = m.Groups["name"].ToString().Trim();
  492.                     int pos = fieldname.IndexOf('#');
  493.                     if (pos > 0)
  494.                     {
  495.                         // Process the switches, correct the fieldname.
  496.                         switches = fieldname.Substring(pos + 1).ToLower().Split(new char[] { '#' }, StringSplitOptions.RemoveEmptyEntries);
  497.                         fieldname = fieldname.Substring(0, pos);
  498.                     }
  499.                 }
  500.             }
  501.  
  502.             return fieldname;
  503.         }
  504.  
  505.         /// <summary>
  506.         /// Returns the fieldname and switches from the given mergefield-instruction
  507.         /// Note: the switches are always returned lowercase !
  508.         /// Note 2: options holds values for formatting and text to insert before and/or after the field value.
  509.         ///         options[0] = Formatting (Upper, Lower, Caps a.k.a. title case, FirstCap)
  510.         ///         options[1] = Text to insert before data
  511.         ///         options[2] = Text to insert after data
  512.         /// </summary>
  513.         /// <param name="field">The field being examined.</param>
  514.         /// <param name="switches">An array of switches to apply to the field.</param>
  515.         /// <param name="options">Formatting options to apply.</param>
  516.         /// <returns>The name of the field.</returns>
  517.         internal static string GetFieldNameWithOptions(SimpleField field, out string[] switches, out string[] options)
  518.         {
  519.             var a = field.GetAttribute("instr", "http://schemas.openxmlformats.org/wordprocessingml/2006/main");
  520.             switches = new string[0];
  521.             options = new string[3];
  522.             string fieldname = string.Empty;
  523.             string instruction = a.Value;
  524.  
  525.             if (!string.IsNullOrEmpty(instruction))
  526.             {
  527.                 Match m = instructionRegEx.Match(instruction);
  528.                 if (m.Success)
  529.                 {
  530.                     fieldname = m.Groups["name"].ToString().Trim();
  531.                     options[0] = m.Groups["Format"].Value.Trim();
  532.                     options[1] = m.Groups["PreText"].Value.Trim();
  533.                     options[2] = m.Groups["PostText"].Value.Trim();
  534.                     int pos = fieldname.IndexOf('#');
  535.                     if (pos > 0)
  536.                     {
  537.                         // Process the switches, correct the fieldname.
  538.                         switches = fieldname.Substring(pos + 1).ToLower().Split(new char[] { '#' }, StringSplitOptions.RemoveEmptyEntries);
  539.                         fieldname = fieldname.Substring(0, pos);
  540.                     }
  541.                 }
  542.             }
  543.  
  544.             return fieldname;
  545.         }
  546.  
  547.         /// <summary>
  548.         /// Returns the first parent of a given <see cref="OpenXmlElement"/> that corresponds
  549.         /// to the given type.
  550.         /// This methods is different from the Ancestors-method on the OpenXmlElement in the sense that
  551.         /// this method will return only the first-parent in direct line (closest to the given element).
  552.         /// </summary>
  553.         /// <typeparam name="T">The type of element being searched for.</typeparam>
  554.         /// <param name="element">The element being examined.</param>
  555.         /// <returns>The first parent of the element of the specified type.</returns>
  556.         internal static T GetFirstParent<T>(OpenXmlElement element)
  557.             where T : OpenXmlElement
  558.         {
  559.             if (element.Parent == null)
  560.             {
  561.                 return null;
  562.             }
  563.             else if (element.Parent.GetType() == typeof(T))
  564.             {
  565.                 return element.Parent as T;
  566.             }
  567.             else
  568.             {
  569.                 return GetFirstParent<T>(element.Parent);
  570.             }
  571.         }
  572.  
  573.         /// <summary>
  574.         /// Creates a paragraph to house text that should appear before or after the MergeField.
  575.         /// </summary>
  576.         /// <param name="text">The text to display.</param>
  577.         /// <param name="fieldToMimic">The MergeField that will have its properties mimiced.</param>
  578.         /// <returns>An OpenXml Paragraph ready to insert.</returns>
  579.         internal static Paragraph GetPreOrPostParagraphToInsert(string text, SimpleField fieldToMimic)
  580.         {
  581.             Run runToInsert = GetRunElementForText(text, fieldToMimic);
  582.             Paragraph paragraphToInsert = new Paragraph();
  583.             paragraphToInsert.Append(runToInsert);
  584.  
  585.             return paragraphToInsert;
  586.         }
  587.  
  588.         /// <summary>
  589.         /// Returns a <see cref="Run"/>-openxml element for the given text.
  590.         /// Specific about this run-element is that it can describe multiple-line and tabbed-text.
  591.         /// The <see cref="SimpleField"/> placeholder can be provided too, to allow duplicating the formatting.
  592.         /// </summary>
  593.         /// <param name="text">The text to be inserted.</param>
  594.         /// <param name="placeHolder">The placeholder where the text will be inserted.</param>
  595.         /// <returns>A new <see cref="Run"/>-openxml element containing the specified text.</returns>
  596.         internal static Run GetRunElementForText(string text, SimpleField placeHolder)
  597.         {
  598.             string rpr = null;
  599.             if (placeHolder != null)
  600.             {
  601.                 var xdoc = XDocument.Parse((placeHolder.Parent).OuterXml.Replace(placeHolder.OuterXml, string.Empty));        
  602.                 if (xdoc.Root != null)
  603.                 {
  604.                     var xrpr = xdoc.Root.Elements().FirstOrDefault(x => x.Name.LocalName == "rPr");
  605.  
  606.                     if (xrpr != null)
  607.                         rpr = xrpr.ToString();
  608.                 }
  609.             }
  610.  
  611.             var r = new Run();
  612.  
  613.             if (!string.IsNullOrEmpty(rpr))
  614.             {
  615.                 r.AppendChild(new RunProperties(rpr));
  616.             }
  617.  
  618.             if (!string.IsNullOrEmpty(text))
  619.             {
  620.                 // first process line breaks
  621.                 string[] split = text.Split(new string[] { "\n" }, StringSplitOptions.None);
  622.                 bool first = true;
  623.                 foreach (string s in split)
  624.                 {
  625.                     if (!first)
  626.                     {
  627.                         r.Append(new Break());
  628.                     }
  629.  
  630.                     first = false;
  631.  
  632.                     // then process tabs
  633.                     bool firsttab = true;
  634.                     string[] tabsplit = s.Split(new[] { "\t" }, StringSplitOptions.None);
  635.                     foreach (string tabtext in tabsplit)
  636.                     {
  637.                         if (!firsttab)
  638.                         {
  639.                             r.Append(new TabChar());
  640.                         }
  641.  
  642.                         r.AppendChild<Text>(new Text(tabtext));
  643.                         firsttab = false;
  644.                     }
  645.                 }
  646.             }
  647.  
  648.             return r;
  649.         }
  650.  
  651.         /// <summary>
  652.         /// Returns the table name from a given fieldname from a Mergefield.
  653.         /// The instruction of a table-Mergefield is formatted as TBL_tablename_columnname
  654.         /// </summary>
  655.         /// <param name="fieldname">The field name.</param>
  656.         /// <returns>The table name.</returns>
  657.         /// <exception cref="ArgumentException">Thrown when fieldname is not formatted as TBL_tablename_columname.</exception>
  658.         internal static string GetTableNameFromFieldName(string fieldname)
  659.         {
  660.             int pos1 = fieldname.IndexOf('_');
  661.             if (pos1 <= 0)
  662.             {
  663.                 throw new ArgumentException("Error: table-MERGEFIELD should be formatted as follows: TBL_tablename_columnname.");
  664.             }
  665.  
  666.             int pos2 = fieldname.IndexOf('_', pos1 + 1);
  667.             if (pos2 <= 0)
  668.             {
  669.                 throw new ArgumentException("Error: table-MERGEFIELD should be formatted as follows: TBL_tablename_columnname.");
  670.             }
  671.  
  672.             return fieldname.Substring(pos1 + 1, pos2 - pos1 - 1);
  673.         }
  674.  
  675.         /// <summary>
  676.         /// Title-cases a string, capitalizing the first letter of every word.
  677.         /// </summary>
  678.         /// <param name="toConvert">The string to convert.</param>
  679.         /// <returns>The string after title-casing.</returns>
  680.         internal static string ToTitleCase(string toConvert)
  681.         {
  682.             return ToTitleCaseHelper(toConvert, string.Empty);
  683.         }
  684.  
  685.         /// <summary>
  686.         /// Title-cases a string, capitalizing the first letter of every word.
  687.         /// </summary>
  688.         /// <param name="toConvert">The string to convert.</param>
  689.         /// <param name="alreadyConverted">The part of the string already converted.  Seed with an empty string.</param>
  690.         /// <returns>The string after title-casing.</returns>
  691.         internal static string ToTitleCaseHelper(string toConvert, string alreadyConverted)
  692.         {
  693.             /*
  694.              * Tail-recursive title-casing implementation.
  695.              * Edge case: toConvert is empty, null, or just white space.  If so, return alreadyConverted.
  696.              * Else: Capitalize the first letter of the first word in toConvert, append that to alreadyConverted and recur.
  697.              */
  698.             if (string.IsNullOrEmpty(toConvert))
  699.             {
  700.                 return alreadyConverted;
  701.             }
  702.             else
  703.             {
  704.                 int indexOfFirstSpace = toConvert.IndexOf(' ');
  705.                 string firstWord, restOfString;
  706.  
  707.                 // Check to see if we're on the last word or if there are more.
  708.                 if (indexOfFirstSpace != -1)
  709.                 {
  710.                     firstWord = toConvert.Substring(0, indexOfFirstSpace);
  711.                     restOfString = toConvert.Substring(indexOfFirstSpace).Trim();
  712.                 }
  713.                 else
  714.                 {
  715.                     firstWord = toConvert.Substring(0);
  716.                     restOfString = string.Empty;
  717.                 }
  718.  
  719.                 System.Text.StringBuilder sb = new StringBuilder();
  720.  
  721.                 sb.Append(alreadyConverted);
  722.                 sb.Append(" ");
  723.                 sb.Append(firstWord.Substring(0, 1).ToUpper(CultureInfo.CurrentCulture));
  724.  
  725.                 if (firstWord.Length > 1)
  726.                 {
  727.                     sb.Append(firstWord.Substring(1).ToLower(CultureInfo.CurrentCulture));
  728.                 }
  729.  
  730.                 return ToTitleCaseHelper(restOfString, sb.ToString());
  731.             }
  732.         }
  733.  
  734.         /// <summary>
  735.         /// Since MS Word 2010 the SimpleField element is not longer used. It has been replaced by a combination of
  736.         /// Run elements and a FieldCode element. This method will convert the new format to the old SimpleField-compliant
  737.         /// format.
  738.         /// </summary>
  739.         /// <param name="mainElement"></param>
  740.         internal static void ConvertFieldCodes(OpenXmlElement mainElement)
  741.         {
  742.             //  search for all the Run elements
  743.             Run[] runs = mainElement.Descendants<Run>().ToArray();
  744.             if (runs.Length == 0) return;
  745.  
  746.             Dictionary<Run, Run[]> newfields = new Dictionary<Run, Run[]>();
  747.  
  748.             int cursor = 0;
  749.             do
  750.             {
  751.                 Run run = runs[cursor];
  752.  
  753.                 if (run.HasChildren && run.Descendants<FieldChar>().Count() > 0
  754.                     && (run.Descendants<FieldChar>().First().FieldCharType & FieldCharValues.Begin) == FieldCharValues.Begin)
  755.                 {
  756.                     List<Run> innerRuns = new List<Run>();
  757.                     innerRuns.Add(run);
  758.  
  759.                     //  loop until we find the 'end' FieldChar
  760.                     bool found = false;
  761.                     string instruction = null;
  762.                     RunProperties runprop = null;
  763.                     do
  764.                     {
  765.                         cursor++;
  766.                         run = runs[cursor];
  767.  
  768.                         innerRuns.Add(run);
  769.                         if (run.HasChildren && run.Descendants<FieldCode>().Count() > 0)
  770.                             instruction += run.GetFirstChild<FieldCode>().Text;
  771.                         if (run.HasChildren && run.Descendants<FieldChar>().Count() > 0
  772.                             && (run.Descendants<FieldChar>().First().FieldCharType & FieldCharValues.End) == FieldCharValues.End)
  773.                         {
  774.                             found = true;
  775.                         }
  776.                         if (run.HasChildren && run.Descendants<RunProperties>().Count() > 0)
  777.                             runprop = run.GetFirstChild<RunProperties>();
  778.                     } while (found == false && cursor < runs.Length);
  779.  
  780.                     //  something went wrong : found Begin but no End. Throw exception
  781.                     if (!found)
  782.                         throw new Exception("Found a Begin FieldChar but no End !");
  783.  
  784.                     if (!string.IsNullOrEmpty(instruction))
  785.                     {
  786.                         //  build new Run containing a SimpleField
  787.                         Run newrun = new Run();
  788.                         if (runprop != null)
  789.                             newrun.AppendChild(runprop.CloneNode(true));
  790.                         SimpleField simplefield = new SimpleField();
  791.                         simplefield.Instruction = instruction;
  792.                         newrun.AppendChild(simplefield);
  793.  
  794.                         newfields.Add(newrun, innerRuns.ToArray());
  795.                     }
  796.                 }
  797.                
  798.                 cursor++;
  799.             } while (cursor < runs.Length);
  800.  
  801.             //  replace all FieldCodes by old-style SimpleFields
  802.             foreach (KeyValuePair<Run, Run[]> kvp in newfields)
  803.             {
  804.                 kvp.Value[0].Parent.ReplaceChild(kvp.Key, kvp.Value[0]);
  805.                 for (int i = 1; i < kvp.Value.Length; i++)
  806.                     kvp.Value[i].Remove();
  807.             }
  808.         }
  809.     }
  810. }