Compartir a través de


Este artículo proviene de un motor de traducción automática.

Microsoft Office

Integrar Windows Workflow Foundation con OpenXML SDK

Rick Spiewak

Descargar el ejemplo de código

Procesos de negocios que involucran un flujo de trabajo a menudo requieren la correspondiente documentos creados o procesados.Esto puede ocurrir, por ejemplo, cuando una aplicación (para un préstamo, una póliza de seguro, redención de acciones etc.) ha sido aprobada o rechazado durante el proceso de flujo de trabajo.Esto podría ser impulsado por un programa (automáticamente) o por un asegurador (como un paso manual).En ese caso, una carta que deba ser escrito o una hoja de cálculo mostrando los saldos producida.

En la edición de junio de 2008 de MSDN Magazine, mostré cómo hacerlo mediante los modelos de objetos de aplicación de Microsoft Office (msdn.microsoft.com/magazine/cc534981).Mediante ese artículo como base, esta vez te mostraré cómo integrar documentos de Microsoft Office-compatible con Windows Workflow Foundation (WF) sin necesidad de interactuar directamente con las aplicaciones de Office.Esto se logra mediante el OpenXML SDK 2.0 para manipular procesamiento de texto y los tipos de documento de hoja de cálculo.Las aplicaciones de Office correspondientes son, por supuesto, Word y Excel.Mientras que cada una de ellas tiene su propio modelo de documento de OpenXML, hay suficientes similitudes para permitir el uso de un conjunto de clases de interfaz que ocultar la mayoría de las diferencias con el propósito de integración de flujo de trabajo.

Porque WF se incluye en Microsoft.NET Framework 4 Client Profile, cualquier.NET Framework 4 instalación incluirá la biblioteca WF.Y porque su uso se ha simplificado considerablemente en el.NET Framework 4 en comparación con el.NET Framework 3.0, cualquier aplicación que requiere funciones de flujo de trabajo básico aún debe considerar el uso de este en lugar de código personalizado.Esto se aplica incluso en casos donde las actividades integradas deben complementarse con actividades personalizadas.

Voy a mostrar cómo una actividad personalizada puede proporcionar la entrada de datos basada en un documento de Office que sirve como un prototipo.La actividad de entrada de datos, a su vez, pasa los datos a las actividades para cada tipo de documento, y las actividades de utilizan estos campos de datos para rellenar los documentos de Oficina de destino.He utilizado Visual Studio 2010 para desarrollar clases para apoyar las operaciones como la enumeración de los campos nombre, relleno en documentos de prototipos y extraer su contenido.Estas clases todos utilizan el SDK de OpenXML en lugar de manipular directamente los modelos de objetos de aplicación de Office.He creado las actividades de flujo de trabajo para apoyar la entrada de datos y rellenar los tipos de documentos de procesamiento de textos y hojas de cálculo.Se muestran los documentos acabados invocando simplemente la aplicación predeterminada para el tipo de documento.Escribí el código utilizando Visual Basic.

Diseño general

Cualquier diseño para la integración de flujo de trabajo y de la Oficina cuenta con tres requisitos generales: la obtención de datos en el flujo de trabajo, el procesamiento de los datos para crear o actualizar los documentos de Office, el almacenamiento o la transmisión de los documentos de salida.Para apoyar estas necesidades, he creado un conjunto de clases que proporcionan interfaces uniformes a los formatos de documento subyacentes de Office usando el SDK de OpenXML.Estas clases proporcionan métodos para:

  • Obtener los nombres de los posibles campos de destino en los documentos.Usaré el término genérico de "campo" para describir estos.En el caso de la palabra, son marcadores; Excel utiliza rangos con nombre.En el caso más general, los marcadores y rangos con nombre pueden ser bastante complejos, pero usaré el caso simple de una única ubicación para los marcadores y celdas individuales para rangos con nombre.Marcadores siempre contienen texto, mientras que las celdas de la hoja de cálculo pueden contener texto, números o fechas.
  • Rellenar campos de destino en un documento por aceptar datos de entrada y que coinciden con los datos del documento.

Llevar a cabo actividades para rellenar los documentos de Office, he utilizado el modelo WF 4 CodeActivity.Este modelo se ha simplificado considerablemente sobre WF 3.0 y proporciona una implementación mucho más clara.Por ejemplo, ya no es necesario declarar explícitamente las propiedades de dependencia.

El reparto

Detrás del flujo de trabajo se levanta un conjunto de clases diseñadas para apoyar las funciones de SDK de OpenXML.Las clases las funciones clave de cargar el documento prototipo en una secuencia de memoria, encontrar y rellenar los campos (marcadores o rangos con nombre) y guardar el documento resultante de la salida.Funciones comunes se recogen en una clase base, incluyendo cargar y guardar la secuencia de memoria.He omitido de comprobación de errores aquí para mayor claridad, pero está incluido en la descarga de código que lo acompaña.Figura 1 muestra cómo cargar y guardar un documento OpenXML.

Figura 1 cargar y guardar un 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

Inclusión de datos en el flujo de trabajo

Uno de los primeros desafíos que enfrentan en la integración de documentos de Office en flujos de trabajo fue cómo pasar datos destinados a los campos de estos documentos. La estructura de flujo de trabajo estándar se basa en el conocimiento anticipado de los nombres de las variables asociadas con las actividades. Las variables pueden definirse con distintos ámbitos para proporcionar visibilidad en el flujo de trabajo y en otras actividades. Decidí que era demasiado rígida, aplicando este modelo directamente como es necesario vincular el diseño de flujo de trabajo general demasiado estrechamente a los campos del documento. En el caso de integración de Office, la actividad de flujo de trabajo actúa como un proxy para un documento de Office. No es realista para intentar predeterminar los nombres de los campos en el documento, ya que esto requeriría que coinciden con una actividad personalizada para un documento determinado.

Si usted mira la manera de argumentos se pasan a las actividades, encontrará pasaron como un diccionario de cadena (objeto). Para rellenar los campos en un documento de Office, se necesitan dos piezas de información: el nombre del campo y el valor que se inserta. He desarrollado aplicaciones que utilizan los productos de flujo de trabajo y de la Oficina antes, y la estrategia general que adoptó funcionado bien: me enumerar los campos de nombre en el documento y hacerlas coincidir con los parámetros de entrada por su nombre. Si los campos del documento coincide con el patrón en el Diccionario básico parámetro de entrada (String, Object), puede pasar en directamente. Sin embargo, este enfoque parejas el flujo de trabajo demasiado estrechamente al documento.

En lugar de nombres variables para que se correspondan con los campos en el documento, decidí utilizar un diccionario genérico (de String, String) para transmitir estos nombres de campo. He nombrado a este parámetro campos y utilizados en cada una de las actividades. Cada entrada en un diccionario de esos es de tipo KeyValuePair (de String, String). La clave se utiliza para que coincida con el nombre del campo; el valor se utiliza para rellenar el contenido del campo. Este diccionario de campos es uno de los parámetros dentro del flujo de trabajo.

Puede iniciar el flujo de trabajo con unas pocas líneas de código en una sencilla ventana de Windows Presentation Foundation (WPF) y menos aún cuando se agregan a una aplicación 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

Quería que las actividades que suele ser útil y tener más de una estrategia para proporcionar el documento de entrada. Para permitir esto, las actividades tienen un parámetro común denominado InputDocument. Estos pueden asociarse a las variables que, a su vez, están conectadas a productos de otras actividades como las necesidades de los dictados de flujo de trabajo. El parámetro contiene la ruta de acceso a un documento de entrada utilizado como un prototipo. Sin embargo, también proporciona el código para utilizar un parámetro de campo cuyo nombre es InputDocument si contiene una ruta de acceso a un tipo de documento adecuado para la aplicación de la Oficina de destino. Un parámetro adicional permite la actividad de destino ser nombrado en un campo de entrada llamado TargetActivity. Esto permite que múltiples actividades en una secuencia; por ejemplo, para evaluar los campos en el documento original de entrada de aplicabilidad.

Cualquier flujo de trabajo real tendrá un origen de datos de entrada. Para fines de demostración, usé una actividad EntradaDeDatos, que puede dibujar su entrada (los campos y valores por defecto) desde cualquiera de los tipos de documento de Office compatibles. Esta actividad abre un cuadro de diálogo que contiene un control DataGrid y botones para seleccionar un documento y guardar los campos de datos. Después de que el usuario selecciona un documento como un prototipo, el DataGrid se rellena con los campos disponibles en el documento, además de los campos InputDocument, OutputDocument y TargetActivity, como se muestra en figura 2. (Por cierto, de Julie Lerman puntos de datos de abril de 2011 columna, "Componer WPF DataGrid columna plantillas para una mejor experiencia de usuario," que encontrará en msdn.microsoft.com/magazine/gg983481, tiene una sugerencia importante sobre el uso de FocusManager para habilitar la edición haga clic en una cuadrícula como la de figura 2.)

The Data Entry Activity Interface
Figura 2 la interfaz de actividad de entrada de datos

Procesamiento de los datos en los documentos

Como señalé anteriormente, cada uno de los tipos de documento de Office tiene su propia forma de proporcionar un conjunto de campos con nombre. Cada una de las actividades está escrita para apoyar un tipo de documento determinado, pero las actividades compatibles con los tipos de documento de Office todos siguen el mismo patrón. Si se proporciona la propiedad InputDocument, se utiliza como ruta de acceso al documento. Si la propiedad InputDocument es null, la actividad busca en la propiedad Fields un InputDocument valor que, si se encuentra, es examinado para ver si contiene una ruta con un sufijo coincidente el documento tipo gestiona la actividad. La actividad también intenta encontrar un documento añadiendo un sufijo adecuado. Si se cumplen estas condiciones, la propiedad InputDocument se establece en este valor y continúa el procesamiento.

Cada entrada coincidente de la colección de campos se utiliza para rellenar el campo correspondiente en el documento de salida. Esto tampoco se pasa como la variable de flujo de trabajo correspondiente (OutputDocument), o se encuentra en la colección de campos como la entrada OutputDocument KeyValuePair. En cada caso, si el documento de salida no tiene ningún sufijo, se anexa un sufijo predeterminado correspondiente. Esto permite que el mismo valor a utilizarse potencialmente para crear distintos tipos de documentos, o incluso varios documentos de diferentes tipos.

El documento de salida se almacenará en la ruta de acceso especificada. En la mayoría de los entornos de flujo de trabajo reales, esto sería un recurso compartido de red o una carpeta de SharePoint. Para mayor simplicidad, usé una ruta local en el código. Asimismo, el código para las actividades de Word y Excel comprueba el documento de entrada para el tipo correspondiente. En el flujo de trabajo de demostración, el documento de entrada por defecto el prototipo seleccionado como base para los campos. El usuario puede cambiar esto, para que los campos se derivan una palabra documento puede utilizarse para definir entradas para un documento de Excel, o viceversa. Figura 3 muestra el código para rellenar un documento de Word.

Figura 3 rellenar un documento de procesamiento de texto en la actividad

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

En figura 3, tenga en cuenta en particular la siguiente línea:

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

Esto es donde se construye la instancia de la clase WordInterface. Ha pasado la ruta de acceso al documento para utilizar como un prototipo, junto con los datos del campo. Simplemente se almacenan en las propiedades correspondientes para su uso por los métodos de la clase.

La clase WordInterface proporciona la función que se rellena el documento de destino. El documento de entrada se utiliza como un prototipo, desde el cual se crea una copia en memoria del documento OpenXML subyacente. Este es un paso importante: la copia en memoria es la que ha rellenado y, a continuación, guardar como el archivo de salida. Creación de la copia en memoria es el mismo para cada tipo de documento y se maneja en la clase base de OfficeInterface. Sin embargo, guardar el archivo de salida es más específico para cada tipo. Figura 4 muestra cómo se rellena el documento de Word la clase WordInterface.

Figura 4 utilizando la clase WordInterface para llenar, en una palabra, 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

He añadido un nombre de campo caso especial llamado FullName. Si el documento contiene un campo con este nombre, concatenar campos de entrada llamados título, FirstName y LastName para rellenarlo. La lógica de esto es una función llamada GetFullName. Porque todos los tipos de documento de Office tienen necesidades similares, esta función es de la clase base de OfficeInterface junto con algunas otras propiedades comunes. He utilizado una instrucción Select Case para hacer de este un punto de extensibilidad. Por ejemplo, podría agregar un campo de FullAddress que concatena los campos de entrada llamados Dirección1, destinatario2, ciudad, Estado, código postal y país.

Almacenar los documentos de salida

Cada una de las clases de actividad tiene una propiedad OutputDocument, que puede definirse por varios medios. Dentro del diseñador, se puede enlazar una propiedad a un parámetro de nivel de flujo de trabajo o a un valor constante. En tiempo de ejecución, cada una de las actividades se verá en su propiedad de OutputDocument para la ruta para guardar el documento. Si no está definido, se verá dentro de su colección de campos de una clave denominada OutputDocument. Si este valor se termina con un sufijo adecuado, se utiliza directamente. Si no tiene un sufijo adecuado, se agrega. La actividad, a continuación, guarda el documento de salida. Esto permite una flexibilidad máxima al decidir dónde colocar la ruta de acceso al documento de salida. Porque se omite el sufijo, puede utilizarse el mismo valor por cualquiera de los tipos de actividad. Aquí es cómo se guarda un documento de Word, primero asegurando que la secuencia de memoria se actualiza y, a continuación, utilizando el método de la clase base:

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

Muestra los flujos de trabajo

Figura 5 muestra el flujo de trabajo sencilla va a utilizar para mostrar cómo funciona la integración. Un segundo ejemplo utiliza un diagrama de flujo se muestra en la figura 6. Voy a ir a su vez a través de cada uno de los tipos de actividad y hablar sobre lo que hacen, pero primero vamos a ver qué hace cada flujo de trabajo.

Simple Workflow with Office Integration
Figura 5 el flujo de trabajo sencilla con integración con Office

Flowchart Workflow with Office Integration
Figura 6 diagrama de flujo de trabajo con integración con Office

El flujo de trabajo consta de una secuencia simple que invoca a su vez cada tipo de actividad. Debido a las actividades de Word y Excel comprobación los tipos de documento de entrada, no intentan procesar el tipo equivocado.

El flujo de trabajo de diagrama de flujo de figura 6 utiliza una actividad de Switch para decidir qué actividad Oficina debe invocarse. Utiliza una expresión simple para hacer esta determinación:

Right(Fields("InputDocument"), 4)

El docm de casos y docx están dirigidos a la palabra, mientras xlsx y xlsm se dirigen a Excel.

Usaré figura 5 para describir las acciones de cada actividad, pero la acción de figura 6 es similar.

Entrada de datos

En la parte superior de la figura 5, se puede ver una instancia de la clase DataEntryActivity. El DataEntryActivity presenta una ventana WPF con un control DataGrid, que está poblado por extraer los campos nombre de la InputDocument, que en este caso es seleccionado por el usuario. El control permite al usuario seleccionar un documento, independientemente de si uno estaba inicialmente previsto. El usuario puede introducir o modificar los valores de los campos. Una clase ObservableCollection independiente se proporciona para permitir el enlace TwoWay requerido para el control DataGrid, como se muestra en figura 7.

Figura 7 ObservableCollection para mostrar 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

El InputDocument puede ser cualquiera de la Oficina compatible tipos, Word (.docx o .docm) o Excel (.xlsx o .xlsm) del documento. Para extraer los campos del documento, se llama a la clase OfficeInterface apropiada. Carga el documento de destino como un objeto de OpenXML y enumera los campos (y su contenido) si está presente. Se admiten los formatos de documentos que contienen macros y las macros son prorrogadas para el documento de salida.

Uno de los campos proporcionados por el DataEntryActivity es el campo de TargetActivity. Esto es sólo el nombre de una actividad cuya propiedad de campos es a poblarse con los campos recogidos en el documento de entrada. El campo TargetActivity es utilizado por otras actividades como una manera de determinar si se debe procesar los campos de datos.

WordFormActivity

El WordFormActivity funciona en un documento (Word) procesamiento de textos. Coincide con las entradas de los campos a los marcadores del documento de Word con los mismos nombres. A continuación, inserta el valor del campo en el marcador de Word. Esta actividad va a aceptar documentos de Word con (.docm) o sin macros (.docx).

ExcelFormActivity

El ExcelFormActivity funciona en un documento de hoja de cálculo (Excel). Coincide con las entradas de campos a cualquier simples rangos con nombre en el documento de Excel con los mismos nombres. A continuación, inserta el valor del campo en el rango con nombre de Excel. Esta actividad va a aceptar documentos de Excel con (.xlsm) o sin macros (.xlsx).

Tipos de documento de Excel tienen algunas características especiales adicionales que requieren un manejo especial si llena de datos formateado y manejan correctamente. Una de estas características es la variedad de implícita formatos de fecha y hora. Afortunadamente, estos son bien documentados (véase ECMA-376 parte 1, 18.8.30 en bit.ly/fUUJ). Cuando se encuentra un formato ECMA, tiene que ser traducido a la correspondiente.Formato de NET. Por ejemplo, se convierte en la ECMA formato mm-dd-aa dd/M/yyy.

Además, hojas de cálculo tienen un concepto de cadenas compartidas y manejo especial es necesaria cuando se inserta un valor en una celda que contendrá una cadena compartida. El método InsertSharedStringItem utilizado aquí se deriva de una figura de 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 finales

El flujo de trabajo de ejemplo simplemente anuncia su propia conclusión. La actividad que se ha seleccionado, por tipo de documento o por el campo TargetActivity, crea su documento de salida en la ubicación especificada. Desde aquí se puede ser recogido por otras actividades de procesamiento adicional. Para fines de demostración, cada una de las actividades termina lanzando el documento de salida, depender de Windows para abrir en la aplicación correspondiente:

System.Diagnostics.Process.Start(OutputDocumentName)

Si desea imprimir en su lugar, puede utilizar lo siguiente:

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

Porque estamos trabajando con sólo el formato del documento, todavía no hay ninguna necesidad de la aplicación de flujo de trabajo estar al tanto de la versión de Office instalada!

En un entorno de producción real, más trabajo normalmente seguiría. Las entradas de la base de datos pueden hacerse, y documentos podrían acabar se enrutan a través de correo electrónico o impresión y enviaron a un cliente. Las aplicaciones de flujo de trabajo de producción también sería probables tomar ventaja de otros servicios, como la persistencia y seguimiento.

En resumen

Yo he esbozado un enfoque de diseño básico para interconexión ventana Workflow Foundation 4 con documentos de Office usando el SDK de OpenXML. El flujo de trabajo de ejemplo ilustra este enfoque y muestra cómo se puede implementar de una manera que personaliza documentos de Office utilizando datos del flujo de trabajo. Las clases desde el cual se construye esta solución son fácilmente modificable y ampliable para satisfacer una amplia variedad de necesidades similares. Aunque las actividades de flujo de trabajo se escriben para tomar ventaja de WF 4 y la.NET Framework 4, las bibliotecas de interfaz de Office también son compatibles con el.NET Framework 3.5.

Rick Spiewak es un ingeniero de sistemas de software de plomo con el MITRE Corp. Trabaja con los Estados Unidos. Aire fuerza Centro electrónico de sistemas de planificación de misión. Spiewak ha trabajado con Visual Basic desde 1993 y Microsoft.NET Framework desde 2002, cuando tenía un beta tester para Visual Studio.NET 2003. Tiene larga experiencia en integración de aplicaciones de oficina con una variedad de herramientas de flujo de trabajo.

Gracias al siguiente experto técnico por su ayuda en la revisión de este artículo: Andrew Coates