MSDN Magazine > Home > Issues > 2008 > Июнь >  Заполнитель форм. Сборка потоков работ для сбор...
Заполнитель форм
Сборка потоков работ для сбора данных и создания документов
Рик Спивак (Rick Spiewak)

В этой статье рассматриваются следующие вопросы.
  • Сборка собственных действий
  • Взаимодействие с Microsoft Office
  • Передача данных действиям потока работ
  • Извлечение данных потока работ в документы Office
В данной статье использованы следующие технологии:
Windows Workflow Foundation, выпуск 2007 системы Office, Visual Basic
Поток работ описывает метод автоматизации бизнес-процессов. Поток работ может быть использован для обработки запросов клиентов или требований о выплате страхового возмещения, для выкупа долей в паевых фондах и так далее. Потоки работ могут запускаться при прибытии документа на электронную почту, получении запроса от веб-узла или вызове клиентом центра технической поддержки. Вслед за этим автоматически либо путем действий пользователя могут быть назначены задачи, которые следует выполнить.
В прошлом попытки предоставить автоматизацию такого рода обычно основывались на монолитных системах потоков работ, вокруг которых строился бизнес-процесс. Windows® Workflow Foundation (WF) предоставляет подход на основе компонентов, который позволяет создавать потоки работ для поддержки бизнес-процесса, а не наоборот.
Бизнес-процессы, запрашивающие использование потока работ, часто требуют употребления или создания относящихся к процессу документов. Это требование может возникнуть, например, когда приложение (скажем, для займа или выкупа акций) было одобрено или отвергнуто в ходе потока работ. Это может произойти после проверки программой (автоматической) или гарантом размещения (ручной). Может потребоваться написание письма или создание таблицы, показывающей баланс.
В этой статье я расскажу о методиках интеграции приложений Microsoft® Office с WF. Я поговорю об использовании форм InfoPath® и других документов Office для сбора данных, о передаче данных целевым действиям и о том, как использовать эти поля данных для принятия решений, а также создания и заполнения новых документов Office.
Я использовал Visual Studio® 2008, чтобы протестировать ряд этих вариантов интеграции WF и Office, включая перечисление поименованных полей, извлечение их содержимого и заполнение документов из шаблонов. Я был в состоянии написать собственные действия потока работ для поддержки ввода данных, вводя различные типы документов и отображая законченные документы. Я также добавил поддержку конструктора для этих действий, так что их внешний вид отличается от других действий WF.

Общая конструкция
В любую конструкцию интеграции потока работ и Office входят три базовых компонента: подача данных в поток работ, использование данных для создания или обновления документов Office и возвращение получившихся документов. Для поддержки этих потребностей я создал набор интерфейсов, позволяющих поддерживать выполнение нескольких отдельных задач потоков работ на лежащих в основе документах и приложениях Office.
Первой из этих задач является получение имен полей данных в документах Office. Хотя для описания их я буду использовать общий термин «поле», в случае Microsoft Word это закладки; Microsoft Excel® использует именованные диапазоны, поля InfoPath появляются как узлы XML, а у PowerPoint® есть имена для форм. (До выпуска 2007 системы Office переименовывать формы внутри интерфейса пользователя было нельзя. В PowerPoint 2007 это можно сделать, открыв панель выделения.)
Далее следует задача заполнения этих полей данными. Я решил проделать это, приняв в качестве ввода Dictionary («Словарь») (Of String, String) и сопоставив содержимое словаря с полями в документе. Вдобавок, я применил интерфейс IDisposable, так чтобы получить возможность очищать объекты COM.
При применении действий для заполнения документов Office я использовал базовый класс, именуемый OfficeFormFill. Он поддерживает шаблон уничтожаемых объектов и имеет в своем составе общий конструктор, придающий действиям визуальное единообразие. Затем из этого базового класса я вывел мои собственные классы действий.
Поток работ был собран из специально созданных действий, именуемых WordFormFill, ExcelFormFill, PowerPointFormFill и DataEntryActivity. Функция DataEntryActivity состоит в простом предоставлении возможности введения связанных с потоками работ переменных, не ограниченных одним документом, в общую структуру словаря, используемую различными действиями OfficeFormFill. Она использует документ Office как шаблон для этой цели, перечисляя именованные поля и представляя их пользователю для заполнения. Шаблон может быть взят из любого поддерживаемого класса документов, вне зависимости от конечного целевого документа или документов, которые будут созданы или заполнены потоком работ.

Подача данных в поток работ
One of the first challenges I faced in integrating Office documents into workflows was how to surface data contained by an input document from within the workflow.Одной из первых проблем, с которыми я столкнулся при интеграции документов Office в потоки работ, было выведение на поверхность данных, содержащихся в вводимом документе, изнутри потока работ.Стандартная парадигма потока работ полагается на предварительное знание имен свойств, связанных с действиями. Свойства зависимости могут быть преобразованы, чтобы стать видимыми потоку работ и другим действиям. Я решил, что это слишком негибко, поскольку требует привязки общей конструкции потока работ к конкретным полям во вводимом документе. В случае интеграции Office поток работ служит универсальным прокси для любого документа Office. Пытаться заранее определить имена полей в документе не реалистично, поскольку это потребует специальных действий потока работ для разных типов документов.
Глядя на то, как аргументы передаются в потоки работ и, следовательно, в действия, можно заметить, что они передаются в виде универсального словаря Dictionary(Of String, Object). Среда исполнения WF затем привязывает элементы словаря к свойствам самого потока работ. Чтобы заполнить поля в документе Office, необходимы два фрагмента информации: имя поля и вводимое значение. В прошлом я взял на вооружение общую стратегию перечислении именованных полей в документе и сопоставления их со словарем параметров ввода WF по именам. Если поле документа совпадает с ключом в словаре, оно передается параметру ввода с тем же именем. Однако в данном случая я полагал, что этот способ обработки использовать не следует, поскольку для этого придется создавать на потоке работ свойства для каждого из потенциальных полей документа, которое может быть использовано.
Вместо наименования свойств в действии и преобразования их для соответствия полям в документе я решил использовать общий словарь Dictionary(Of String, String) для передачи последних. Я назвал этот параметр Fields («Поля») и использую его для каждого из действий потока работ. Ключ используется для сопоставления имени поля, значение – для заполнения содержимого поля. Словарь Fields затем передается как одна из записей в параметрах, передаваемых потоку работ. Так что реальные параметры, по крайней мере там, где это имеет значение для интеграции Office, расположены в Dictionary внутри Dictionary (see рис. 1).
Private WithEvents workflowRuntime As WorkflowRuntime = Nothing
Private workflowInstance As WorkflowInstance = Nothing
Private WithEvents waitHandle As New AutoResetEvent(False)
Public Sub main()
   Dim WorkflowType As Type = GetType(Workflow4)
   Dim Params As New Dictionary(Of String, Object)
   Dim Fields As New Dictionary(Of String, String)
   Params.Add("Fields", Fields)
   ' Start the workflow
   workflowRuntime = New WorkflowRuntime
   workflowInstance = _
      workflowRuntime.CreateWorkflow(WorkflowType, Params)
   workflowInstance.Start()
   waitHandle.WaitOne()
Я хотел добиться полезности действий потоков работ в широком спектре ситуаций и наличия более чем одной доступной стратегии для указания вводимого документа или шаблона. Чтобы добиться этого, все действия имеют общий параметр InputDocument. Это свойство зависимости, так что его можно преобразовать сообразно требованиям потока работ. Оно содержит путь ко вводимому документу или шаблону. Однако код также обеспечивает использование параметра Field («Поле»), имя которого совпадает с именем действия, если он содержит путь к документу или шаблону, подходящему для целевого приложения Office.
Любой реальный поток работ будет иметь источник входящих данных. В демонстрационных целях я использовал действие DataEntry, которое может вывести свой шаблон (поля и значения по умолчанию) из любого поддерживаемого типа документов Office. Оно отображает форму ввода, которая позволяет пользователю указать целевое действие. Это может быть определенное действие OfficeFormFill или составное действие. Мой пример – IfElseActivity. Коду необходима информация лишь об общем свойстве Fields, используемом всеми действиями OfficeFormFill.

Обработка данных
Как я отметил ранее, каждый из типов документов Office имеет собственную коллекцию именованных полей. Каждое из действий, являющееся производным от OfficeFormFill, было написано для поддержки определенного типа документов. Хотя сочетание функций, ссылающихся на несколько типов документов, определенно возможно (DataEntryActivity была примером этого), здесь есть одна загвоздка: если в коде сослаться на одну из первичных сборок средств взаимодействия (PIA) Office, а она будет отсутствовать на системе, где, в итоге, будет развернут код, можно получить исключение, даже если путь исполнения не использует конкретную сборку PIA. Например, оператор Select Case или If, который кажется обходящим отсутствующий компонент, все равно приведет к исключению. По этой причине и стоит изолировать вызовы к компонентам Office. Если вы решите создать составное действие, не забудьте поддержать эту изоляцию. И, само собой, всегда тестируйте свое приложение во всех возможных целевых средах.
Все действия, которые поддерживают каждый из типов документов Office, следуют одинаковому шаблону. Если предоставляется свойство InputDocument, оно используется как часть документа или шаблона. Если свойство InputDocument имеет значение null, то действие ищет в свойстве Fields ключ, совпадающий с именем действия. Если он найден, то он изучается, чтобы найти, содержит ли он путь с суффиксом, совпадающим с типом документа, который обрабатывается действием. Если эти условия выполнены, то свойство InputDocument устанавливается на данное значение.
Каждая совпадающая запись из коллекции Fields используется для заполнения соответствующего поля в документе. Результат помещается в исходящий документ. Все это либо передается как соответствующее свойство зависимости (OutputDocument), или обнаруживается в коллекции Fields как запись Output KeyValuePair. Во всех случаях, если у исходящего документа нет суффикса, то к нему добавляется соответствующий суффикс по умолчанию. Это позволяет, в принципе, использовать одно и то же значение для создания различных типов документов или даже нескольких документов различных типов.
Получившийся документ будет сохранен по указанному пути. В большинстве рабочих сред это будет общий сетевой ресурс или библиотека документов SharePoint®. Для простоты я использовал локальный путь.
Каждое из действий также имеет поле, именуемое Visible («Видимость»), чтобы, если это необходимо, показать документ как заполненный. На рис. 2 показан код для заполнения документа Word. Обратите, в частности, внимание на эту строку:
WordFiller = New WordInterface(InputDocument, Fields)
<STAThread()> Protected Overrides Function Execute _
  (ByVal executionContext As ActivityExecutionContext) _
  As ActivityExecutionStatus

  Dim Status As ActivityExecutionStatus

  ' Open the target document or template
  If String.IsNullOrEmpty(Me.InputDocument) Then
    If Me.Fields _
      IsNot Nothing AndAlso Me.Fields.ContainsKey(Me.Name) Then  
      ' Use a field named for *this* activity if it is a document 
      Dim NameValue As String = Fields(Me.Name)
      Select Case System.IO.Path.GetExtension(NameValue).ToLowerInvariant
        Case ".doc", ".docx", ".dot", ".dotx"
          InputDocument = NameValue
        Case Else
          Throw New ArgumentException( _ 
   "Input Document Invalid or Missing")
      End Select
    End If
  End If

  ' Create or open the required document from an input 
  ' document or template
  WordFiller = New WordInterface(InputDocument, Fields)

  ' Production workflow may not want to show the document
  If Me.Fields.ContainsKey("Visible") Then
    Boolean.TryParse(Me.Fields("Visible"), WordFiller.Visible)
  End If

  ' Get success or failure from the Word Interface class
  Dim Success As Boolean = WordFiller.FillInDocument()
  WordApp = WordFiller.WordApp

  If Success Then
    ' Find the target output document
    If String.IsNullOrEmpty(Me.OutputDocument) Then
      If Me.Fields.ContainsKey("Output") Then
        Dim NameValue As String = Fields("Output").ToString

        If NameValue.EndsWith(".doc", _
          StringComparison.CurrentCultureIgnoreCase) _
          OrElse NameValue.EndsWith(".docx", _
          StringComparison.CurrentCultureIgnoreCase) Then

          ' Set the document property to the provided name
          Me.OutputDocument = NameValue
        Else 'Force .doc suffix
          Me.OutputDocument = NameValue & ".doc"
        End If
      End If
    End If

    ' Save the output 
    WordApp.ActiveDocument.SaveAs(Me.OutputDocument)

    Cleanup()

    Status = ActivityExecutionStatus.Closed
    executionContext.CloseActivity()
  Else
    Cleanup()

    Status = ActivityExecutionStatus.Closed
    executionContext.CancelActivity(Me)
  End If

  Return Status
End Function
Именно здесь экземпляр класса WordInterface был собран и передал путь документу для использования в качестве шаблона вместе с данными полей. Они были просто сохранены в соответствующих свойствах для использования методами класса.
Класс WordInterface предоставляет функции для заполнения целевого документа (см. рис. 3). Если входящий документ является шаблоном, то создается новый экземпляр документа. Если это документ, то он открывается как документ только для чтения, чтобы предотвратить случайное его переписывание. Этот класс также наследует свое свойство Visible от базового класса. Отметьте, что приведенный здесь код, ради краткости, опускает некоторые проверки на предмет ошибок.
Public Overrides Function FillInDocument() As Boolean
  Dim Status As Boolean = False
  _WordApp = New Word.Application
  WordApp.Visible = Visible

  ' Check for template
  Select Case Path.GetExtension(Document).ToLowerInvariant
    Case ".dot", ".dotx"
      _WordDocument = WordApp.Documents.Add(Document)
    Case ".doc", ".docx"
      _WordDocument = WordApp.Documents.Open(FileName:=Document, _
        ReadOnly:=True)
  End Select

  ' Determine dictionary variables to use 
  ' based on bookmarks in the document matching Fields entries
  If WordDocument IsNot Nothing _
    AndAlso WordDocument.Bookmarks.Count > 0 Then

    Dim BookMark As Word.Bookmark = Nothing
    For i As Integer = 1 To WordDocument.Bookmarks.Count
      BookMark = WordDocument.Bookmarks(i)
      Dim BookMarkName As String = BookMark.Name
      Dim rng As Word.Range = BookMark.Range
      If Me.Fields.ContainsKey(BookMarkName) Then
        rng.Text = Fields(BookMarkName).ToString   
        'This results in the bookmark being lost, it needs to be replaced
        WordApp.ActiveDocument.Bookmarks.Add(BookMarkName, rng)
      Else
        ' Handle special case(s)
        Select Case BookMark.Name
          Case "FullName"
            rng.Text = GetFullName(Fields)
        End Select
      End If
    Next
      Status = True
  Else
    Status = False
  End If

  Return Status
End Function
Я добавил специальное имя поля, именуемое FullName. Если документ содержит поле с этим именем, я сцепляю поля ввода, именуемые Title, FirstName и LastName, чтобы заполнить его. Поскольку все документы Office имеют похожие потребности, эта функция была перенесена в базовый класс OfficeInterface вместе с некоторыми другими общими свойствами. Классы OfficeInterface также обрабатывают необходимую логику очистки, чтобы гарантировать правильное удаление объектов COM, созданных для управления документами Office.
Каждый из классов OfficeFormFill имеет свойство OutputDocument. Есть несколько способов установки этих свойств напрямую. Внутри конструктора свойство может быть привязано к параметру уровня потока работ (включая свойства, преобразованные из других действий) или быть постоянным значением. Во время выполнения каждый из типов OfficeFormFill будет просматривать свое свойство OutputDocument на предмет пути, по которому следует сохранить его документ. Если путь не установлен, он будет искать в коллекции Fields ключ, именуемый Output. Если значение заканчивается соответствующим суффиксом, оно используется как есть. Если у него нет соответствующего суффикса, то он добавляется. Затем Activity («Действие») сохраняет получившийся документ. Это допускает максимум гибкости при решении, где поместить путь к исходящему документу Опять же, поскольку суффикс пропущен, то же значение может быть использовано любым из типов OfficeFormFill для сохранения документа в верных соответствующих форматах. (В Word и PowerPoint добавленным мною суффиксом является версия режима совместимости. Excel предоставляет свойство Excel8CompatibilityMode, которое может быть использовано для определения типа документа, так что это можно решить напрямую.)

Образец потока работ
На рис. 4 показан образец потока работ, который я использую для демонстрации работы интеграции. Я переберу все действия по очереди и расскажу, что они делают.
Рис. 4 Поток работ создания документа (щелкните изображение, чтобы увеличить его)
Наверху EnterCustomerData является экземпляром класса DataEntryActivity. Он полагается на свойство DataEntryDocument, которое было в данном случае просто установлено во время проектирования (см. рис. 5). Это свойство также могло быть установлено программой, запустившей поток работ. DataEntryActivity представляет форму Windows Form с элементом управления DataGridView, который заполняется путем извлечения именованных полей из DataEntryDocument. Он также показывает путь к этому документу. Пользователь может добавлять или изменять значения в полях.
Рис. 5. Привязки для действия EnterCustomerData (щелкните изображение, чтобы увеличить его)
DataEntryDocument может быть любым из поддерживаемых Office типов документов. Для этого примера используется шаблон документа InfoPath. Чтобы извлечь поля из документа, используется соответствующий класс OfficeInterface. Он загружает целевой документ в соответствующее приложение и перечисляет поля (а также их содержимое, если есть). Я использовал флажок, установленный по умолчанию, чтобы исключить имена по умолчанию для форм в PowerPoint. Они имеют имена вроде TextBox 1. В коде в DataEntryActivityForm есть процедура для исключения имен полей, основанных на регулярном выражении, и она была использована для пропуска включения этих значений и имен в DataGridView.
Одним из полей, предоставленных DataEntryActivity, является TargetActivity. Это просто имя действия, чье свойство Fields будет заполнено полями, собранными из вводимого документа. DataEntryActivity затем может иметь своей целью любое действие, производное от OfficeFormFill, или любое составное действие CompositeActivity. В этой статье поле TargetActivity показано как заранее заполненное CustomerScenario.
Целевое действие передает данные из DataEntryActivity. Когда целью является составное действие, этот процесс может потребовать прохода вверх по дереву действий для обнаружения подходящего свойства Fields. Я создал класс WorkflowUtilities для помещения в него процедур для обнаружения действий по имени или по желаемому свойству.

Выбор направления
Действие CustomerScenario – это действие IfElseActivity (являющееся производным от CompositeActivity). Тип IfElseActivity исполняет каждую ветвь по очереди, ожидая возвращаемого значения True. После того, как ветвь возвращает True, другие ветви не исполняются.
Структура CompositeActivity поддерживается внутри файла расширяемого языка разметки приложений (XAML), именуемого Workflow4.xoml, а ее логика – в файле кода, именуемом Workflow4.xoml.vb. Первая часть действия CustomerScenario показана на рис. 6.
<IfElseActivity x:Name="CustomerScenario">
  <IfElseBranchActivity x:Name="ProcessApproval">
    <IfElseBranchActivity.Condition>
    <CodeCondition Condition="ifElseBranchActivity1Condition" />
    </IfElseBranchActivity.Condition>
    <CodeActivity x:Name="codeActivity1" ExecuteCode=
      "{ActivityBind Workflow4,Path=codeActivity1_ExecuteCode1}" />
    <ns1:WordFormFill Description="Fill in fields in a Word document"
      x:Name="WriteCustomerLetter" Fields="{x:Null}" 
      OutputDocument="{x:Null}" 
      InputDocument=" C:\MSDN Workflow\Templates\Customer Letter.doc" />
</IfElseBranchActivity>
Чтобы код, на котором основана ветвь, обработал это решение, возвращаемое значение устанавливается в свойстве Result («Результат») аргументов событий, передаваемых каждой ветви. Решение продолжать в определенной ветви принимается в коде на основе значения Status («Состояние»), передаваемого действию. Ветвь, помеченная ProcessApproval, ищет значение Approved («Одобрено»). Ветвь ProcessDisapproval ищет значение Disapproved («Не одобрено»). Все остальные значения обрабатываются ветвью ProcessUndecided. На рис. 7 показан код, исполненный в ветви ProcessApproval.
Public Sub ifElseBranchActivity1Condition(ByVal sender As Object, _
  ByVal e As ConditionalEventArgs)
  ' Look for status matching desired value for *this* branch
  If Me.Fields.ContainsKey("Status") AndAlso _
    Me.Fields("Status") = "Approved" Then

    ' Look for a WordFormFill activity as a child to *this* branch
    Dim target As Activity = _
      WorkflowUtilities.FindActivityByType( _
      GetType(WordFormActivity.WordFormFill), _
      (DirectCast(sender, Activity)))

    If target IsNot Nothing Then
      Dim TargetActivity As WordFormActivity.WordFormFill = _
        DirectCast(target, WordFormActivity.WordFormFill)
      TargetActivity.Fields = Me.Fields
      ' Set input document by using field named for target activity
      TargetActivity.Fields.Add(TargetActivity.Name, Me.InputDocument)
      e.Result = True

    Else
      e.Result = False
    End If

  Else
    e.Result = False
  End If

End Sub
Другие ветви похожи, но передают свои данные типу действий PowerPointFormFill или ExcelFormFill. В реальном бизнес-процессе они могут просто использовать различные шаблоны Word, чтобы проделывать операции вроде создания письма клиента вместо взаимодействия с различными типами документов Office.
У экземпляра WordFormFill, исполняемого в данной ветви, его вводимый (шаблонный) документ установлен во время проектирования, как показано на рис. 8. Обратите внимание, что поле Fields первоначально равно null. Это обусловлено тем, что оно установлено программно.
Рис. 8. Свойства WriteCustomerLetter (щелкните изображение, чтобы увеличить его)

Команда поддержки
Часть подхода, используемого в этом экземпляре потока работ, полагается на способность одного действия находить другое. Для этого требуется, чтобы действия должны быть дочерними действиями CompositeActivity, в состав которого входят основные типы потоков работ: последовательный, тьюрингов (конечноавтоматный) и управляемый правилами. Это значит, что между действиями любого нетривиального рабочего процесса будет наблюдаться этот тип отношений.
Класс WorkFlowUtilities предоставляет методы для поиска действия-наследника в дереве действий по его имени или типу. Он также предоставляет функции поиска действий-предков с определенным свойством выше по дереву.
Эти служебные методы поддерживают поиск целевых действий для передачи им параметров, а также поиск источника вводимых параметров. Так что, для примера, когда DataEntryActivity передается параметр TargetActivity в его коллекции Fields, он может найти его в любом месте дерева действий, начиная с собственного контейнера:
Dim EntryActivity As Activity = Nothing
EntryActivity = WorkflowUtilities.FindActivityByName( _
  TargetActivityName, Me.Parent)

Завершающие штрихи
Образец потока работ объявляет завершение действия. Действие, выбранное полем Status, создает готовый документ в указанной точке. Оттуда его могут подобрать другие действия для дальнейшей обработки.
Я очертил базовый подход к разработке взаимодействия потока работ с клиентскими приложениями Office. Придерживаясь принципов объектно-ориентированного проектирования, можно создавать пригодные к повторному использованию действия потоков работ и вспомогательные классы, удовлетворяющие широкий спектр похожих потребностей.

Рик Спивак (Rick Spiewak) – старший разработчик программных систем в корпорации The MITRE Corporation. Он работает с Центром электронных систем управления полетами ВВС США. Он работал с Visual Basic начиная с 1993 года и с Microsoft .NET Framework начиная с 2002 года. Он также принадлежал к числу бета-тестеров Visual Studio .NET 2003.

Page view tracker