【问题标题】:What's the best approach to generate Word document based on existing templates基于现有模板生成 Word 文档的最佳方法是什么
【发布时间】:2012-11-23 03:07:02
【问题描述】:

TL;DR 我可以使用 .NET 生成 Word 文档,例如 XAML ItemTemplates 吗?

我发现很难想出一个满足我所有要求的解决方案,所以我想我会把它扔到 stackoverflow 中,希望有人可以指导我。非常感谢。

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

我的理想解决方案:我希望它像 DataTemplates 在 xaml 中一样工作。我发现了很多示例和解决方案,其中模板表示静态文档,需要替换一些(单个)动态内容。

例如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);

此函数将接受国家列表,并针对每个国家

  • 使用为国家准备的模板来呈现国家数据。
  • 对于国家/地区的每个城市,使用为城市准备的模板在国家/地区模板中显示城市数据
  • 对于城市中的每个郊区,使用为郊区准备的模板在城市模板中呈现郊区数据。

最后,这些生成的文档“片段”将使用文档级模板累积到一个最终的 Word 文档中,该模板将指定标题页、页眉/页脚、TOC。

【问题讨论】:

  • docx 文档是普通旧 XML 的 ZIP 存档。没有限制,你可以随意操作XML,甚至不需要安装Office,也不需要第三方库;只需在 Word 模板中围绕要充当 Country-、City-、Suburb-子模板的部分设置书签,手动打开 docx 以查看 XML 中的内容,然后以编程方式打开模板 XML,拉出带书签的部分,并将它们复制到尽可能多的国家/城市/等。你有。
  • 好的,所以理论上我可以拥有这些项目级别的模板?所以在我看来,我认为您所说的是使用 OpenXMl 来加载节点,就像任何其他 XML 一样,并以编程方式查找并用内容替换“书签”存根?之后将节点插入到最终的文档模板中?
  • 没错。如果您将 docx 作为 zip 存档查看内部,您将看到 word\document.xml,在那里您将看到例如&lt;w:bookmarkStart w:id="0" w:name="country_template"/&gt;&lt;w:p...&gt;&lt;...&gt;This is country #Country&lt;/...&gt;&lt;/w:p&gt;&lt;w:bookmarkStart w:id="1" w:name="city_template"/&gt;&lt;w:p...&gt;&lt;...&gt;This is city #City&lt;/...&gt;&lt;/w:p&gt;&lt;w:bookmarkStart w:id="2" w:name="suburb_template"/&gt;&lt;w:p...&gt;&lt;...&gt;This is suburb #Suburb&lt;/...&gt;&lt;/w:p&gt;&lt;w:bookmarkEnd w:id="2"/&gt;&lt;w:bookmarkEnd w:id="1"/&gt;&lt;w:bookmarkEnd w:id="0"/&gt;
  • 是的,正如@vladr 所说。您可能想花一些时间阅读 Eric White 在ericwhite.com/blogblogs.msdn.com/b/ericwhite 上的资料。另外,openxmldeveloper.org 也有很多文章。

标签: .net ms-word openxml


【解决方案1】:

Templater 的设计考虑了该用例。

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

免责声明:我是作者。

【讨论】:

  • 我看过 Templater,但我看不出它如何用于显示分层数据。我可以看到它有网格,但我认为它不能做我的例子,可能有 0 对多的国家,每个城市都有 0 对多的城市?
  • 非常感谢@RikardPavelic ... Templater 正是我所需要的。
【解决方案2】:

我最终得到了我想要的。在 Eric White 的文章的帮助下,我手动完成了所有操作。

所以源头的味道是这样的。有一个模板,确保前三段是您想要的 3 个层次结构级别。循环遍历你的集合,克隆节点,替换文本,重复。

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

【讨论】:

    猜你喜欢
    • 2010-09-10
    • 1970-01-01
    • 1970-01-01
    • 2011-03-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多