Este artigo foi traduzido por máquina.

Microsoft Office

Integrando o Windows Workflow Foundation ao OpenXML SDK

Rick Spiewak

Baixar o código de exemplo

Processos comerciais que envolvem um fluxo de trabalho muitas vezes requerem o correspondente documentos serão criados ou transformados. Isso pode ocorrer, por exemplo, quando um aplicativo (para um empréstimo, uma apólice de seguro, resgate de ações e assim por diante) foi aprovado ou rejeitado durante o processo de fluxo de trabalho. Isso pode ser conduzido por um programa (automaticamente) ou por um underwriter (como uma etapa manual). Nesse caso, uma carta talvez precise ser escrito, ou uma planilha mostrando saldos produzido.

Na edição de Junho de 2008 da MSDN Magazine, mostrei como fazer isso usando os modelos de objeto de aplicativo do Microsoft Office (msdn.microsoft.com/magazine/cc534981). Com base nesse artigo, desta vez mostrarei como integrar documentos Microsoft Office compatível com o Windows Workflow Foundation (WF) sem a necessidade de interagir diretamente com aplicativos do Office. Isto é conseguido usando o OpenXML SDK 2.0 para manipular processamento de texto e tipos de documento. Os aplicativos do Office correspondentes são, naturalmente, Word e Excel. Enquanto cada um deles tem seu próprio modelo de documento OpenXML, há bastante semelhanças para permitir o uso de um conjunto de classes de interface que ocultar a maior parte das diferenças de integração do fluxo de trabalho.

Porque o WF está incluído no Microsoft.NET Framework 4 Client Profile, qualquer.NET Framework 4 instalação incluirá biblioteca do WF. E porque seu uso foi bastante simplificado, na.NET Framework 4, em comparação com o.NET Framework 3.0, qualquer aplicativo que requer funções de fluxo de trabalho até mesmo básico deve considerar usar isso no lugar do código personalizado. Isso se aplica mesmo para casos onde as atividades internas devem ser complementadas por atividades personalizadas.

Eu vou mostrar como uma atividade personalizada pode dar entrada de dados com base em um documento do Office que serve como um protótipo. A atividade de entrada de dados, por sua vez, transmite dados para actividades para cada tipo de documento, e as atividades usam esses campos de dados para o preenchimento de documentos do Office de destino. Eu usei o Visual Studio 2010 para desenvolver classes para oferecer suporte a operações como enumerar campos nomeados, extrair seu conteúdo e preenchimento de documentos a partir de protótipos. Essas classes todos usam o OpenXML SDK em vez de diretamente manipular modelos de objeto do aplicativo Office. Eu criei atividades fluxo de trabalho para dar suporte a entrada de dados e preenchendo os tipos de documentos de processamento de texto e planilha. Documentos acabados são exibidos por simplesmente chamar o aplicativo padrão para o tipo de documento. Eu escrevi o código usando o Visual Basic.

Design geral

Qualquer design para integrar o fluxo de trabalho e o Office possui três requisitos gerais: obtenção de dados no fluxo de trabalho; processamento de dados para criar ou atualizar documentos do Office; armazenar ou transmitir os documentos de saída. Para oferecer suporte a essas necessidades, eu criei um conjunto de classes que fornecem interfaces uniformes para os formatos de documento do Office subjacentes usando o OpenXML SDK. Essas classes fornecem métodos para:

  • Obter os nomes dos campos de destino potenciais nos documentos. Vou usar o termo genérico "campo" para descrever estas. No caso da palavra, estes são marcadores; O Excel usa intervalos nomeados. No caso mais geral, indicadores e intervalos nomeados podem ser bastante complexos, mas vou usar o caso simples de um único local para marcadores e células únicas para intervalos nomeados. Marcadores sempre contêm texto, Considerando que células de planilha podem conter texto, números ou datas.
  • Preencha campos de destino um documento aceitando entrada de dados e os dados para o documento de correspondência.

Ao implementar atividades para preencher os documentos do Office, usei o modelo WF 4 CodeActivity. Este modelo é bastante simplificado sobre WF 3.0 e fornece uma implementação muito mais clara. Por exemplo, já não é necessário declarar explicitamente as propriedades de dependência.

O elenco de apoio

Por trás do fluxo de trabalho representa um conjunto de classes projetadas para oferecer suporte as funções do OpenXML SDK. As classes de desempenhar as funções principais de Carregando o documento de protótipo em um fluxo de memória, encontrando e preenchendo os campos (marcadores ou intervalos nomeados) e salvando o documento de saída resultante. Funções comuns são coletadas em uma classe base, incluindo carregando e salvando o fluxo de memória. Eu omiti a verificação de erros aqui para maior clareza, mas ele está incluído no download de código que acompanha. Figura 1 mostra como carregar e salvar um documento OpenXML.

Figura 1 carregando e salvando um documento 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

Obtendo dados no fluxo de trabalho

Um dos primeiros desafios que enfrentei para integrar documentos do Office em fluxos de trabalho foi como passar dados para campos nesses documentos. A estrutura de fluxo de trabalho padrão se baseia no conhecimento prévio dos nomes das variáveis associadas a actividades. Variáveis podem ser definidas com escopos diferentes para fornecer visibilidade, o fluxo de trabalho e em outras atividades. Eu decidi aplicar esse modelo diretamente era demasiado rígida, como ele necessário amarrar o design de fluxo de trabalho geral demasiado firmemente para os campos do documento. No caso de integração com o Office, a atividade de fluxo de trabalho está atuando como um proxy para um documento do Office. Não é realista tentar predeterminar os nomes dos campos no documento, já que isso exigiria uma atividade personalizada para um determinado documento de correspondência.

Se você olhar a maneira como argumentos são passados para atividades, você encontrará que são passados como um Dictionary (Of String, Object). Para preencher os campos em um documento do Office, você precisa de duas partes de informação: o nome do campo e o valor para inserir. Eu desenvolvi aplicativos usando produtos de fluxo de trabalho e Office antes, e a estratégia geral eu adoptado funcionou bem: eu enumerar os campos nomeados no documento e ajustá-las aos parâmetros de entrada pelo nome. Se os campos do documento corresponderem ao padrão no dicionário básico de parâmetro de entrada (String, Object), eles podem ser passados diretamente. No entanto, essa abordagem casais o fluxo de trabalho demasiado firmemente ao documento.

Em vez de nomes variáveis para corresponder a campos no documento, decidi usar um Dictionary genérico (Of String, String) para transmitir esses nomes de campo. Chamei este parâmetro campos e usou-o em cada uma das actividades. Cada entrada em um dicionário de é do tipo KeyValuePair (Of String, String). A chave é usada para corresponder ao nome de domínio; o valor é usado para preencher o conteúdo do campo. Esse dicionário Fields é então um dos parâmetros dentro do fluxo de trabalho.

Você pode iniciar o fluxo de trabalho com apenas algumas linhas de código em uma janela de Windows Presentation Foundation (WPF) simples e menos ainda quando adicionado a um aplicativo existente:

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

Eu queria que as atividades geralmente útil e ter mais do que uma estratégia disponível para fornecer o documento de entrada. Para permitir isso, as atividades têm um parâmetro comum chamado InputDocument. Estes podem ser anexadas a variáveis que, por sua vez, estão conectados às saídas de outras atividades conforme as necessidades de ditar o fluxo de trabalho. O parâmetro contém o caminho para um documento de entrada usado como um protótipo. No entanto, o código também prevê usando um parâmetro de campo cujo nome é InputDocument se ele contém um caminho para um tipo de documento apropriado para o aplicativo do Office de destino. Um parâmetro adicional permite que a atividade de destino a ser nomeado em um campo de entrada chamado TargetActivity. Isso permite várias atividades em uma seqüência; por exemplo, para avaliar os campos no documento de entrada original para a aplicabilidade.

Qualquer fluxo de trabalho real terá uma origem de dados de entrada. Para fins de demonstração, eu usei uma atividade DataEntry, que pode desenhar sua entrada (campos e valores padrão) de qualquer um dos tipos de documento com suporte do Office. Esta atividade é aberta uma caixa de diálogo que contém um DataGrid e os botões para seleccionar um documento e salvar os campos de dados. Após o usuário seleciona um documento como um protótipo, o DataGrid é preenchido com os campos disponíveis do documento, além de campos InputDocument, OutputDocument e TargetActivity, conforme mostrado na Figura 2. (Coluna pontos de dados de abril de 2011 do aliás, de Julie Lerman, "Compor WPF DataGrid coluna modelos para uma melhor experiência de usuário," que você encontrará em msdn.microsoft.com/magazine/gg983481, tem uma dica importante sobre o uso de FocusManager para permitir que um único clique de edição em uma grade, como a mostrada na Figura 2.)

The Data Entry Activity Interface
Figura 2 A Interface de atividade de entrada de dados

Processamento de dados em documentos

Como observei anteriormente, cada um dos tipos de documento do Office tem sua própria maneira de fornecer uma coleção de campos nomeados. Cada uma das atividades é escrita para suportar um tipo específico de documento, mas as atividades que oferecem suporte os tipos de documento do Office todos seguem o mesmo padrão. Se a propriedade InputDocument for fornecida, ele é usado como o caminho para o documento. Se a propriedade InputDocument for nula, a atividade irá procurar na propriedade Fields para um InputDocument valor que, se encontrado, é examinado para ver se ele contém um caminho com um sufixo correspondente documento digite as alças de atividade. A atividade também tenta localizar um documento acrescentando um sufixo apropriado. Se essas condições forem atendidas, a propriedade InputDocument será definida para esse valor e processamento continua.

Cada entrada correspondente da coleção Fields é usada para preencher o campo correspondente no documento de saída. Isso é passado como a variável fluxo de trabalho correspondente (OutputDocument) ou for encontrado na coleção Fields como a entrada de OutputDocument KeyValuePair. Em cada caso, se o documento de saída não tem nenhum sufixo, é acrescentado um sufixo padrão apropriado. Isso permite que o mesmo valor ser usadas para criar diferentes tipos de documentos, ou até mesmo vários documentos de diferentes tipos.

O documento de saída será armazenado no caminho especificado. Na maioria dos ambientes de fluxo de trabalho do mundo real, isso seria um compartilhamento de rede ou uma pasta do SharePoint. Para simplificar, usei um caminho local no código. Além disso, o código para as actividades de Word e Excel verifica o documento de entrada para o tipo correspondente. No fluxo de trabalho de demonstração, o documento de entrada padrão é o protótipo selecionado como base para os campos. O usuário pode alterá-la, para que campos derivado de uma palavra documento pode ser usado para definir entradas para um documento do Excel, ou vice-versa. Figura 3 mostra o código para preencher um documento do Word.

Figura 3 preenchendo um documento de processamento de texto na atividade

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

Em Figura 3, em especial observe a seguinte linha:

Dim InputWordInterface = _
  New WordInterface(InputDocumentName, InputFields))

Isto é onde a instância de classe WordInterface é construída. Passou o caminho para o documento para usar como um protótipo, juntamente com os dados do campo. Eles simplesmente são armazenados nas propriedades correspondentes para uso pelos métodos da classe.

A classe WordInterface fornece a função que preenche o documento de destino. O documento de entrada é usado como um protótipo, do qual uma cópia na memória do documento OpenXML subjacente é criada. Este é um passo importante — a cópia na memória é o que tem preenchido e, em seguida, salva como arquivo de saída. Criação de cópia de memória é o mesmo para cada tipo de documento e é tratada na classe base OfficeInterface. No entanto, salvar o arquivo de saída é mais específica para cada tipo. Figura 4 mostra como o documento do Word é preenchido pela classe WordInterface.

Figura 4 usando a classe WordInterface para preencher no Word documento

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

Adicionei um nome de campo caso especial chamado FullName. Se o documento contém um campo com esse nome, eu concatenar campos de entrada chamados Title, FirstName e LastName para preenchê-lo. A lógica para isso é uma função chamada GetFullName. Porque todos os tipos de documento do Office têm necessidades semelhantes, esta função é na classe base OfficeInterface juntamente com algumas outras propriedades comuns. Eu usei uma declaração Select Case para tornar este um ponto de extensibilidade. Por exemplo, você poderia adicione um campo de FullAddress que concatena os campos de entrada chamados Address1, Endereço2, cidade, estado, código postal e país.

Armazenar os documentos de saída

Cada uma das classes de atividade tem uma propriedade OutputDocument, que pode ser definida por vários meios. Dentro do designer, uma propriedade pode ser ligado a um parâmetro de nível de fluxo de trabalho ou para um valor constante. Em tempo de execução, cada uma das atividades irá procurar em sua propriedade OutputDocument o caminho salvar seu documento. Se isso não estiver definido, ele ficará dentro da sua coleção de campos para uma chave chamada OutputDocument. Se esse valor é encerrada com um sufixo apropriado, é usado diretamente. Se ele não tem um sufixo apropriado, um é adicionado. A activity salva então o documento de saída. Isso permite uma flexibilidade máxima para decidir onde colocar o caminho no documento de saída. Como o sufixo é omitido, o mesmo valor pode ser usado por qualquer um dos tipos de actividade. Aqui é como um documento do Word é salvo, primeiro assegurar que o fluxo de memória é atualizado e, em seguida, usando o método na classe base:

Public Sub SaveDocument(ByVal document As WordprocessingDocument)
  document.MainDocumentPart.Document.Save()
  MyBase.SaveDocument()
End Sub

Fluxos de trabalho de amostra

Figura 5 mostra o fluxo de trabalho simple, vou usar para mostrar como funciona a integração. Um segundo exemplo usa um fluxograma, mostrado na Figura 6. Vou passar por cada um dos tipos de actividade, por sua vez e falar sobre o que fazer, mas primeiro vamos ver o que faz cada fluxo de trabalho.

Simple Workflow with Office Integration
Figura 5 fluxo de trabalho simples com integração com o Office

Flowchart Workflow with Office Integration
Figura 6 fluxograma Workflow com integração com o Office

O fluxo de trabalho consiste em uma seqüência simple que chama cada tipo de atividade, por sua vez. Porque as actividades de Word e Excel verificar os tipos de documento de entrada, eles não irá tentar processar o tipo errado.

O fluxo de trabalho de fluxograma no Figura 6 usa uma atividade Switch para decidir qual atividade do Office deve ser chamada. Ele usa uma expressão simples para fazer essa determinação:

Right(Fields("InputDocument"), 4)

Casos docm e docx são direcionadas para o Word, enquanto xlsx e xlsm são direcionadas para o Excel.

Vou usar Figura 5 para descrever as ações de cada atividade, mas a ação em Figura 6 é semelhante.

Entrada de dados

Na parte superior do Figura 5, você pode ver uma instância da classe DataEntryActivity. O DataEntryActivity apresenta uma janela WPF com um Controlarar de DataGrid, que é preenchido por extrair os campos nomeados de InputDocument, que neste caso é selecionada pelo usuário. O controle permite que o usuário selecione um documento, independentemente de saber se um foi fornecido inicialmente. O usuário pode, em seguida, insira ou modifique os valores nos campos. Uma classe separada de ObservableCollection é fornecida para habilitar a ligação de TwoWay necessária para o DataGrid, como mostrado na Figura 7.

Figura 7 ObservableCollection para exibir campos

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

O InputDocument pode ser tanto do Office com suporte tipos, Word (. docx ou. docm) ou Excel (. xlsx ou. xlsm) de documentos. Para extrair os campos do documento, a classe OfficeInterface apropriada é chamada. Ela carrega o documento de destino como um objeto de OpenXML e enumera os campos (e seu conteúdo,) se estiver presente. São suportados os formatos de documentos que contêm macros e as macros são transportadas para o documento de saída.

Um dos campos fornecidos pelo DataEntryActivity é o campo TargetActivity. Este é apenas o nome de uma atividade cuja propriedade Fields é preenchida com os campos coletados do documento de entrada. O campo TargetActivity é usado por outras actividades como uma maneira de determinar se deve processar os campos de dados.

WordFormActivity

O WordFormActivity opera em um documento de processamento de texto (Word). Ela corresponde as entradas de campos para todos os indicadores no documento do Word com os mesmos nomes. Ele, em seguida, insere o valor do campo do indicador do Word. Esta actividade irá aceitar documentos de Word (. docm) ou sem macros (. docx).

ExcelFormActivity

O ExcelFormActivity opera em um documento de planilha (Excel). Ela corresponde as entradas de campos para qualquer simples intervalos nomeados no documento Excel com os mesmos nomes. Ele, em seguida, insere o valor do campo do Excel intervalo nomeado. Esta actividade irá aceitar documentos do Excel (. xlsm) ou sem macros (. xlsx).

Tipos de documento do Excel tem algumas características especiais adicionais que exigem Manipulação especial se cheia nos dados estão a ser formatado e tratados corretamente. Um desses recursos é a variedade de formatos de hora e data implícita. Felizmente, estes são bem documentados (ver ECMA-376 parte 1, 18.8.30 na bit.ly/fUUJ). Quando um formato ECMA é encontrado, ele precisa ser traduzido para o correspondente.Formato líquido. Por exemplo, torna-se o ECMA formato mm-dd-yy M/dd/yyy.

Além disso, folhas de cálculo têm um conceito de seqüências de caracteres compartilhadas e manipulação especial é necessária ao inserir um valor em uma célula que conterá uma seqüência de caracteres compartilhada. O método de InsertSharedStringItem usado aqui é derivado daquela contida no 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

Toques finais

O fluxo de trabalho de exemplo simplesmente anuncia sua própria conclusão. A atividade selecionada, por tipo de documento ou o campo TargetActivity, cria o documento de saída no local especificado. Aqui ela pode ser selecionada por outras atividades para um processamento adicional. Para fins de demonstração, cada uma das actividades termina com o lançamento do documento de saída, baseando-se no Windows para abri-lo no aplicativo apropriado:

System.Diagnostics.Process.Start(OutputDocumentName)

Se você quiser imprimir em vez disso, você pode usar o seguinte:

Dim StartInfo As New System.Diagnostics.ProcessStartInfo( _
  OutputDocumentName) StartInfo.Verb = "print"
System.Diagnostics.Process.Start(StartInfo)

Como estamos trabalhando com apenas o formato do documento, não há ainda nenhuma necessidade para o aplicativo de fluxo de trabalho para estar ciente da versão do Office instalada!

Em um ambiente de produção real, mais trabalho seria seguir normalmente. Entradas de banco de dados podem ser feitas, e documentos podem acabar sendo roteados por email ou impresso e enviado para um cliente. Aplicativos de fluxo de trabalho de produção, também poderão tirar proveito de outros serviços, tais como persistência e controle.

Resumo

Já descrevi uma abordagem de projeto básico para a interface janela Workflow Foundation 4 com documentos do Office usando o OpenXML SDK. O fluxo de trabalho de exemplo ilustra essa abordagem e mostra como ele pode ser implementado em uma maneira que personaliza a documentos do Office usando dados no fluxo de trabalho. As classes de que esta solução está construída são facilmente modificável e extensível para atender a uma ampla variedade de necessidades semelhantes. Embora as atividades de fluxo de trabalho são gravadas para tirar proveito do WF 4 e o.NET Framework 4, as bibliotecas de interface do Office também são compatíveis com o.NET Framework 3.5.

Rick Spiewak é um engenheiro de sistemas de software de chumbo com A MITRE Corp Ele trabalha com os EUA. Ar força Electronic Systems Center em planejamento de missão. Spiewak tem trabalhado com Visual Basic desde 1993 e a Microsoft.NET Framework desde 2002, quando ele era um testador beta para o Visual Studio.NET 2003. Ele tem longa experiência integrando aplicativos do Office com uma variedade de ferramentas fluxo de trabalho.

Graças ao seguinte especialista técnico para revisão deste artigo: Andrew Coates