基于现有模板生成Word文档的最佳方法是什么?

3

TL;DR 我能否像XAML ItemTemplates一样使用.NET生成Word文档?

我发现很难找到一个满足所有需求的解决方案,所以我想在stackoverflow上问问,希望有人可以指导我。非常感谢。

简单来说,我需要根据数据库中的数据生成Word文档。

我的理想解决方案:我希望它的工作方式类似于XAML中的DataTemplates。我找到了很多示例和解决方案,其中模板表示静态文档,其中包含一些需要替换的(单个)动态内容。

例如:WordDocGenerator

问题在于,我需要一种解决方案,可以为层次结构的每个级别指定模板,文档生成器将使用这些项目级别模板基于文档级模板构建最终文档。

我的具体要求是:

  • 最好使用.NET解决方案
  • 服务器上不需要安装Office
  • 存在一个仅封装文档“视图”的模板(不一定是Word模板),用户可以随意修改(当然有限制)。这非常重要,因为用户只需直接修改Word模板即可控制演示方式。
  • 每个数据层次结构的每个级别最好都有一个相应的模板。
  • 页眉和页脚
  • 目录

假设数据层次结构如下:

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; }
}

在我看来,解决方案将是一个函数调用,该函数接受一个国家列表。

// returns path to generated document
public static string GenerateDocument(IList<Country> countries);

这个函数将接受国家列表,并针对每个国家:
  • 使用预设的“Countries”模板来呈现该国家的数据。
  • 对于每个城市,在国家模板内使用预设的“Cities”模板来呈现城市数据。
  • 对于每个郊区,在城市模板内使用预设的“Suburbs”模板来呈现郊区数据。
最后,这些生成的文档“片段”将被累积到一个最终的Word文档中,该文档使用文档级别模板指定标题页、页眉/页脚和目录。

docx文档是普通旧的XML的ZIP存档。天空是极限,您可以随意操纵XML,甚至无需安装Office,也无需第三方库;只需在Word模板中设置书签,围绕要作为国家、城市、郊区子模板的部分,手动打开docx以查看在XML中的外观,然后以编程方式打开模板XML,提取书签部分,并为您拥有的所有国家/城市等复制它们。 - vladr
好的,那么我理论上可以拥有这些项目级模板吗?所以在我的想法中,你是说使用OpenXML加载节点,像任何其他XML一样,并以编程方式查找和替换带书签的占位符,然后将节点插入最终的文档模板中? - syclee
жІҰ错。如жһњдҢ е°†docx文件дҢњдёғzipеҺ‹зә©еЊ…打еәЂпәЊдҢ дәљзњ‹е€°word\document.xmlпәЊењЁй‡ЊйқұдҢ дәљзњ‹е€°дң‹е¦‚<w:bookmarkStart w:id="0" w:name="country_template"/><w:p...><...>This is country #Country</...></w:p><w:bookmarkStart w:id="1" w:name="city_template"/><w:p...><...>This is city #City</...></w:p><w:bookmarkStart w:id="2" w:name="suburb_template"/><w:p...><...>This is suburb #Suburb</...></w:p><w:bookmarkEnd w:id="2"/><w:bookmarkEnd w:id="1"/><w:bookmarkEnd w:id="0"/>гЂ‚ - vladr
示例文件: https://docs.google.com/open?id=0B_mBmA1S1tEAaklhbFJDcXA3ajA - vladr
太棒了,我会看一下的,谢谢。 - syclee
1
是的,就像@vladr所说的那样。您可能想花些时间阅读Eric White在http://ericwhite.com/blog/和http://blogs.msdn.com/b/ericwhite/上的文章。此外,http://www.openxmldeveloper.org也有很多文章。 - Todd Main
2个回答

3

Templater是为此用例而设计的。

您可以定义文档的区域(如表格或列表),这些区域将基于正在处理的当前对象进行复制。

免责声明:我是作者。


我已经看过Templater了,但是我看不出它怎么能用来显示层次化数据。我知道它可以做网格,但是我不认为它能够处理我的例子,其中可能有0到多个国家,而每个城市又可能有0到多个城市。 - syclee
当然可以。请查看 https://github.com/ngs-doo/TemplaterExamples/tree/master/SalesOrderMVP - Rikard Pavelic
非常感谢 @RikardPavelic……Templater 正是我所需要的。 - Charles

1

最终我得到了想要的结果。我手动完成了所有操作,并从Eric White的文章中获得了很多帮助。

源代码的一小部分如下。首先确保模板的前三段是你想要的三个层次结构。然后遍历你的集合,克隆节点,替换文本,重复此过程。

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);
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接