WordML 模板编辑器 – 使用 Office 2003 WordML 功能进行数据可视化
本文介绍如何使用 Office 2003 WordML 功能来显示存储在 .NET 数据集中的数据。
引言
我们中的许多人都处理过数据操作和数据呈现。很可能,你的经理或客户多次要求数据报告或文档要与 Office 兼容。他们习惯于使用 Office,他们依赖 Office,而你无法改变这一事实。他们可以使用报表工具提供的导出功能,但我猜他们更喜欢创建自己的模板,并且希望在那里看到数据。与其提供一个难以管理的 Office 文档的替代方案,不如“给”他们的数据披上 Office 的外衣。他们会欣赏的。
WordML 模板编辑器实用工具
该实用工具 WordML 模板编辑器允许用户配置和填充 Office 2003 XML 模板,该模板是用 WordML 语言(用于定义 MS Word 文档的标记语言)编写的。这个“模板”是一个简单的 XML 文件,包含一些预定义的结构,而不是 MS Word 的“.DOT”模板。
这个想法来源于 MS Word 的功能,该功能允许用户使用外部数据源(邮件合并)并管理元数据(合并域)。不幸的是,在邮件合并的情况下,当你希望在同一文档中显示许多记录时,会出现一个问题。当直接从 MS Word 应用程序使用邮件合并功能时,在文档中显示数据源的所有记录已经足够困难。你必须将主文档配置为“目录”类型,并且对于每条记录,所有文档内容都会重复处理。但如果你希望在同一文档中显示两个或更多记录集,情况会变得更加复杂。
假设我们想从 Northwind 数据库提取关于订单及其详细信息以及产品列表的信息。因为输出将是正式文档,所以我们希望显示来自配置文件中的 Northwind 公司信息。所以,我们有四个记录集:两个只包含一条记录(订单和公司信息),另外两个包含多条记录(订单详细信息和产品列表)。
模板可以从 MS Word 应用程序构建,也可以从 WordML 模板编辑器实用工具应用程序构建,该实用工具应用程序托管 MS Word 应用程序并拥有对其活动文档的完全内部访问权限。
如果你想直接从 MS Word 应用程序设置模板,你必须将其链接到一个包含邮件合并所需字段的外部数据源。最简单的解决方案是将一个只包含字段定义和一个虚拟记录作为假记录的文本文件设置为数据源。缺点是,如果你想将模板移动到另一台计算机,你还必须移动数据源。为了唯一性和区分属于不同表的字段,字段名必须包含表名和表中的字段名(连接起来)。你可以通过在模板中添加MergeField
类型的对象来避免使用外部数据源。缺点是你必须知道字段名(当然,是通过与它们的表名连接而成的)。
如果模板是使用实用工具应用程序构建的,你则不需要外部数据源,但你需要使用你想要显示的数据集(结构,而不是数据)。该实用工具显示数据集的表列表以及每个表对应的字段。你可以将任何字段插入到活动文档的任何文本位置。
四个表中有两个表只有一条记录。在模板中,你可以区分将只显示一次的元素和将重复显示的元素(对应于记录的数量)。对于前者,显示是通过在指定位置进行简单插入来完成的。对于后者,显示稍微复杂一些。因为 MS Word 文档不允许通过标识符访问元素,所以我选择了书签插入解决方案。用于复制元素的书签具有标准名称:数据表名 + "WMLRepeat" token + 索引,其中索引 >= 1 用于数据表在文档中出现多次的情况。当解析模板时,如果文档中出现这样的书签,分析器会尝试识别表对象,如果成功,则表行(包含至少一个合并域)会被复制,并且对于数据表中的每条记录,字段都会被替换为数据。
当字段被数据替换时,你可以执行一些日期和数字格式化。格式化信息存储在内部模板变量中。
到目前为止,只有表行对象可以作为重复元素使用,但如果你想使用其他对象,你可以使用一个隐藏的表对象并添加文本框、列表、格式化文本等。
模板创建
模板创建步骤如下:
- 创建或提供可供用户访问的数据集:这可以是一个可自定义的条目列表,该列表在内部为模板编辑器提供数据源;
- 打开一个 MS Word 文档,或创建一个新的文档(编辑器可以处理标准的 DOC MS Word 文档或 XML MS Word 文档,但只有 XML 文档可以用作数据可视化模板);
- 用你需要的视觉元素修改“模板”;
- 选择要插入到“模板”中的数据表和字段;
- 对于“重复”元素(在文档中出现多次的记录),在活动文档中选择指定位置(用作 MS Word 表对象中模板行的行),并插入一个特殊书签(新书签将添加到“WMLRepeat”书签列表中);
- 可选地添加语言名称、日期时间格式、数字分组分隔符、小数点分隔符、小数位数;
- 将模板保存为 XML。
主编辑器窗体(frmWordControl
)托管 MS Word 应用程序在一个控件中,如“致谢”部分指定的两篇文章中所述。所需的结构使用 MS Word DOM 添加到模板中。它使用数据集对象或文本文件(如前一节所述构建)中的数据源。窗体实例化如下:
dsData = new DataSet();
dsData.Tables.Add(dt1);
dsData.Tables.Add(dt2);
…
frmWordControl frm = new frmWordControl(dsData);
frm.Show();
在表单中显示元数据后,您可以:
- 在指定位置插入合并域
Word.Range rng = wordCtl.Application.Selection.Range; item = listFields.Items[fieldIndex].ToString(); if(item.Trim() != string.Empty) { wordCtl.Application.ActiveDocument.MailMerge.Fields.Add(rng, item); }
- 在指定位置插入特殊的“重复”书签
Word.Range rng = wordCtl.Application.Selection.Range; object oRng = (object)rng; wordCtl.Application.ActiveDocument.Bookmarks.Add(tableName + "WMLRepeat" + index.ToString(), ref oRng);
- 指定格式化信息(语言、日期格式、小数点分隔符、数字分组分隔符、小数位数——存储在内部文档变量中)
object oVariableValue; object oVariableName; Word.Variable var; oVariableValue = (object)languageName; oVariableName = (object)"LanguageName"; var = wordCtl.Application.ActiveDocument.Variables.get_Item( ref oVariableName); if(var != null) var.Value = languageName; else wordCtl.Application.ActiveDocument.Variables.Add("LanguageName", ref oVariableValue);
您可以创建新文档,从磁盘打开它们,并将它们保存为标准的 DOC 或 XML 格式。
模板可视化和 CExWordMLFiller 类
模板可视化可以从 Windows 应用程序或 Web 应用程序执行。在这两种情况下,客户端计算机都应安装 Office 2003 或仅 Word Viewer 2003。
CExWordMLFiller
类负责检查WordML
模板并用数据集提供的数据填充它。假设你有一个数据集dsData
和一个 XML 文档xmlTemplateDoc
,其中包含模板。以下代码是获得混合了数据集数据和模板提供的视觉功能的新的 XML 文档所需的顺序:
CExWordMLFiller filler = new CExWordMLFiller(dsData, xmlTemplateDoc.OuterXml);
if(!filler.OperationFailed)
{
filler.Transform();
if(filler.OperationFailed)
{
foreach(string err in filler.ErrorList)
{
MessageBox.Show(err);
}
return;
}
}
string copyFileName = Path.GetTempFileName() + ".xml";
filler.WordMLDocument.Save(copyFileName);
filler
对象的WordMLDocument
属性包含转换结果对应的 XML 文档对象。你可以将结果保存到文件,或者将其用作文档、流或字符串。
构造函数需要两个参数:将被显示的数据集,以及作为字符串的模板内容。内容必须是有效的 WordML 字符串,并且将被加载到一个 XML 文档对象中。同样,数据集将与其架构一起加载到一个 XML 文档中,该文档将用于在转换结束时删除未填充的字段。出于某些原因(相应的数据表在数据集中丢失),某些字段可能无法填充数据。未填充的字段必须被“删除”,因为可视化文件可能包含“丑陋”的合并字段定义,如“«OrderOrderID»”。
LoadTemplate
方法为数据集值准备日期和数字格式化。格式化信息从docVar
节点(文档内部变量)加载。
XmlNode varNode = xmlTemplateDoc.SelectSingleNode("//w:docVar" +
"[@w:name='LanguageName']/@w:val", nsmgr);
if(varNode != null)
{
languageName = varNode.Value;
try
{
ci = new CultureInfo(languageName, false);
numberFormat = ci.NumberFormat;
}
catch
{
languageName = null;
numberFormat = null;
}
}
else
{
languageName = null;
}
Transform
方法执行转换。此方法循环遍历数据集的Tables
集合,并调用TransformDataTable
方法,该方法检查每个数据表是否需要可视化(是否应显示其行)。之后,为了消除脏结构,它会移除“邮件合并”节点(RemoveMailMergeNode
)和未填充的字段(RemoveUnfilledFields
)。
TransformDataTable
方法识别“重复”元素的位置以及仅显示它们的位置。TransformWordMLTableRepeat
方法使用aml:annotation
WordML
元素(对应于“repeat”书签)来识别使用当前数据表行的 MS Word 表。它获取模板行节点(w:tr
),并使用三个参数调用TransformDataRow
方法:数据行、表节点和模板行节点。在处理完所有行后,模板行节点会从表中删除。
string tableName = dt.TableName;
XmlNodeList oColl =
xmlTemplateDoc.SelectNodes("//w:tbl[contains(descendant::aml:a" +
"nnotation/@w:name, '" + tableName + repeatAttribute +
"') and (contains(descendant::w:instrText, ' MERGEFIELD \"" +
tableName + "') or contains(descendant::w:instrText, ' MERGEFIELD " +
tableName + "'))]", nsmgr);
XmlNode templateRowNode;
if(oColl != null && oColl.Count > 0)
{
foreach(XmlNode tableNode in oColl)
{
templateRowNode = tableNode.SelectSingleNode("w:tr[contains" +
"(descendant::w:instrText, ' MERGEFIELD \"" +
tableName + "') or contains" +
"(descendant::w:instrText," +
" ' MERGEFIELD " + tableName + "')]", nsmgr);
if(templateRowNode != null)
{
foreach(DataRow dr in dt.Rows)
{
TransformDataRow(dr, tableNode, templateRowNode);
}
tableNode.RemoveChild(templateRowNode);
}
}
}
TransformDataRow
方法在当前表行节点中检查数据行字段的显示位置,并用实际数据替换其值。模板行节点被克隆,并插入到 MS Word 表对象中的模板行节点之前。
ReplaceFieldData
方法识别字段位置,并应用日期和数字格式化。
oColl = baseNode.SelectNodes("//w:p[w:r/w:instrText=' MERGEFIELD " +
fieldName + " ']", nsmgr);
…
foreach(XmlNode fieldNode in oColl)
{
//select run element containing the underlying data
dataNode = fieldNode.SelectSingleNode("//w:t[.='«" +
fieldName + "»']", nsmgr);
…
if(colType == typeof(DateTime))
{
if(dateTimeFormat != null)
{
DateTime dt = DateTime.Parse(data);
dataNode.InnerText = dt.ToString(dateTimeFormat);
}
else
{
dataNode.InnerText = data;
}
}
…
}
Windows 应用程序将通过其类型的关联来打开结果文件。
System.Diagnostics.Process.Start(copyFileName);
Web 应用程序需要更多代码才能在浏览器中显示输出。
Response.ClearContent();
Response.ClearHeaders();
Response.Clear();
Response.ContentType = "application/msword";
Response.Charset = "";
Response.AddHeader("Content-disposition",
"inline; filename=\"" +
copyFileName + "\"");
Response.AddHeader("Content-length", fi.Length.ToString());
Response.WriteFile(copyFileName);
Response.Flush();
Response.Close();
模板可视化和 CWordDOCFiller 类
正如 Trevor Farley 先生推荐的那样,我最近添加了CWordDOCFiller
类,该类能够解析 DOC 模板。该类执行的操作与CExWordMLFiller
类相同,但使用了 Word DOM。不幸的是,当访问Fields
集合时,如果数据量很大,会出现一些性能问题(对于同一个 Order 模板,DOM 版本耗时 2-3 分钟,而 XML 方法几乎瞬间完成)。当然,DOM 方法更具可读性且易于维护,并且可以应用于早期版本的 MS Word。
使用CWordDOCFiller
类所需的代码顺序是:
string templateFileName = Application.StartupPath + @"\Templates\Order.doc";
string copyFileName = Path.GetTempFileName() + ".doc";
File.Copy(templateFileName, copyFileName, true);
CWordDOCFiller filler = new CWordDOCFiller(dsData, copyFileName);
if(!filler.OperationFailed)
{
filler.Transform();
if(filler.OperationFailed)
{
foreach(string err in filler.ErrorList)
{
MessageBox.Show(err, "Error", MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
return;
}
}
else
{
foreach(string err in filler.ErrorList)
{
MessageBox.Show(err, "Error", MessageBoxButtons.OK,
MessageBoxIcon.Error);
}
}
构造函数接收两个参数:数据集和模板路径。为了避免修改模板,将其复制到%TEMP%文件夹。该类使用Word.Application
(在一个私有对象oApp
中)来加载和修改文档。格式化信息存储在Word.Variables
集合中,并在字段内容被数据替换时使用。用于应用转换的主要方法是Transform
,它将所有转换操作应用于数据集中的数据表。
bOperationFailed = false;
try
{
foreach(DataTable dt in dsData.Tables)
{
TransformTableRepeat(dt);
}
//fill with data no repeat
ReplaceFieldDataNoRepeat();
oApp.Visible = true;
}
catch(Exception ex)
{
while(ex != null)
{
errorList.Add(ex.Message);
ex = ex.InnerException;
}
bOperationFailed = true;
}
TransformTableRepeat
方法为每个DataRow
对象执行转换,从模板 Word 表行中消耗元信息。模板行被复制,复制的行是需要用数据填充的对象(在TransformDataRow
方法中)。最后,模板行被删除。
string tableName = dt.TableName;
foreach(Word.Bookmark bmk in oTemplateDoc.Bookmarks)
{
if(bmk.Name.StartsWith(tableName + repeatAttribute))
{
Word.Table tbl = bmk.Range.Tables[1];
Word.Row row = bmk.Range.Rows[1];
for(int i = 0; i < dt.Rows.Count; i++)
{
TransformDataRow(dt.Rows[i], tbl, row, bmk.Name, i);
}
row.Delete();
}
}
TransformDataRow
方法创建新字段并将它们添加到指定位置,确保新字段名称在文档中是唯一的。同样,它完成了新创建字段的数据填充。
DataTable dt = dr.Table;
string tableName = dt.TableName;
object oTemplateRow = (object) templateRow;
Word.Row row = tbl.Rows.Add(ref oTemplateRow);
string fieldName;
Word.Fields fields;
int fieldIndex;
Word.Field field = null;
string dataFieldName = string.Empty;
Type dataFieldType = typeof(object);
foreach(Word.Cell cell in templateRow.Cells)
{
fieldName = cell.Range.Fields[1].Code.Text.Trim();
fieldName = fieldName.Replace("MERGEFIELD ", string.Empty);
fieldName = fieldName + bmkName + rowIndex.ToString();
oTemplateDoc.MailMerge.Fields.Add(
row.Cells[cell.ColumnIndex].Range, fieldName);
fields = row.Cells[cell.ColumnIndex].Range.Fields;
fieldIndex = fields.Count;
field = fields[fieldIndex];
dataFieldName = field.Code.Text.Trim();
dataFieldName = dataFieldName.Replace("MERGEFIELD " +
dt.TableName, string.Empty);
dataFieldName = dataFieldName.Replace(bmkName +
rowIndex.ToString(), string.Empty);
dataFieldType = dt.Columns[dataFieldName].DataType;
ReplaceFieldData(field, dr[dataFieldName].ToString(),
dataFieldType);
}
ReplaceFieldDataNoRepeat
方法完成了“非重复”字段的数据填充。
bOperationFailed = false;
DataRow firstRow;
foreach(Word.Field field in oTemplateDoc.Fields)
{
if(field.Code.Text.IndexOf(repeatAttribute) == -1)
{
foreach(DataTable dt in dsData.Tables)
{
if(dt.Rows.Count > 0)
{
firstRow = dt.Rows[0];
for(int j = 0; j < dt.Columns.Count; j++)
{
if(field.Code.Text.Trim() == "MERGEFIELD " +
dt.TableName + dt.Columns[j].ColumnName)
ReplaceFieldData(field, firstRow[j].ToString(),
dt.Columns[j].DataType);
}
}
}
}
}
ReplaceFieldData
引用一个Word.Field
对象并设置数据,使用格式化信息。打开文件的代码不再需要,因为CWordDOCFiller
类中嵌入的 Application 对象允许显示转换的结果。
使用应用程序
解决方案WordDataSetTemplateEditor.root
包含四个项目:
WinWordControl
– 托管应用程序的用户控件;WordDataSetTemplateEditor
– 主编辑器项目;NorthwindDA
– Northwind 数据库的数据访问组件;Test
– 用于特定订单和订单详细信息可视化以及产品字母顺序列表的测试应用程序。
Templates文件夹中的模板Order.xml和Order.doc是Test应用程序所需的模板。当项目编译时,这些模板将被复制到应用程序启动路径中的类似文件夹中。
我注意到,如果你想更新一个字段,数据替换就会被取消,并且无法恢复。我认为这是一个消除 FIELD 定义的好理由。在 WordML 方法中,这需要为每个字段移除特定的 XML 节点(w:r/w:fldChar和w:r/w:instrText),而在 Word DOM 方法中,这个问题使用Fields
集合的Unlink
方法(oTemplateDoc.Fields.Unlink();
)得到了更优雅的解决。
谢谢
非常感谢 Matthias Hänel 和 Anup Shinde 出色的文章(.NET 的 Word 控件和将 Microsoft Word 集成到你的 .NET 应用程序中)。感谢 Trevor Farley 推荐我创建CWordDOCFiller