使用 OpenXML SDK 集成 Windows Workflow Foundation
往往涉及工作流的业务流程的需要,相应的文档创建或处理。 例如,这可以发生时 (要求贷款、 保险政策、 赎回股份,等等) 的申请已被批准或拒绝工作流过程中。 这可能由驱动程序 (自动) 或 (手动步骤) 作为承销商。 在这种情况下,可能需要编写,一封信或显示余额的电子表格制作。
在 2008 年 6 月的 MSDN 杂志问题,展示了如何实现此目的,使用 Microsoft Office 应用程序的对象模型 (msdn.microsoft.com/magazine/cc534981)。 使用作为基础的那篇文章,这次我带你如何集成 Microsoft Office 兼容的文档窗口工作流的基础 (WF) 而无需直接与 Office 应用程序进行交互。 这被通过使用 OpenXML SDK 2.0 操作文字处理和电子表格文档类型。 当然,相应的 Office 应用程序,Word 和 Excel。 尽管每个有它自己的 OpenXML 文档模型,有足够的相似之处允许使用一组隐藏大部分的工作流集成的目的差异的接口类。
由于白表包含在 Microsoft。NET 框架 4 客户端配置文件,任何。NET 框架 4 安装将包括白表库。 因为在大大简化了其使用。NET 框架 4 相比。NET 框架 3.0,要求甚至基本工作流功能的任何应用程序应考虑使用此自定义代码的位置。 这甚至适用情况下需要自定义活动,辅之以内置的活动。
我会展示如何自定义活动可以提供基于充当一个原型的 Office 文档的数据输入。 将数据条目的活动,反过来,数据传递到活动的每个文档类型,和活动为填充目标 Office 文档中使用这些数据字段。 我用 Visual Studio 2010 发展类以支持操作,如枚举的命名字段中提取它们的内容和从原型中文档中填充。 所有这些类使用 OpenXML SDK (而不是直接操作 Office 应用程序的对象模型。 创建支持数据输入和灌装中的文字处理和电子表格的文档类型的工作流活动。 通过只需调用文档类型的默认应用程序显示已完成的文档。 我写了使用 Visual Basic 代码。
总体设计
任何设计集成工作流和办公室有三个一般要求:获取数据到工作流 ; 创建或更新 Office 文档 ; 数据处理 存储或传递的输出文件。 为了支持这些需求,创建一组类,以提供统一的接口来使用 OpenXML SDK 的基础办公文档格式。 这些类提供的方法:
- 获取文档中的潜在目标字段的名称。 我将使用通用术语"字段"来描述了这些。 在 Word 的情况下,这些是书签 ; Excel 将使用的命名的范围。 最一般的情况下,书签和命名的范围可能非常复杂,但是单个位置的简单情况将用于书签和命名区域的单个单元格。 书签总是包含文本,而电子表格单元格可以包含文本、 数字或日期。
- 通过接受输入的数据和匹配文档数据填充在目标文档中的域。
为实施活动,以填补在 Office 文档中,我使用白表 4 CodeActivity 模型。 此模型可以极大地简化了白表 3.0,并提供了很多更清晰的实现。 例如,它不再需要显式声明依赖项属性。
配角
工作流背后一套旨在支持的 OpenXML SDK 功能的类。 类执行原型文档加载到内存的流、 寻找和填写字段 (如书签或命名的区域),以及由此产生的输出文档保存的关键的功能。 常见的功能将会收集到一个基类,包括加载和保存内存流。 省略掉错误检查这里为清楚起见,但它包含在随附的代码下载。 图 1 演示如何加载和保存 OpenXML 文档。
图 1 加载和保存 OpenXML 文档
Public Sub LoadDocumentToMemoryStream(ByVal documentPath As String)
Dim Bytes() As Byte = File.ReadAllBytes(documentPath)
DocumentStream = New MemoryStream()
DocumentStream.Write(Bytes, 0, Bytes.Length)
End Sub
Public Sub SaveDocument()
Dim Directory As String = Path.GetDirectoryName(Me.SaveAs)
Using OutputStream As New FileStream(Me.SaveAs, FileMode.Create)
DocumentStream.WriteTo(OutputStream)
OutputStream.Close()
DocumentStream.Close()
End Using
End Sub
在工作流中获取数据
我面临将 Office 文档集成到工作流中的第一个挑战之一是如何传递目的地为这些文档中的字段的数据。 标准工作流结构依赖于促进知识与活动关联的变量的名称。 可以用不同的范围提供到该工作流,和其他活动的可见性定义变量。 我决定直接应用这种模式是过于僵硬的因为它需要绑得太紧,文档字段的整体工作流设计。 Office 集成,工作流活动作为 Office 文档的代理。 它不是现实,试图预先确定在文档中,字段的名称,这将需要匹配特定的文档以自定义活动。
如果你看看参数传递到活动的方式,你会发现他们正在通过作为字典对象的字符串)。 要填写 Office 文档中的字段,您需要两项信息:字段和要插入的值的名称。 我就开发了应用程序使用工作流产品和办公室之前,和我所采用的一般策略奏效:枚举文档中的命名的字段,并与他们输入参数的名称相匹配。 如果文档字段匹配的模式基本输入的参数字典 (字符串,对象) 中,它们可以被直接传递中。 然而,这种做法情侣工作流太紧到文档。
向文档中的域与对应的命名变量,而不是我决定使用泛型字典 (的字符串,字符串) 传达这些字段的名称。 我的命名此参数的字段,并在每项活动中使用它。 这样的词典中的每个条目是类型 KeyValuePair (的字符串,字符串)。 关键用于匹配字段的名称 ; 值用来填写字段内容。 然后,此字段字典是工作流中的参数之一。
一个简单的 Windows 演示文稿基础 (WPF) 窗口中的代码的几个线条和更少添加到现有的应用程序时,您可以启动工作流:
Imports OfficeWorkflow
Imports System.Activities
Class MainWindow
Public Sub New()
InitializeComponent()
WorkflowInvoker.Invoke(New Workflow2)
MessageBox.Show("Workflow Completed")
Me.Close()
End Sub
End Class
我想要的活动通常是有用的并有多个战略提供的输入的文档。 为允许此,活动有一个名为 InputDocument 的常见参数。 这些可以附加到变量,反过来,连接到的其他活动产出随着需要的工作流的指示。 该参数包含用作一个原型的输入文档的路径。 然而,该代码还提供使用其名是 InputDocument,如果它包含文档类型适合于目标 Office 应用程序的路径字段参数。 一个附加参数允许在一个名为 TargetActivity 的输入字段命名目标活动。 这允许多个活动的序列 ; 例如,要评估的适用性的原始输入文档中的字段。
任何实际的工作流将有输入数据的来源。 用于演示目的,我从任何受支持的 Office 文档类型使用数据输入活动,其中可以绘制其输入 (字段和默认值)。 这项活动将打开包含数据网格和用于选择文档并保存数据字段按钮的对话框。 用户选择一个文档作为一个原型后,DataGrid 充满中可用的字段从文档,再加上的 InputDocument、 OutputDocument 和 TargetActivity 的字段,如中所示图 2。 (顺便说一下,朱莉列尔曼 2011 年 4 月数据点列中,"撰写 WPF 数据网格列模板为好的用户体验,",你会发现在 msdn.microsoft.com/magazine/gg983481,已启用单击编辑,例如在网格中的 FocusManager 的使用重要提示 图 2。)
图 2 数据条目活动接口
处理数据文件
正如我前面指出的 Office 文档类型的每个有自己的方式提供的已命名的字段集合。 每项活动编写支持特定的文档类型,但支持所有的 Office 文档类型的活动遵循相同的图案。 如果提供的 InputDocument 属性,则它用作文档的路径。 如果 InputDocument 属性为空,活动看上去在字段属性中的 InputDocument 值,如果发现,检查的看它是否包含路径后缀匹配文档键入活动处理。 活动还将尝试查找文档通过附加适当的后缀。 如果满足这些条件,InputDocument 属性设置为此值,并处理收益。
从字段集合的每个匹配的条目用于填充在输出文档中的相应字段中。 这作为相应的工作流变量 (OutputDocument),要么传入的或者为 OutputDocument KeyValuePair 的条目字段集合中找到。 在每种情况下,如果输出文档已没有后缀,会附加适当的默认后缀。 这允许对有可能用于创建不同的文档类型或甚至多个文档的不同类型相同的值。
输出文件将存储在指定的路径。 在大多数实际工作流环境中,这将是一个网络共享或 SharePoint 文件夹。 为简单起见,我在代码中使用的本地路径。 另外,Word 和 Excel 活动代码检查输入相应类型的文档。 在示范工作流中,输入的文档默认的字段选择作为基础的原型。 用户可以对此进行更改,以便从字段派生的 Word 文档可以用来定义输入 Excel 文档,反之亦然。 图 3 演示用于填充 Word 文档中的代码。
在活动中的字处理文档中填充的图 3
Protected Overrides Sub Execute(ByVal context As CodeActivityContext)
InputFields = Fields.Get(Of Dictionary(Of String, String))(context)
InputDocumentName = InputDocument.Get(Of String)(context)
If String.IsNullOrEmpty(InputDocumentName) Then InputDocumentName = _
InputFields("InputDocument")
OutputDocumentName = OutputDocument.Get(Of String)(context)
If String.IsNullOrEmpty(OutputDocumentName) Then OutputDocumentName = _
InputFields("OutputDocument")
' Test to see if this is the right activity to process the input document
InputFields.TryGetValue(("TargetActivity"), TargetActivityName)
' If there is no explicit target, see if the document is the right type
If String.IsNullOrEmpty(TargetActivityName) Then
If Not (InputDocumentName.EndsWith(".docx") _
Or InputDocumentName.EndsWith(".docm")) _
Then Exit Sub
'If this is the Target Activity, fix the document name as needed
ElseIf TargetActivityName = Me.DisplayName Then
If Not (InputDocumentName.EndsWith(".docx") _
Or InputDocumentName.EndsWith(".docm")) _
Then InputDocumentName &= ".docx"
End If
Else
Exit Sub
End If
' This is the target activity, or there is no explicit target and
' the input document is a Word document
Dim InputWordInterface = New WordInterface(InputDocumentName, InputFields)
If Not (OutputDocumentName.EndsWith(".docx") _
Or OutputDocumentName.EndsWith(".docm")) _
Then OutputDocumentName &= ".docx"
InputWordInterface.SaveAs = OutputDocumentName
Dim Result As Boolean = InputWordInterface.FillInDocument()
' Display the resulting document
System.Diagnostics.Process.Start(OutputDocumentName)
End Sub
在图 3,特别注意以下行:
Dim InputWordInterface = _
New WordInterface(InputDocumentName, InputFields))
这是构造 WordInterface 类实例时。 已通过路径到作为一个原型,字段数据一起使用的文档。 只需使用相应的属性中的形式由该类的方法存储。
WordInterface 类提供了在目标文档中填充的功能。 输入的文档作为一个原型,从中创建的基础 OpenXML 文档的内存中副本。 这是一个重要的步骤 — — 的内存中副本是一个,它填充,然后保存为输出文件。 内存中副本的创建相同的每个文档类型和 OfficeInterface 基类中被处理。 然而,保存输出文件是更特定于每个类型。 图 4 显示 Word 文档如何填写由 WordInterface 类。
图 4 填写在 Word 文档中使用 WordInterface 类
Public Overrides Function FillInDocument() As Boolean
Dim Status As Boolean = False
' Assign a reference to the existing document body.
Dim DocumentBody As Body = WordDocument.MainDocumentPart.Document.Body
GetBuiltInDocumentProperties()
Dim BookMarks As Dictionary(Of String, String) = Me.GetFieldNames(DocumentBody)
' Determine dictionary variables to use -
based on bookmarks in the document matching Fields entries
If BookMarks.Count > 0 Then
For Each item As KeyValuePair(Of String, String) In BookMarks
Dim BookMarkName As String = item.Key
If Me.Fields.ContainsKey(BookMarkName) Then
SetBookMarkValueByElement(DocumentBody, BookMarkName, Fields(BookMarkName))
Else
' Handle special case(s)
Select Case item.Key
Case "FullName"
SetBookMarkValueByElement(DocumentBody, _
BookMarkName, GetFullName(Fields))
End Select
End If
Next
Status = True
Else
Status = False
End If
If String.IsNullOrEmpty(Me.SaveAs) Then Return Status
Me.SaveDocument(WordDocument)
Return Status
End Function
我添加称为 FullName 的特殊案例字段名称。 如果文档包含一个具有此名称的字段,我将连接输入的字段称为标题、 名字和姓氏填写它。 这样的逻辑是在调用 GetFullName 函数。 因为所有 Office 文档类型都有相似的需求,此函数是 OfficeInterface 基类沿与其他一些常见的属性中。 我已经用 Select Case 语句来做这一个可扩展性点。 例如,您可以添加连接输入的字段称为地址 1、 地址 2、 城市、 州、 邮政编码和国家的 FullAddress 字段。
存储输出文档
每个活动类有一个 OutputDocument 的属性,可以通过几种方式进行设置。 在设计器中,属性都可以绑定到工作流水平参数或常量的值。 在运行时,每项活动会在它的 OutputDocument 属性保存其文档的路径。 如果这不设置,它将查找在其键名为 OutputDocument 的字段集合中。 如果此值以适当的后缀结尾,是直接使用。 如果它没有适当的后缀,被添加了一个。 该活动然后保存输出文档。 这允许最大的灵活性,在决定往哪儿放到输出文档的路径。 因为省略后缀,则可以通过以下两种活动类型相同的值。 这里是如何保存的 Word 文档,首先确保内存流更新,然后使用基类中的方法:
Public Sub SaveDocument(ByVal document As WordprocessingDocument)
document.MainDocumentPart.Document.Save()
MyBase.SaveDocument()
End Sub
工作流示例
图 5 显示我将展示如何整合工作中使用的简单的工作流。 第二个示例使用流程图所示图 6。 我又去通过每个活动类型和谈论他们做的但是,首先让我们看看每个工作流干什么。
图 5 简单的 Office 集成工作流
图 6 流程图 Office 集成的工作流
工作流由一个简单的序列,依次调用每个活动类型组成。 Word 和 Excel 活动检查输入的文档类型,因为他们不会尝试处理错误的类型。
流程图中的工作流图 6 使用 Switch 活动决定应调用哪个办事处的活动。 它做出此决定使用简单的表达式:
Right(Fields("InputDocument"), 4)
例 docm 和 docx 被定向到的单词,虽然中兑换 xlsx 和 xlsm 被定向到 Excel。
我将使用图 5 来描述每个活动,但在行动的行动图 6 类似。
数据输入
顶部的图 5,你可以看到 DataEntryActivity 类的实例。 DataEntryActivity 提供的 DataGrid 控件,通过提取的 InputDocument,在这种情况下用户所选的已命名的字段,则填充的 WPF 窗口。 该控件允许用户选择的无论是否之一最初提供的文档。 然后,用户可以输入或修改字段中的值。 如中所示,使所需的双向绑定到数据网格,提供一个单独的 ObservableCollection 类图 7。
图 7 显示字段的 ObservableCollection
Imports System.Collections.ObjectModel
' ObservableCollection of Fields for display in WPF
Public Class WorkflowFields
Inherits ObservableCollection(Of WorkFlowField)
'Create the ObservableCollection from an input Dictionary
Public Sub New(ByVal data As Dictionary(Of String, String))
For Each Item As KeyValuePair(Of String, String) In data
Me.Add(New WorkFlowField(Item))
Next
End Sub
Public Function ContainsKey(ByVal key As String) As Boolean
For Each item As WorkFlowField In Me.Items
If item.Key = key Then Return True
Next
Return False
End Function
Public Shadows Function Item(ByVal key As String) As WorkFlowField
For Each Field As WorkFlowField In Me.Items
If Field.Key = key Then Return Field
Next
Return Nothing
End Function
End Class
Public Class WorkFlowField
Public Sub New(ByVal item As KeyValuePair(Of String, String))
Me.Key = item.Key
Me.Value = item.Value
End Sub
Property Key As String
Property Value As String
End Class
InputDocument 可以是受支持的 Office 文档类型,(.docx 或.docm) 的 Word 或 Excel (.xlsx 或.xlsm)。 要从文档中提取字段,调用适当的 OfficeInterface 类。 它加载目标文档作为 OpenXML 对象和枚举的字段 (以及它们的内容),如果存在。 包含宏的文档格式支持,并且宏会被带到输出文档。
由 DataEntryActivity 提供的字段之一是 TargetActivity 字段。 这是只是其字段属性是字段的输入文档从收集用填充活动的名称。 TargetActivity 字段用于其他活动作为一种方式来确定是否处理数据字段。
WordFormActivity
WordFormActivity 对文字处理 (Word) 文档进行操作。 它匹配的字段条目,在 Word 文档中具有相同名称的所有书签。 它然后插入 Word 书签的字段的值。 这项活动将接受 (.docm) 的 Word 文档或无宏 (.docx)。
ExcelFormActivity
ExcelFormActivity 对电子表格 (Excel) 文档进行操作。 它匹配任何简单的命名区域,具有相同名称的 Excel 文档中的字段条目。 它然后插入命名范围的 Excel 的字段的值。 这项活动将接受 (.xlsm) 的 Excel 文档或不 (.xlsx) 宏。
Excel 文档类型都有一些附加的特殊功能,如果填写数据进行格式化并正确处理,需要特殊处理。 这些功能之一是隐式的日期和时间格式的品种。 幸运的是,这些都是罄竹难书 (请参见 ECMA 376 部分 1,在 18.8.30 bit.ly/fUUJ)。 当遇到 ECMA 格式时,需要将其转换为相应的。网格式。 例如,ECMA 格式 mm-dd-yy 变成 yyy/M/dd。
此外,电子表格有一个概念的共享字符串,并将值插入到是包含共享的字符串的单元格时,将需要特殊处理。 这里使用的 InsertSharedStringItem 方法被从包含 OpenXML SDK 中的一个:
If TargetCell.DataType.HasValue Then
Select Case TargetCell.DataType.Value
Case CellValues.SharedString ' Handle case of a shared string data type
' Insert the text into the SharedStringTablePart.
Dim index As Integer = InsertSharedStringItem(NewValue, SpreadSheetDocument)
TargetCell.CellValue.Text = index.ToString
Status = True
End Select
End If
最后的润色
示例工作流只是宣布自己完成。 活动,选择文档类型或 TargetActivity 字段中,创建其输出文档中指定的位置。 从这里它可以拾取的其他活动进行其他处理。 用于演示目的,每项活动结束通过启动输出文档,依靠 Windows 在相应的应用程序中打开它:
System.Diagnostics.Process.Start(OutputDocumentName)
如果您要打印的相反,可以使用以下内容:
Dim StartInfo As New System.Diagnostics.ProcessStartInfo( _
OutputDocumentName) StartInfo.Verb = "print"
System.Diagnostics.Process.Start(StartInfo)
因为我们正在使用的只有文档格式,仍没有必要知道安装的 Office 版本的工作流应用程序的 !
在实际生产环境中,更多的工作通常会跟随。 可能作出的数据库项,和文档可能最终会通过电子邮件发送或打印以及发送给客户。 生产工作流应用程序也可能利用其他服务,如持久性和跟踪。
接近尾声了
一种基本的设计方法与 Office 文档使用 OpenXML SDK 接口窗口工作流基础 4 出轮廓。 示例工作流说明了这种方法,并显示如何实现自定义 Office 文档工作流中使用的数据的方式。 此解决方案生成的类是随时可修改的和可扩展的满足各种类似的需要。 虽然工作流活动将被写入利用白表 4 和。NET 框架 4,也符合 Office 接口库。NET 框架 3.5。
Rick Spiewak 是与米特雷公司的铅软件系统工程师 他与美国工作 空军的任务规划电子系统中心。Spiewak 已与自 1993 年以来的 Visual Basic 和微软合作。2002 年以来,当他是 Visual Studio 的 beta 版测试网框架。NET 2003 年。 他有多年的经验,结合多种工作流工具的 Office 应用程序。
多亏了以下的技术专家审查这篇文章:Andrew Coates