TL;DR Can I generate Word documents with .NET like XAML ItemTemplates?
I'm finding it pretty hard to come up with a solution that meets all my requirements, so I thought I'll throw this out to stackoverflow in the hope someone can guide me. Much thanks in advance.
Simply put, I need to generate Word document based on data from a database.
My ideal solution: I want it to work like how DataTemplates work in xaml. I've found heaps of examples and solutions where the Template represents a static document, with bits of (single) dynamic content which needs to be replaced.
e.g. WordDocGenerator
The thing is, I need a solution where I can specify a template for each level of the hierarchy, and the document generator will use these item level templates to build a final document based on a Document level template.
My specific requirements are:
Let's say the data hierarchy is like this
class Country
{
public string Name { get; set; }
public IList<City> { get; set; }
}
class City
{
public string Name { get; set; }
public IList<Suburb> { get; set;}
}
class Suburb
{
public string Name { get; set; }
public int Postcode { get; set; }
}
In my mind the solution will be a function call, which accepts a list of countries.
// returns path to generated document
public static string GenerateDocument(IList<Country> countries);
This function will accept the list of countries, and for each country
Finally, these generated document 'pieces' will be accumulated into one final Word Document using a Document level template, which will specify the Title page, headers/footers, TOC.
I eventually got what I wanted. I did everything manually, with a lot of help from Eric White's articles.
So a taste of the source is this. Have a template ensuring the first three paragraphs are the 3 levels of hierarchy you want. Loop through your collections, clone the node, replace the text, repeat.
private const string COUNTRY_TITLE = "[[CountryTitle]]"
private const string CITY_TITLE = "[[CityTitle]]"
private const string SUBURB_TITLE = "[[SuburbTitle]]"
using (WordprocessingDocument myDoc = WordprocessingDocument.Open(outputPath, true))
{
var mainPart = myDoc.MainDocumentPart;
var body = mainPart.Document.Body;
var originalCountryPara = body.ElementAt(0);
var originalCityPara = body.ElementAt(1);
var originalSuburbPara = body.ElementAt(2);
foreach (var country in Countries)
{
if (!String.IsNullOrEmpty(country.Title))
{
// clone Country level node on template
var clonedCountry = originalCountryPara.CloneNode(true);
// replace Country title
Helpers.CompletelyReplaceText(clonedCountry as Paragraph, COUNTRY_TITLE, country.Title);
body.AppendChild(clonedCountry);
}
foreach (var city in country.Cities)
{
if (!String.IsNullOrEmpty(city.Title))
{
// clone City level node on template
var clonedCity = originalCityPara.CloneNode(true);
// replace City title
Helpers.CompletelyReplaceText(clonedCity as Paragraph, CITY_TITLE, city.Title);
body.AppendChild(clonedCity);
}
foreach (var suburb in city.Suburbs)
{
// clone Suburb level node on template
var clonedSuburb = originalSuburbPara.CloneNode(true);
// replace Suburb title
Helpers.CompletelyReplaceText(clonedSuburb as Paragraph, SUBURB_TITLE, suburb.Title);
body.AppendChild(clonedSuburb);
}
}
}
body.RemoveChild(originalCountryPara);
body.RemoveChild(originalCityPara);
body.RemoveChild(originalSuburbPara);
mainPart.Document.Save();
}
/// <summary>
/// 'Completely' refers to the fact this method replaces the whole paragraph with newText if it finds
/// existingText in this paragraph. The only thing spared is the pPr (paragraph properties)
/// </summary>
/// <param name="paragraph"></param>
/// <param name="existingText"></param>
/// <param name="newText"></param>
public static void CompletelyReplaceText(Paragraph paragraph, string existingText, string newText)
{
StringBuilder stringBuilder = new StringBuilder();
foreach (var text in paragraph.Elements<Run>().SelectMany(run => run.Elements<Text>()))
{
stringBuilder.Append(text.Text);
}
string paraText = stringBuilder.ToString();
if (!paraText.Contains(existingText)) return;
// remove everything here except properties node
foreach (var element in paragraph.Elements().ToList().Where(element => !(element is ParagraphProperties)))
{
paragraph.RemoveChild(element);
}
// insert new run with text
var newRun = new Run();
var newTextNode = new Text(newText);
newRun.AppendChild(newTextNode);
paragraph.AppendChild(newRun);
}