Бизнес-процессы, включающие какой-либо рабочий процесс, часто требуют создания или обработки соответствующих документов. Это может происходить, например, когда запрос приложения (для обработки кредитов, полисов страхования, выкупа акций и т. д.) одобряется или отклоняется в ходе рабочего процесса. Эти действия могут управляться программой (автоматически) или менеджером (вручную). В таком случае должно быть составлено письмо или создана таблица, показывающая состояние счета.
В номере «MSDN Magazine» за июнь 2008 г. я показывал, как делать это, используя объектные модели приложений Microsoft Office (msdn.microsoft.com/magazine/cc534981). Взяв ту статью за основу, на этот раз я покажу, как интегрировать документы, совместимые с Microsoft Office, с Windows Workflow Foundation (WF) без прямого взаимодействия с приложениями Office. Это достигается применением OpenXML SDK 2.0 для манипуляций документами. Хотя в Word и Excel есть своя модель документов OpenXML, они достаточно похожи, чтобы для интеграции рабочего процесса можно было использовать набор интерфейсных классов, скрывающих большую часть различий.
Так как WF входит в Microsoft .NET Framework 4 Client Profile, в любой системе с установленной .NET Framework 4 есть библиотека WF. И поскольку ее использование значительно упростилось в .NET Framework 4 по сравнению с .NET Framework 3.0, в любом приложении, которому требуются даже базовые WF-функции, стоит подумать о ее применении вместо собственного кода. Это распространяется даже на случаи, где встроенные операции нужно дополнить своими операциями.
Я продемонстрирую, как собственная операция может предоставлять данные на основе документа Office, который служит в качестве прототипа. Эта операция в свою очередь передает данные операциям для каждого типа документов, и они используют эти поля данных для заполнения целевых документов Office. Я применял Visual Studio 2010 для разработки классов, которые поддерживают такие операции, как перечисление именованных полей, извлечение их содержимого и заполнение документов из прототипов. Все эти классы используют OpenXML SDK вместо прямого манипулирования объектными моделями приложений Office. Я создал операции рабочего процесса для поддержки ввода данных и заполнения документов текстового процессора и электронных таблиц. Законченные документы отображаются простым запуском приложения по умолчанию для документа данного типа. Весь код написан на Visual Basic.
Общая архитектура
К любой архитектуре для интеграции рабочего процесса и Office предъявляются три общих требования: получение данных в рабочем процессе, обработка данных для создания или обновления документов Office и сохранение или передача выходных документов. Для поддержки этих требований, я создал набор классов, которые предоставляют унифицированные интерфейсы для нижележащих форматов документов Office, используя OpenXML SDK. Эти классы содержат методы для:
- получения имен потенциальных целевых полей в документах. Я буду использовать для их описания обобщенный термин «поля». В случае Word это закладки (bookmarks), а в Excel — именованные диапазоны. В самом общем случае как закладки, так и именованные диапазоны могут быть весьма сложными, но я буду рассматривать простой случай отдельного места для закладок и отдельных ячеек для именованных диапазонов. Закладки всегда содержат текст, тогда как ячейки электронной таблицы могут содержать текст, числа или даты;
- заполнения целевых полей в документе, принимая входные данные и сравнивая их с данными в документе.
Чтобы реализовать операции, заполняющие документы Office, я прибегнул к модели CodeActivity из WF 4. Эта модель значительно упростилась со времен WF 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. Пытаться заранее предопределить имена полей в документе нереалистично, так как для этого нужно было бы сопоставить собственную операцию с конкретным документом.
Если вы посмотрите на то, как аргументы передаются в операции, то заметите, что они передаются в виде Dictionary(Of String, Object). Чтобы заполнить поля в документе Office, нужны две части информации: имя поля и вставляемое значение. Я и раньше разрабатывал приложения с использованием рабочих процессов и Office и придерживался универсальной стратегии, которая всегда хорошо работала: я перечислял именованные поля в документе и соотносил их с входными параметрами по именам. Если поля документа соответствуют шаблону в базовом словаре входных параметров (String, Object), их можно передать напрямую. Однако этот подход слишком жестко связывает рабочий процесс с документом.
Вместо именования переменных в соответствии с полями в документе я решил использовать обобщенный Dictionary(Of String, String) для передачи имен полей. Этот параметр я назвал Fields и использовал его в каждой операции. Каждый элемент в таком словаре имеет тип KeyValuePair(Of String, String). Ключ обеспечивает поиск имени поля, а значение записывается в поле. Затем этот словарь Fields становится одним из параметров в рабочем процессе.
Вы можете начать рабочий процесс всего несколькими строками кода в простом окне Windows Presentation Foundation (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 содержит путь ко входному документу, используемому как прототип. Однако в коде также предусматривается применение параметра Field, чье имя — InputDocument, если он содержит путь к типу документа, подходящего для целевого приложения Office. Дополнительный параметр позволяет именовать целевую операцию во входном поле TargetActivity. Это дает возможность нескольким операциям в последовательности, например, оценивать пригодность полей в оригинальном входном документе.
Структура стандартного рабочего процесса требует глубокого знания имен переменных, сопоставленных с операциями.
У любого реального рабочего процесса есть источник входных данных. В демонстрационных целях я использовал операцию DataEntry, которая может извлекать свой ввод (поля и значения по умолчанию) из любых поддерживаемых типов документов Office. Эта операция открывает диалоговое окно, содержащее DataGrid и кнопки для выбора документа и сохранения полей данных. После того как пользователь выбирает документ в качестве прототипа, DataGrid заполняется доступными полями из документа, а также в этот элемент управления добавляются поля InputDocument, OutputDocument и TargetActivity, как показано на рис. 2. (Кстати, Джули Лерман в своей статье «Composing WPF DataGrid Column Templates for a Better User Experience» за апрель 2011 г., которую можно найти по ссылке msdn.microsoft.com/magazine/gg983481, дала важный совет по использованию FocusManager для поддержки редактирования в такой сетке, как на рис. 2, одним щелчком кнопки мыши.)
Рис. 2. Интерфейс операции Data Entry
Обработка данных в документах
Как уже отмечалось, каждый тип документов Office по-своему предоставляет набор именованных полей. Каждая операция пишется для поддержки конкретного типа документов, но все операции, поддерживающие те или иные типы документов Office, следуют одинаковому шаблону. Если предоставляется свойство InputDocument, оно используется как путь к документу. Когда оно равно null, операция ищет в свойстве Fields значение InputDocument и, если оно обнаруживается, проверяет, содержится ли в нем путь с суффиксом, совпадающим с обрабатываемым этой операцией типом документов. Операция также пытается найти документ, добавляя подходящий суффикс. Если эти условия выполняются, свойству InputDocument присваивается это значение и обработка продолжается.
Каждый совпадающий элемент из набора Fields используется для заполнения соответствующего поля в выходном документе. Последний либо передается в переменной OutputDocument рабочего процесса, либо отыскивается в наборе Fields как элемент OutputDocument KeyValuePair. В любом случае, если у выходного документа нет суффикса, добавляется подходящий суффикс по умолчанию. Потенциально это позволяет использовать одно значение для создания документов нескольких типов или даже нескольких документов разных типов.
Выходной документ будет храниться по указанному пути. На практике в большинстве сред рабочих процессов это будет общий сетевой ресурс или папка SharePoint. Для простоты я использовал в коде локальный путь. Кроме того, код для операций Word и Excel проверяет входной документ на соответствующий тип. В демонстрационном рабочем процессе входным документом по умолчанию считается прототип, выбранный в качестве основы для Fields. Пользователь может изменить это, чтобы Fields, производный от документа Word, можно было задействовать для определения входных данных для документа Excel, и наоборот. Код, отвечающий за заполнение документа Word, показан на рис. 3.
Рис. 3. Заполнение документа Word в операции
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")
' Проверяем, подходит ли эта операция
' для обработки входного документа
InputFields.TryGetValue(("TargetActivity"), _
TargetActivityName)
' Если мишень не указана явно, проверяем тип документа
If String.IsNullOrEmpty(TargetActivityName) Then
If Not (InputDocumentName.EndsWith(".docx") _
Or InputDocumentName.EndsWith(".docm")) _
Then Exit Sub
' Если это Target Activity,
' изменяем имя документа нужным образом
ElseIf TargetActivityName = Me.DisplayName Then
If Not (InputDocumentName.EndsWith(".docx") _
Or InputDocumentName.EndsWith(".docm")) _
Then InputDocumentName &= ".docx"
End If
Else
Exit Sub
End If
' Это целевая операция или цель явно не указана
' и входным является документ Word
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()
' Отображаем полученный документ
System.Diagnostics.Process.Start(OutputDocumentName)
End Sub
Обратите внимание на рис. 3 на следующую строку:
Dim InputWordInterface = _
New WordInterface(InputDocumentName, InputFields))
Именно здесь конструируется экземпляр класса WordInterface. Ему передаются путь к документу-прототипу, а также данные полей. Все это сохраняется в соответствующих свойствах для использования методами класса.
Класс WordInterface предоставляет функцию, которая заполняет целевой документ. Входной документ используется как прототип, из которого в памяти создается копия нижележащего документа OpenXML. Это важный этап: именно копия в памяти заполняется, а затем сохраняется как выходной файл. Создание копии в памяти одинаково для каждого типа документов и обрабатывается в базовом классе OfficeInterface. Однако процедура сохранения выходного файла более специфична для каждого типа документов. На рис. 4 показано, как документ Word заполняется классом WordInterface.
Рис. 4. Применение класса WordInterface для заполнения документа Word
Public Overrides Function FillInDocument() As Boolean
Dim Status As Boolean = False
' Присваиваем ссылку на существующее тело документа
Dim DocumentBody As Body = _
WordDocument.MainDocumentPart.Document.Body
GetBuiltInDocumentProperties()
Dim BookMarks As Dictionary(Of String, String) = _
Me.GetFieldNames(DocumentBody)
' Определяем используемые переменные словаря на основе
' закладок в документе, подходящих элементам Fields
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
' Обрабатываем особый случай (случаи)
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. Если в документе есть поле с таким именем, я сцепляю содержимое входных полей Title, FirstName и LastName для его заполнения. Соответствующая логика находится в функции GetFullName. Поскольку все типы документов Office предъявляют схожие требования, эта функция помещена в базовый класс OfficeInterface наряду с несколькими общими свойствами. Чтобы создать точку расширения для подобных случаев я использовал выражение Select Case. Например, вы могли бы добавить поле FullAddress, которое объединяет содержимое входных полей Address1, Address2, City, State, ZipCode и Country.
Сохранение выходных документов
Каждый из классов операций имеет свойство OutputDocument, значение которого можно присваивать несколькими способами. В дизайнере свойство можно связать с параметром или константой уровня рабочего процесса. В период выполнения каждая операция будет искать в ее свойстве OutputDocument путь сохранения своего документа. Если это свойство не задано, операция будет искать в своем наборе Fields ключ с именем 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 решает, какую операцию Office следует вызвать. Принимая это решение, она использует простое выражение:
Right(Fields("InputDocument"), 4)
В случаях docm и docx документ направляется в Word, а в случаях xlsx и xlsm — в Excel.
Для описания действия каждой операции я воспользуюсь рис. 5, но операции на рис. 6 работают аналогично.
Ввод данных
В верхней части рис. 5 вы видите экземпляр класса DataEntryActivity. Он представляет WPF-окно с элементом управления DataGrid, который заполняется извлечением именованных полей из InputDocument (в данном случае входной документ выбирается пользователем). Этот элемент управления позволяет выбрать документ независимо от того, был ли он предоставлен изначально. После этого пользователь может ввести или изменить значения в полях. Отдельный класс ObservableCollection обеспечивает необходимую двухстороннюю привязку с DataGrid, как показано на рис. 7.
Рис. 7. ObservableCollection для отображения полей
Imports System.Collections.ObjectModel
' Наблюдаемый набор Fields для отображения в WPF
Public Class WorkflowFields
Inherits ObservableCollection(Of WorkFlowField)
' Создаем ObservableCollection из входного словаря
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 — Word (.docx, .docm) или Excel (.xlsx, .xlsm). Чтобы извлечь поля из документа, вызывается соответствующий класс OfficeInterface. Он загружает целевой документ как OpenXML-объект и перечисляет поля (и их содержимое, если оно есть). Поддерживаются и документы, содержащие макросы, которые в таком случае переносятся в выходной документ.
Одно из полей, предоставляемых операцией DataEntryActivity, — TargetActivity. Это просто имя операции, чье свойство Fields нужно заполнить полями, собранными из входного документа. Поле TargetActivity используется другими операциями, чтобы определять, надо ли обрабатывать поля данных.
WordFormActivity
WordFormActivity работает с документом Word. Она сопоставляет элементы Fields с любыми одноименными закладками в документе Word и вставляет значение поля в закладку Word, если обнаружено совпадение имен. Эта операция принимает документы Word как с макросами (.docm), так и без них (.docx).
ExcelFormActivity
ExcelFormActivity работает с документом Excel. Она сопоставляет элементы Fields с любыми простыми именованными диапазонами в документе Excel, если обнаруживает совпадение имен. Затем вставляет значение поля в именованный диапазон Excel. Эта операция принимает документы Excel как с макросами (.xlsm), так и без них (.xlsx).
Типы документов Excel обладают дополнительной специфической функциональностью, которую нужно поддерживать, если заполняемые данные требуется корректно форматировать и обрабатывать. Один из видов такой функциональности — множество неявных форматов даты и времени. К счастью, они хорошо документированы (см. ECMA-376 Part 1, 18.8.30 по ссылке bit.ly/fUUJ). Встречая ECMA-формат, нужно транслировать его в соответствующий .NET-формат. Например, ECMA-формат mm-dd-yy становится M/dd/yyy.
Кроме того, в электронные таблицы заложена концепция общих строк (shared strings), и при вставке значения в ячейку, содержащую общую строку, требуется особая обработка. Используемый ниже метод InsertSharedStringItem наследуется от одного из содержащихся в OpenXML SDK:
If TargetCell.DataType.HasValue Then
Select Case TargetCell.DataType.Value
' Случай обработки типа данных SharedString
Case CellValues.SharedString
' Вставляем текст в SharedStringTablePart
Dim index As Integer = InsertSharedStringItem(NewValue, _
SpreadSheetDocument)
TargetCell.CellValue.Text = index.ToString
Status = True
End Select
End If
Завершающие штрихи
Пример рабочего процесса просто объявляет о своем завершении. Операция, выбранная по типу документа или полю Target Activity, создает свой выходной документ в заданном месте. Отсюда он может быть взят другими операциями для дополнительной обработки. В демонстрационных целях каждая из операций завершается тем, что отображает выходной документ, полагаясь на Windows в его открытии в соответствующем приложении:
System.Diagnostics.Process.Start(OutputDocumentName)
Если вам нужно распечатать этот документ, используйте следующий код:
Dim StartInfo As New System.Diagnostics.ProcessStartInfo( _
OutputDocumentName) StartInfo.Verb = "print"
System.Diagnostics.Process.Start(StartInfo)
Поскольку мы работаем с единственным форматом документа, приложению на основе рабочего процесса не требуется знать версию установленного пакета Office!
В производственной среде, как правило, требуется больше работы. Могут создаваться записи в базе данных, а документы в конечном счете — передаваться по электронной почте или распечатываться и отправляться клиенту. Производственные приложения на основе рабочих процессов также скорее всего использовали бы возможности других сервисов, например сохранения данных и отслеживания изменений.
Заключение
Я обрисовал базовый архитектурный подход к взаимодействию Window Workflow Foundation 4 с документами Office, используя OpenXML SDK. Пример рабочего процесса иллюстрирует этот подход и показывает возможную реализацию для создания выходных документов Office на основе данных в рабочем процессе. Классы, на базе которых построено это решение, можно модифицировать и расширять под ваши потребности. Хотя операции рабочего процесса написаны так, чтобы задействовать преимущества WF 4 и .NET Framework 4, интерфейсные библиотеки Office также совместимы с .NET Framework 3.5.