Compartilhar via


{1>A introdução de um desenvolvedor ao Windows Workflow Foundation (WF) no .NET 4<1}

Matt Milner

Novembro de 2009

Atualizado para lançamento: abril de 2010

Visão geral

Como os desenvolvedores de software sabem, escrever aplicativos pode ser desafiador e estamos constantemente procurando ferramentas e estruturas para simplificar o processo e nos ajudar a nos concentrar nos desafios de negócios que estamos tentando resolver.  Passamos de escrever código em linguagens de máquina, como assembler, para linguagens de nível superior, como C# e Visual Basic, que facilitam nosso desenvolvimento, removem preocupações de nível inferior, como gerenciamento de memória, e aumentam nossa produtividade como desenvolvedores.  Para desenvolvedores da Microsoft, a mudança para o .NET permite que o CLR (Common Language Runtime) aloque memória, limpe objetos desnecessários e manipule constructos de baixo nível, como ponteiros. 

Grande parte da complexidade de um aplicativo reside na lógica e no processamento que se passa nos bastidores.  Problemas como execução assíncrona ou paralela e geralmente coordenar as tarefas para responder a solicitações de usuário ou solicitações de serviço podem levar rapidamente os desenvolvedores de aplicativos de volta para a codificação de baixo nível de identificadores, retornos de chamada, sincronização etc. Como desenvolvedores, precisamos do mesmo poder e flexibilidade de um modelo de programação declarativa para os internos de um aplicativo como temos para a interface do usuário em Windows Presentation Foundation (WPF). O WF (Windows Workflow Foundation) fornece a estrutura declarativa para criar lógica de aplicativo e serviço e fornece aos desenvolvedores uma linguagem de nível mais alto para lidar com tarefas assíncronas, paralelas e outros processamentos complexos.

Ter um runtime para gerenciar memória e objetos nos liberou para nos concentrarmos mais nos aspectos comerciais importantes da escrita de código.  Da mesma forma, ter um runtime que possa gerenciar as complexidades da coordenação do trabalho assíncrono fornece um conjunto de recursos que melhora a produtividade do desenvolvedor.  O WF é um conjunto de ferramentas para declarar seu fluxo de trabalho (sua lógica de negócios), atividades para ajudar a definir a lógica e o fluxo de controle e um runtime para executar a definição de aplicativo resultante.  Em suma, o WF trata-se de usar uma linguagem de nível mais alto para escrever aplicativos, com o objetivo de tornar os desenvolvedores mais produtivos, aplicativos mais fáceis de gerenciar e mudar mais rapidamente para implementar.  O runtime do WF não só executa seus fluxos de trabalho para você, como também fornece serviços e recursos importantes ao escrever a lógica do aplicativo, como persistência de estado, indicadores e retomada da lógica de negócios, o que leva a agilidade de threads e processos, permitindo escalar verticalmente e escalar horizontalmente os processos de negócios. 

Para obter mais informações conceituais sobre como usar o WF para criar seus aplicativos pode torná-lo mais produtivo, recomendo que você leia "The Workflow Way" de David Chappell, encontrado na seção Recursos Adicionais. 

Novidades no WF4

Na versão 4 do Microsoft® .NET Framework, o Windows Workflow Foundation apresenta uma quantidade significativa de alterações em relação às versões anteriores da tecnologia que foram enviadas como parte do .NET 3.0 e 3.5.  Na verdade, a equipe revisitou o núcleo do modelo de programação, do runtime e das ferramentas e reprotetou cada um deles para aumentar o desempenho e a produtividade, bem como para lidar com os comentários importantes obtidos dos compromissos do cliente usando as versões anteriores.  As alterações significativas feitas foram necessárias para fornecer a melhor experiência para os desenvolvedores que adotam o WF e permitir que o WF continue a ser um componente fundamental forte que você pode criar em seus aplicativos. Apresentarei as mudanças de alto nível aqui, e ao longo do artigo cada tópico terá um tratamento mais aprofundado. 

Antes de continuar, é importante entender que a compatibilidade com versões anteriores também era uma meta fundamental nesta versão.  Os novos componentes da estrutura são encontrados principalmente nos assemblies System.Activities.* enquanto os componentes de estrutura compatíveis com versões anteriores são encontrados nos assemblies System.Workflow.*.  Os assemblies System.Workflow.* fazem parte do .NET Framework 4 e fornecem compatibilidade com versões anteriores completas para que você possa migrar seu aplicativo para o .NET 4 sem alterações no código do fluxo de trabalho. Ao longo deste artigo, usarei o nome WF4 para fazer referência aos novos componentes encontrados nos assemblies System.Activities.* e WF3 para fazer referência aos componentes encontrados nos assemblies System.Workflow.*. 

Designers

Uma das áreas de melhoria mais visíveis está no designer de fluxo de trabalho. A usabilidade e o desempenho foram as principais metas para a equipe para a versão do VS 2010.  O designer agora dá suporte à capacidade de trabalhar com fluxos de trabalho muito maiores sem uma degradação no desempenho e os designers são todos baseados em Windows Presentation Foundation (WPF), aproveitando ao máximo a experiência avançada do usuário que se pode criar com a estrutura declarativa da interface do usuário.  Os desenvolvedores de atividades usarão XAML para definir a aparência de suas atividades e interagir com os usuários em um ambiente de design visual.  Além disso, hospedar novamente o designer de fluxo de trabalho em seus próprios aplicativos para permitir que não desenvolvedores exibam e interajam com seus fluxos de trabalho agora é muito mais fácil. 

Fluxo de Dados

No WF3, o fluxo de dados em um fluxo de trabalho era opaco.  O WF4 fornece um modelo claro e conciso para fluxo de dados e escopo no uso de argumentos e variáveis.  Esses conceitos, familiares para todos os desenvolvedores, simplificam a definição de armazenamento de dados, bem como o fluxo dos dados para dentro e para fora de fluxos de trabalho e atividades.  O modelo de fluxo de dados também torna mais óbvias as entradas e saídas esperadas de uma determinada atividade e melhora o desempenho do runtime à medida que os dados são gerenciados com mais facilidade. 

Fluxograma

Uma nova atividade de fluxo de controle chamada Fluxograma foi adicionada para permitir que os desenvolvedores usem o modelo de Fluxograma para definir um fluxo de trabalho.  O Fluxograma se assemelha mais aos conceitos e processos de pensamento que muitos analistas e desenvolvedores passam ao criar soluções ou projetar processos de negócios.  Portanto, fazia sentido fornecer uma atividade para facilitar a modelagem do pensamento conceitual e do planejamento que já havia sido feito.  O Fluxograma permite conceitos como retornar às etapas anteriores e dividir a lógica com base em uma única condição ou uma lógica Switch/Case. 

Modelo de Programação

O modelo de programação do WF foi renovado para torná-lo mais simples e robusto.  A atividade é o tipo base principal no modelo de programação e representa fluxos de trabalho e atividades.  Além disso, você não precisa mais criar um WorkflowRuntime para invocar um fluxo de trabalho, basta criar uma instância e executá-la, simplificando o teste de unidade e cenários de aplicativo em que você não deseja passar pelo problema de configurar um ambiente específico.  Por fim, o modelo de programação de fluxo de trabalho torna-se uma composição totalmente declarativa de atividades, sem código ao lado, simplificando a criação de fluxo de trabalho.

Integração do WCF (Windows Communication Foundation)

Os benefícios do WF certamente se aplicam à criação de serviços e ao consumo ou à coordenação de interações de serviço.  Um grande esforço foi para aprimorar a integração entre o WCF e o WF.  Novas atividades de mensagens, correlação de mensagens e suporte de hospedagem aprimorado, juntamente com a definição de serviço totalmente declarativa são as principais áreas de melhoria. 

Introdução com Fluxo de Trabalho

A melhor maneira de entender o WF é começar a usá-lo e aplicar os conceitos.  Abordarei vários conceitos fundamentais em relação aos fundamentos do fluxo de trabalho e, em seguida, percorrerei a criação de alguns fluxos de trabalho simples para ilustrar como esses conceitos se relacionam entre si. 

Estrutura de fluxo de trabalho

As atividades são os blocos de construção do WF e todas as atividades derivam da Atividade.  Uma nota de terminologia – As atividades são uma unidade de trabalho no WF. As atividades podem ser compostas juntas em atividades maiores. Quando uma Atividade é usada como um ponto de entrada de nível superior, ela é chamada de "Fluxo de Trabalho", assim como Main é simplesmente outra função que representa um ponto de entrada de nível superior para programas CLR.   Por exemplo, a Figura 1 mostra um fluxo de trabalho simples sendo criado no código. 

Sequência s = nova sequência

{

    Atividades = {

        new WriteLine {Text = "Hello"},

        new Sequence {

            Atividades =

            {

                new WriteLine {Text = "Workflow"},

                new WriteLine {Text = "World"}

            }

        }

    }

};

Figura 1: Um fluxo de trabalho simples

Observe na Figura 1 que a atividade Sequência é usada como a atividade raiz para definir o estilo de fluxo de controle raiz para o fluxo de trabalho. Qualquer atividade pode ser usada como raiz ou fluxo de trabalho e executada, até mesmo um WriteLine simples.   Ao definir a propriedade Activities na Sequência com uma coleção de outras atividades, defini a estrutura do fluxo de trabalho.  Além disso, as atividades filho podem ter atividades filho, criando uma árvore de atividades que compõem a definição geral do fluxo de trabalho. 

Modelos de fluxo de trabalho e o fluxo de trabalho Designer

O WF4 é fornecido com muitas atividades e o Visual Studio 2010 inclui um modelo para definir atividades. As duas atividades mais comuns usadas para o fluxo de controle raiz são Sequência e Fluxograma.  Embora qualquer Atividade possa ser executada como um fluxo de trabalho, esses dois fornecem os padrões de design mais comuns para definir a lógica de negócios.   A Figura 2 mostra um exemplo de um modelo de fluxo de trabalho sequencial que define o processamento de um pedido que está sendo recebido, salvo e, em seguida, notificações enviadas a outros serviços.   

 

Figura 2: Design de fluxo de trabalho sequencial

O tipo de fluxo de trabalho Flowchart está sendo introduzido no WF4 para atender a solicitações comuns de usuários existentes, como poder retornar às etapas anteriores em um fluxo de trabalho e porque ele se assemelha mais ao design conceitual feito por analistas e desenvolvedores que trabalham na definição da lógica de negócios.  Por exemplo, considere um cenário que envolva a entrada do usuário em seu aplicativo.  Em resposta aos dados fornecidos pelo usuário, seu programa deve continuar no processo ou retornar a uma etapa anterior para solicitar a entrada novamente. Com um fluxo de trabalho sequencial, isso envolveria algo semelhante ao mostrado na Figura 3, em que uma atividade DoWhile é usada para continuar o processamento até que alguma condição seja atendida. 

Figura 3: Sequencial para ramificação de decisão

O design na Figura 3 funciona, mas como desenvolvedor ou analista examinando o fluxo de trabalho, o modelo não representa a lógica original ou os requisitos descritos.   O fluxo de trabalho fluxograma na Figura 4 fornece um resultado técnico semelhante ao modelo sequencial usado na Figura 3, mas o design do fluxo de trabalho corresponde mais de perto ao pensamento e aos requisitos conforme definido originalmente. 

Figura 4: Fluxo de trabalho de fluxograma

Fluxo de dados em fluxos de trabalho

O primeiro pensamento que a maioria das pessoas tem quando pensa no fluxo de trabalho é o processo empresarial ou o fluxo do aplicativo.  No entanto, tão crítico quanto o fluxo são os dados que impulsionam o processo e as informações coletadas e armazenadas durante a execução do fluxo de trabalho.  No WF4, o armazenamento e o gerenciamento de dados tem sido uma área privilegiada de consideração de design. 

Há três conceitos main para entender em relação aos dados: Variáveis, Argumentos e Expressões.  As definições simples para cada uma são que as variáveis são para armazenar dados, argumentos são para passar dados e expressões são para manipular dados. 

Variáveis – armazenamento de dados

Variáveis em fluxos de trabalho são muito parecidas com as variáveis que você está acostumado em linguagens imperativas: elas descrevem um local nomeado para que os dados sejam armazenados e seguem determinadas regras de escopo.  Para criar uma variável, primeiro você determina em qual escopo a variável precisa estar disponível.  Assim como você pode ter variáveis no código que estão disponíveis no nível da classe ou do método, suas variáveis de fluxo de trabalho podem ser definidas em escopos diferentes.  Considere o fluxo de trabalho na Figura 5.  Neste exemplo, você pode definir uma variável no nível raiz do fluxo de trabalho ou no escopo definido pela atividade de sequência Coletar Dados do Feed. 

Figura 5: Variáveis com escopo para atividades

Argumentos – passando dados

Os argumentos são definidos em atividades e definem o fluxo de dados dentro e fora da atividade.  Você pode pensar em argumentos para atividades como você usa argumentos para métodos em código imperativo.  Os argumentos podem ser Entrada, Saída ou Entrada/Saída e ter um nome e um tipo.  Ao criar um fluxo de trabalho, você pode definir argumentos na atividade raiz que permite que os dados sejam passados para o fluxo de trabalho quando eles forem invocados.  Você define argumentos no fluxo de trabalho tanto quanto variáveis usando a janela de argumento. 

À medida que você adiciona atividades ao fluxo de trabalho, precisará configurar os argumentos para as atividades e isso é feito principalmente referenciando variáveis no escopo ou usando expressões, o que discutirei a seguir.  Na verdade, a classe base Argument contém uma propriedade Expression que é uma Atividade que retorna um valor do tipo de argumento, portanto, todas essas opções estão relacionadas e dependem da Atividade. 

Ao usar o designer de fluxo de trabalho para editar argumentos, você pode digitar expressões que representam valores literais na grade de propriedades ou usar nomes de variáveis para fazer referência a uma variável no escopo, conforme mostrado na Figura 6, em que emailResult e emailAddress são variáveis definidas no fluxo de trabalho. 

Figura 6: Configurando argumentos em atividades

Expressões – atuando em dados

Expressões são atividades que você pode usar em seu fluxo de trabalho para operar em dados.  As expressões podem ser usadas em locais onde você usa Atividade e está interessado em um valor retornado, o que significa que você pode usar expressões na configuração de argumentos ou para definir condições em atividades como as atividades While ou If.  Lembre-se de que a maioria das coisas no WF4 derivam de Atividade e expressões não são diferentes, elas são derivadas de Activity<TResult> , o que significa que retornam um valor de um tipo específico.  Além disso, o WF4 inclui várias expressões comuns para fazer referência a variáveis e argumentos, bem como expressões do Visual Basic.  Devido a essa camada de expressões especializadas, as expressões usadas para definir argumentos podem incluir código, valores literais e referências de variáveis.  A Tabela 1 fornece uma pequena amostragem dos tipos de expressões que você pode usar ao definir argumentos usando o designer de fluxo de trabalho.  Ao criar expressões no código, há várias outras opções, incluindo o uso de expressões lambda. 

Expression Tipo de expressão

“Olá Mundo”

Valor da cadeia de caracteres literal

10

Valor int32 literal

System.String.Concat("hello", " ", "world")

Invocação de método imperativo

"Olá" & "mundo"

Expressão do Visual Basic

argInputString

Referência de argumento (nome do argumento)

varResult

Referência de variável (nome da variável)

"hello: " argInputString &

Literais e argumentos/variáveis mistos

Tabela 1: Expressões de exemplo

Criando seu primeiro fluxo de trabalho

Agora que abordei os principais conceitos em torno de Atividade e fluxo de dados, posso criar um fluxo de trabalho usando esses conceitos.  Começarei com um fluxo de trabalho hello world simples para me concentrar nos conceitos em vez da verdadeira proposta de valor do WF.  Para começar, crie um novo projeto de Teste de Unidade no Visual Studio 2010.  Para usar o núcleo do WF, adicione uma referência ao assembly System.Activities e adicione instruções using para System.Activities, System.Activities.Statements e System.IO no arquivo de classe de teste.  Em seguida, adicione um método de teste, conforme mostrado na Figura 7, para criar um fluxo de trabalho básico e executá-lo. 

[TestMethod]

public void TestHelloWorldStatic()

{

    StringWriter writer = new StringWriter();

    Console.SetOut(writer);

    Sequence wf = new Sequence

    {

        Atividades = {

            new WriteLine {Text = "Hello"},

            new WriteLine {Text = "World"}

        }

    };

    WorkflowInvoker.Invoke(wf);

    Assert.IsTrue(String.Compare(

        "Olá\r\nworld\r\n",

        Escritor. GetStringBuilder(). ToString()) == 0,

        "Cadeia de caracteres incorreta gravada");

}

Figura 7: Criando olá, mundo no código

A propriedade Text na atividade WriteLine é uma cadeia de caracteres InArgument<> e, neste exemplo, passei um valor literal para essa propriedade.

A próxima etapa é atualizar esse fluxo de trabalho para usar variáveis e passar essas variáveis para os argumentos de atividade.  A Figura 8 mostra o novo teste atualizado para usar as variáveis.

[TestMethod]

public void TestHelloWorldVariables()

{

    StringWriter writer = new StringWriter();

    Console.SetOut(writer);

Sequence wf = new Sequence

{

    Variáveis = {

        new Variable string<>{Default = "Hello", Name = "greeting"},

        new Variable string<> { Default = "Bill", Name = "name" } },

        Atividades = {

            new WriteLine { Text = new VisualBasicValue<string>("greeting"),

            new WriteLine { Text = new VisualBasicValue<string>(

            "name + \"Gates\"")}

        }

    };

    WorkflowInvoker.Invoke(wf);

}

Figura 8: Fluxo de trabalho de código com variáveis

Nesse caso, as variáveis são definidas como o tipo Cadeia de caracteres variável<> e recebem um valor padrão.  As variáveis são declaradas dentro da atividade Sequence e, em seguida, referenciadas do argumento Text das duas atividades.  Como alternativa, eu poderia usar expressões para inicializar as variáveis, conforme mostrado na Figura 9, em que a classe TResult> VisualBasicValue<é usada passando uma cadeia de caracteres que representa a expressão.  No primeiro caso, a expressão refere-se ao nome da variável e, no segundo caso, a variável é concatenada com um valor literal.  A sintaxe usada em expressões textuais é Visual Basic, mesmo ao escrever código em C#.

Atividades = {

    new WriteLine { Text = new VisualBasicValue<string>("greeting"),

        TextWriter = writer },

    new WriteLine { Text = new VisualBasicValue<string>("name + \"Gates\""),

        TextWriter = writer }

}

Figura 9: Usando expressões para definir argumentos

Embora os exemplos de código ajudem a ilustrar pontos importantes e geralmente se sintam confortáveis com os desenvolvedores, a maioria das pessoas usará o designer para criar fluxos de trabalho.  No designer, você arrasta atividades para a superfície de design e usa uma grade de propriedades atualizada, mas familiar, para definir argumentos.  Além disso, o designer de fluxo de trabalho contém regiões expansíveis na parte inferior para editar argumentos para o fluxo de trabalho e variáveis.   Para criar um fluxo de trabalho semelhante no designer, adicione um novo projeto à solução, escolhendo o modelo biblioteca de atividades e adicione uma nova Atividade. 

Quando o designer de fluxo de trabalho é exibido pela primeira vez, ele não contém nenhuma atividade.  A primeira etapa na definição da atividade é escolher a atividade raiz.  Para este exemplo, adicione uma atividade flowchart ao designer arrastando-a da categoria Fluxograma na caixa de ferramentas.  No designer flowchart, arraste duas atividades WriteLine da caixa de ferramentas e adicione uma abaixo da outra ao Fluxograma.  Agora você precisa conectar as atividades para que o Fluxograma saiba o caminho a seguir.  Faça isso primeiro passando o mouse sobre o círculo verde "iniciar" na parte superior do Fluxograma para ver as alças de captura e, em seguida, clique e arraste um para o primeiro WriteLine e solte-o na alça de arrastar que aparece na parte superior da atividade.  Faça o mesmo para conectar o primeiro WriteLine ao segundo WriteLine.  A superfície de design deve se parecer com a Figura 10. 

Figura 10: Layout de fluxograma olá

Depois que a estrutura estiver em vigor, você precisará configurar algumas variáveis.  Clicando na superfície de design e clicando no botão Variáveis próximo à parte inferior do designer, você pode editar a coleção de variáveis para o Fluxograma.  Adicione duas variáveis chamadas "greeting" e "name" para listar e definir os valores padrão como "Hello" e "Bill", respectivamente – certifique-se de incluir as aspas ao definir os valores como esta é uma expressão para que as cadeias de caracteres literais precisem ser entre aspas.  A Figura 11 mostra a janela variáveis configurada com as duas variáveis e seus valores padrão. 

Figura 11: Variáveis definidas no designer de fluxo de trabalho

Para usar essas variáveis nas atividades WriteLine, insira "greeting" (sem aspas) na grade de propriedades para o argumento Text da primeira atividade e "name" (novamente sem as aspas) para o argumento Text no segundo WriteLine.   Em runtime, quando os argumentos Text forem avaliados, o valor na variável será resolvido e usado pela atividade WriteLine.

No projeto de teste, adicione uma referência ao projeto que contém o fluxo de trabalho.  Em seguida, você pode adicionar um método de teste como o mostrado na Figura 12 para invocar esse fluxo de trabalho e testar a saída.  Na Figura 12, você pode ver que o fluxo de trabalho está sendo criado criando uma instância da classe criada.  Nesse caso, o fluxo de trabalho foi definido no designer e compilado em uma classe derivada de Activity, que pode ser invocada. 

[TestMethod]

public void TestHelloFlowChart()

{

    StringWriter tWriter = new StringWriter();

    Console.SetOut(tWriter);

    Workflows.HelloFlow wf = new Workflows.HelloFlow();

    WorkflowInvoker.Invoke(wf);

    Declarações omitidas

}

Figura 12: Testando o fluxograma

Até agora, tenho usado variáveis no fluxo de trabalho e usado-as para fornecer valores a argumentos nas atividades writeline.  O fluxo de trabalho também pode ter argumentos definidos que permitem passar dados para o fluxo de trabalho ao invocá-los e receber a saída quando o fluxo de trabalho for concluído.  Para atualizar o Fluxograma do exemplo anterior, remova a variável "name" (selecione-a e pressione a tecla Delete) e, em vez disso, crie um argumento "name" do tipo cadeia de caracteres.  Você faz isso da mesma maneira, exceto que usa o botão Argumentos para exibir o editor de argumentos.  Observe que os argumentos também podem ter uma direção e você não precisa fornecer um valor padrão, pois o valor será passado para a atividade em runtime.  Como você está usando o mesmo nome para o argumento que fez para a variável, os argumentos text das atividades WriteLine permanecem válidos.  Agora, em runtime, esses argumentos avaliarão e resolve o valor do argumento "name" no fluxo de trabalho e usarão esse valor.  Adicione um argumento adicional do tipo cadeia de caracteres com uma direção de Out e um nome de "fullGreeting"; isso será retornado ao código de chamada. 

Figura 13: Definindo argumentos para o fluxo de trabalho

No Fluxograma, adicione uma atividade Assign e conecte-a à última atividade WriteLine.  Para o argumento To, insira "fullGreeting" (sem aspas) e, para o argumento Value, insira "nome da saudação & " (sem aspas). Isso atribuirá a concatenação da variável greeting com o argumento name ao argumento de saída fullGreeting. 

Agora, no teste de unidade, atualize o código para fornecer o argumento ao invocar o fluxo de trabalho.  Os argumentos são passados para o fluxo de trabalho como uma cadeia de caracteres de dicionário<, objeto> em que a chave é o nome do argumento.  Você pode fazer isso simplesmente alterando a chamada para invocar o fluxo de trabalho, conforme mostrado na Figura 14. Observe que os argumentos de saída também estão contidos em uma cadeia de caracteres dictionary<, coleção de objetos> com chave no nome do argumento. 

Cadeia de caracteres IDictionary<, resultados do objeto> = WorkflowInvoker.Invoke(wf,

    nova cadeia de caracteres do dicionário<, objeto> {

        {"name", "Bill" } }

    );

string outValue = results["fullGreeting"]. ToString();

Figura 14: Passando argumentos para um fluxo de trabalho

Agora que você viu os conceitos básicos de como montar atividades, variáveis e argumentos, vou orientá-lo em um tour pelas atividades incluídas na estrutura para habilitar fluxos de trabalho mais interessantes focados na lógica de negócios em vez de conceitos de baixo nível.

Tour pela paleta de atividades do fluxo de trabalho

Com qualquer linguagem de programação, você espera ter constructos principais para definir a lógica do aplicativo.  Ao usar uma estrutura de desenvolvimento de nível mais alto, como o WF, essa expectativa não muda.   Assim como você tem instruções em linguagens .NET como If/Else, Switch e While, para gerenciar o fluxo de controle, você também precisa desses mesmos recursos ao definir sua lógica em um fluxo de trabalho declarativo.  Esses recursos vêm na forma da biblioteca de atividades base que acompanha a estrutura.  Nesta seção, lhe darei um tour rápido das atividades fornecidas com a estrutura para lhe dar uma ideia da funcionalidade fornecida prontamente.

Atividades primitivas e atividades de coleção

Ao mover para um modelo de programação declarativa, é fácil começar a se perguntar como realizar tarefas comuns de manipulação de objetos que são de segunda natureza ao escrever código.  Para as tarefas em que você se encontra trabalhando com objetos e precisando definir propriedades, invocar comandos ou gerenciar uma coleção de itens, há um conjunto de atividades projetadas especificamente com essas tarefas em mente. 

Atividade Descrição

Atribuir

Atribui um valor a um local – habilitando as variáveis de configuração.

Atrasar

Atrasa o caminho de execução por um período de tempo especificado.

InvokeMethod

Invoca um método em um objeto .NET ou método estático em um tipo .NET, opcionalmente com um tipo de retorno T.

WriteLine

Grava o texto especificado em um gravador de texto – o padrão é Console.Out

AddToCollection<T>

Adiciona um item a uma coleção tipada.

RemoveFromCollection<T>

Remove um item de uma coleção tipada.

ClearCollection<T>

Remove todos os itens de uma coleção.

ExistsInCollection<T>

Retorna um valor booliano que indica se o item especificado existe na coleção. 

 

Atividades de fluxo de controle

Ao definir a lógica de negócios ou os processos de negócios, ter controle do fluxo de execução é fundamental. As atividades de fluxo de controle incluem noções básicas, como Sequência, que fornece um contêiner comum quando você precisa executar etapas em ordem e lógica de ramificação comum, como as atividades If e Switch.  As atividades de fluxo de controle também incluem lógica de loop com base em dados (ForEach) e Conditions(While).  O mais importante para simplificar a programação complexa são as atividades paralelas, que permitem que várias atividades assíncronas sejam executadas ao mesmo tempo. 

Atividade Descrição

Sequência

Para executar atividades em série

While/DoWhile

Executa uma atividade filho enquanto uma condição (expressão) é verdadeira

ForEach<T>

Itera em uma coleção enumerável e executa a atividade filho uma vez para cada item na coleção, aguardando a conclusão do filho antes de iniciar a próxima iteração. Fornece acesso tipado ao item individual que conduz a iteração na forma de um argumento nomeado.

If

Executa uma das duas atividades filho dependendo do resultado da condição (expressão).

Alternar<T>

Avalia uma expressão e agenda a atividade filho com uma chave correspondente. 

Paralelo

Agenda todas as atividades filho de uma só vez, mas também fornece uma condição de conclusão para permitir que a atividade cancele atividades filho pendentes se determinadas condições forem atendidas. 

ParallelForEach<T>

Itera em uma coleção enumerável e executa a atividade filho uma vez para cada item na coleção, agendando todas as instâncias ao mesmo tempo. Assim como o ForEach<T>, essa atividade fornece acesso ao item de dados atual na forma de um argumento nomeado.

Escolher

Agenda todas as atividades de PickBranch filho e cancela todas, exceto a primeira, para que o gatilho seja concluído.  A atividade PickBranch tem um Gatilho e uma Ação; cada é uma Atividade.  Quando uma atividade de gatilho é concluída, a seleção cancela todas as outras atividades filho. 

 Os dois exemplos abaixo mostram várias dessas atividades em uso para ilustrar como compor essas atividades juntas.  O primeiro exemplo, Figura 15, inclui o uso do ParallelForEach<T> para usar uma lista de URLs e obter de forma assíncrona o RSS feed no endereço especificado.  Depois que o feed é retornado, o T ForEach<> é usado para iterar sobre os itens do feed e processá-los. 

Observe que o código declara e define uma instância do VisualBasicSettings com referências aos tipos System.ServiceModel.Syndication.  Esse objeto de configurações é usado ao declarar instâncias do VisualBasicValue<T> referenciando tipos de variáveis desse namespace para habilitar a resolução de tipos para essas expressões.  Observe também que o corpo da atividade ParallelForEach usa uma ActivityAction mencionada na seção sobre a criação de atividades personalizadas.  Essas ações usam DelegateInArgument e DelegateOutArgument da mesma forma que as atividades usam InArgument e OutArgument. 

Figura 15: Controlar atividades de fluxo

O segundo exemplo, Figura 16, é um padrão comum de espera por uma resposta com um tempo limite.  Por exemplo, aguardar que um gerente aprove uma solicitação e enviar um lembrete se a resposta não tiver chegado no tempo alocado.  Uma atividade DoWhile causa a repetição de aguardar uma resposta enquanto a atividade Pick é usada para executar uma atividade ManagerResponse e uma atividade Delay ao mesmo tempo que os gatilhos.  Quando o Atraso for concluído primeiro, o lembrete será enviado e, quando a atividade ManagerResponse for concluída primeiro, o sinalizador será definido para sair do loop DoWhile. 

Figura 16: Atividades de Escolha e DoWhile

O exemplo na Figura 16 também mostra como os indicadores podem ser usados em fluxos de trabalho.  Um indicador é criado por uma atividade para marcar um lugar no fluxo de trabalho, para que o processamento possa ser retomado a partir desse ponto posteriormente.  A atividade Delay tem um indicador que é retomado após um determinado período de tempo ter passado.  A atividade ManagerResponse é uma atividade personalizada que aguarda a entrada e retoma o fluxo de trabalho quando os dados chegam.  As atividades de mensagens, discutidas em breve, são as atividades main para a execução de indicadores.  Quando um fluxo de trabalho não está processando ativamente o trabalho, quando ele está apenas aguardando que os indicadores sejam retomados, ele é considerado ocioso e pode ser persistido em um repositório durável.  Os indicadores serão discutidos mais detalhadamente na seção sobre como criar atividades personalizadas. 

Migração

Para desenvolvedores que estão usando o WF3, a atividade interoperabilidade pode desempenhar um papel vital na reutilização de ativos existentes. A atividade, encontrada no assembly System.Workflow.Runtime, encapsula o tipo de atividade existente e exibe as propriedades na atividade como argumentos no modelo WF4.  Como as propriedades são argumentos, você pode usar expressões para definir os valores.  A Figura 17 mostra a configuração da atividade interoperabilidade para chamar uma atividade WF3.  Os argumentos de entrada são definidos com referências a variáveis no escopo dentro da definição de fluxo de trabalho.    As atividades criadas no WF3 tinham propriedades em vez de argumentos, portanto, cada propriedade recebe um argumento de entrada e saída correspondente, permitindo diferenciar os dados enviados para a atividade e os dados que você espera recuperar após a execução da atividade. Em um novo projeto WF4, você não encontrará essa atividade na caixa de ferramentas porque a estrutura de destino está definida como .NET Framework perfil de cliente 4.  Altere a estrutura de destino nas propriedades do projeto para .NET Framework 4 e a atividade aparecerá na caixa de ferramentas.

Figura 17: Configuração da atividade de interoperabilidade

Fluxograma

Ao criar fluxos de trabalho de Fluxograma, há vários constructos que podem ser usados para gerenciar o fluxo de execução dentro do Fluxograma.  Esses constructos fornecem etapas simples, pontos de decisão simples com base em uma única condição ou uma instrução switch.  A potência real do Fluxograma é a capacidade de conectar esses tipos de nó ao fluxo desejado.

Construção/atividade Descrição

Fluxograma

O contêiner para uma série de etapas de fluxo, cada etapa de fluxo pode ser qualquer atividade ou um dos constructos a seguir, mas, para ser executado, ele deve estar conectado dentro do Fluxograma. 

FlowDecision

Fornece lógica de ramificação com base em uma condição.

FlowSwitch<T>

Habilita vários branches com base no valor de uma expressão. 

Flowstep

Representa uma etapa no Fluxograma com a capacidade de se conectar a outras etapas. Esse tipo não é mostrado na caixa de ferramentas, pois é adicionado implicitamente pelo designer.  Essa atividade encapsula outras atividades no fluxo de trabalho e fornece a semântica de navegação para as próximas etapas no fluxo de trabalho. 

É importante observar que, embora haja atividades específicas para o modelo de Fluxograma, todas as outras atividades podem ser usadas no fluxo de trabalho.  Da mesma forma, uma atividade flowchart pode ser adicionada a outra atividade para fornecer a semântica de execução e design de um Fluxograma, dentro desse fluxo de trabalho.  Isso significa que você pode ter uma sequência com várias atividades e um Fluxograma bem no meio. 

Atividades de mensagem

Um dos principais focos no WF4 é a integração mais rígida entre o WF e o WCF.  Em termos de fluxos de trabalho, isso significa atividades para modelar operações de mensagens, como enviar e receber mensagens.  Na verdade, há várias atividades diferentes incluídas na estrutura para mensagens, cada uma com funcionalidade e finalidade ligeiramente diferentes. 

Atividade Descrição

Enviar/Receber

Atividades de mensagens unidirecionais para enviar ou receber uma mensagem.  Essas mesmas atividades são compostas em interações de estilo de solicitação/resposta. 

ReceiveAndSendReply

Modela uma operação de serviço que recebe uma mensagem e envia uma resposta de volta. 

SendAndReceiveReply

Invoca uma operação de serviço e recebe a resposta. 

InitializeCorrelation

Permita inicializar valores de correlação explicitamente como parte da lógica do fluxo de trabalho, em vez de extrair os valores de uma mensagem. 

CorrelationScope

Define um escopo de execução em que um identificador de correlação é acessível para receber e enviar atividades simplificando a configuração de um identificador compartilhado por várias atividades de mensagens. 

TransactedReceiveScope

Permite que a lógica de fluxo de trabalho seja incluída na mesma transação fluída para uma operação do WCF usando a atividade Receive.

Para invocar uma operação de serviço de dentro de um fluxo de trabalho, você seguirá as etapas conhecidas de adicionar uma referência de serviço ao seu projeto de fluxo de trabalho.  O sistema de projeto no Visual Studio consumirá os metadados do serviço e criará uma atividade personalizada para cada operação de serviço encontrada no contrato.  Você pode pensar nisso como o equivalente de fluxo de trabalho de um proxy WCF que seria criado em um projeto C# ou Visual Basic.   Por exemplo, usando um serviço existente que pesquisa reservas de hotel com o contrato de serviço mostrado na Figura 18; adicionar uma referência de serviço a ela produz uma atividade personalizada mostrada na Figura 19. 

[ServiceContract]

interface pública IHotelService

{

    [OperationContract]

    Listar<HotelSearchResult> SearchHotels(

        HotelSearchRequest requestDetails);

}

Figura 18: Contrato de serviço

Figura 19: Atividade personalizada do WCF

Mais informações sobre como criar fluxos de trabalho expostos como serviços WCF podem ser encontradas na seção Serviços de Fluxo de Trabalho mais adiante neste artigo. 

Transações e tratamento de erros

Escrever sistemas confiáveis pode ser difícil e requer que você faça um bom trabalho de tratamento de erros e gerenciamento para manter o estado consistente em seu aplicativo.  Para um fluxo de trabalho, os escopos para gerenciar o tratamento de exceções e transações são modelados usando atividades com propriedades do tipo ActivityAction ou Activity para permitir que os desenvolvedores modelem a lógica de processamento de erros.  Além desses padrões comuns de consistência, o WF4 também inclui atividades que permitem modelar a coordenação distribuída de longa execução por meio de compensação e confirmação. 

Atividade Descrição

CancellationScope

Usado para permitir que o desenvolvedor de fluxo de trabalho reaja se um corpo de trabalho for cancelado. 

TransactionScope

Habilita a semântica semelhante ao uso de um escopo de transação no código executando todas as atividades filho no escopo em uma transação. 

TryCatch/Catch<T>

Usado para modelar o tratamento de exceções e capturar exceções tipada. 

Throw

Pode ser usado para gerar uma exceção da atividade.  

Relançar

Usado para relançar uma exceção, geralmente uma que foi capturada usando a atividade TryCatch. 

CompensableActivity

Define a lógica para executar atividades filho que talvez precisem ter suas ações compensadas após o êxito.  Fornece um espaço reservado para a lógica de compensação, a lógica de confirmação e o tratamento de cancelamento.

Compensate

Invoca a lógica de tratamento de compensação para uma atividade compensatória.

Confirmar

Invoca a lógica de confirmação para uma atividade compensavel. 

A atividade TryCatch fornece uma maneira familiar de definir o escopo de um conjunto de trabalho para capturar quaisquer exceções que possam ocorrer, e a atividade Catch<T> fornece o contêiner para a lógica de tratamento de exceções.  Você pode ver um exemplo de como essa atividade é usada na Figura 20, com cada bloco de exceção tipado sendo definido por uma atividade Catch<T> , fornecendo acesso à exceção por meio do argumento nomeado visto à esquerda de cada captura.   Se a necessidade surgir, a atividade Rethrow poderá ser usada para relançar a exceção capturada ou a atividade Throw para gerar uma nova exceção própria. 

Figura 20: Atividade TryCatch

As transações ajudam a garantir a consistência no trabalho de curta duração e a atividade TransactionScope fornece o mesmo tipo de escopo que você pode obter no código .NET usando a classe TransactionScope. 

Por fim, quando você precisa manter a consistência, mas não pode usar uma transação atômica entre os recursos, pode usar a compensação.  A compensação permite definir um conjunto de trabalhos que, se ele for concluído, pode ter um conjunto de atividades para compensar as alterações feitas.  Como um exemplo simples, considere a Atividade Compensavel na Figura 21 em que um email é enviado.  Se depois que a atividade for concluída, ocorrerá uma exceção, a lógica de compensação poderá ser invocada para colocar o sistema de volta em um estado consistente; nesse caso, um email de acompanhamento para retirar a mensagem anterior. 

Figura 21: Atividade compensada

Criando e executando fluxos de trabalho

Assim como acontece com qualquer linguagem de programação, há duas coisas fundamentais que você faz com os fluxos de trabalho: defina-os e execute-os.  Em ambos os casos, o WF fornece várias opções para oferecer flexibilidade e controle. 

Opções para criar fluxos de trabalho

Ao projetar ou definir fluxos de trabalho, há duas opções de main: código ou XAML. O XAML fornece a experiência verdadeiramente declarativa e permite que toda a definição do fluxo de trabalho seja definida na marcação XML, fazendo referência a Atividades e tipos criados usando o .NET.  A maioria dos desenvolvedores provavelmente usará o designer de fluxo de trabalho para criar fluxos de trabalho que resultarão em uma definição declarativa de fluxo de trabalho XAML.  Como o XAML é apenas XML, no entanto, qualquer ferramenta pode ser usada para criá-la, o que é um dos motivos pelos quais é um modelo tão poderoso para criar aplicativos.  Por exemplo, o XAML mostrado na Figura 22 foi criado em um editor de texto simples e pode ser compilado ou usado diretamente para executar uma instância do fluxo de trabalho que está sendo definido. 

<p:Activity x:Class="Workflows.HelloSeq" xmlns="https://schemas.microsoft.com/netfx/2009/xaml/activities/design" xmlns:p="https://schemas.microsoft.com/netfx/2009/xaml/activities" xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml">

  <x:Members>

    <x:Property Name="greeting" Type="p:InArgument(x:String)" />

    <x:Property Name="name" Type="p:InArgument(x:String)" />

  </x:Members>

  <p:Sequence>

    <p:WriteLine>[greeting &amp; " from workflow"]</p:WriteLine>

    <p:WriteLine>[name]</p:WriteLine>

  </p:Sequence>

</p:Activity>

Figura 22: Fluxo de trabalho definido em XAML

Esse mesmo XAML é gerado pelo designer e pode ser gerado por ferramentas personalizadas, facilitando muito o gerenciamento.  Além disso, também é possível, como foi mostrado anteriormente, criar fluxos de trabalho usando código.  Isso envolve a criação da hierarquia de atividades por meio do uso das várias atividades na estrutura e suas atividades personalizadas.  Você já viu vários exemplos de fluxos de trabalho definidos inteiramente no código.  Ao contrário do WF3, agora é possível criar um fluxo de trabalho no código e serializá-lo facilmente para XAML; fornecendo mais flexibilidade na modelagem e no gerenciamento de definições de fluxo de trabalho. 

Como mostrarei, para executar um fluxo de trabalho, tudo o que você precisa é de uma Atividade e que pode ser uma instância interna de código ou uma criada a partir de XAML.  O resultado final de cada uma das técnicas de modelagem é uma classe derivada de Activity ou uma representação XML que pode ser desserializada ou compilada em uma Atividade. 

Opções para executar fluxos de trabalho

Para executar um fluxo de trabalho, você precisa de uma Atividade que defina o fluxo de trabalho.  Há duas maneiras típicas de obter uma Atividade que pode ser executada: criá-la em código ou ler em um arquivo XAML e desserializar o conteúdo em uma Atividade.  A primeira opção é simples e já mostrei vários exemplos.  Para carregar um arquivo XAML, você deve usar a classe ActivityXamlServices, que fornece um método load estático.  Basta passar um objeto Stream ou XamlReader e você obtém de volta a Atividade representada no XAML.  

Depois de ter uma Atividade, a maneira mais simples de executá-la é usando a classe WorkflowInvoker, como fiz nos testes de unidade anteriormente.  O método Invoke dessa classe tem um parâmetro do tipo Atividade e retorna uma cadeia de caracteres IDictionary<, objeto> .  Se você precisar passar argumentos para o fluxo de trabalho, primeiro defina-os no fluxo de trabalho e, em seguida, passe os valores junto com a Atividade para o método Invoke como dicionário de pares nome/valor.  Da mesma forma, todos os argumentos Out ou In/Out definidos no fluxo de trabalho serão retornados como resultado da execução do método.  A Figura 23 fornece um exemplo de carregamento de um fluxo de trabalho de um arquivo XAML, passando argumentos para o fluxo de trabalho e recuperando os argumentos de saída resultantes. 

MathWF de atividade;

using (Stream mathXaml = File.OpenRead("Math.xaml"))

{

    mathWF = ActivityXamlServices.Load(mathXaml);

}

var outputs = WorkflowInvoker.Invoke(mathWF,

    new Dictionary<string, object> {

    { "operand1", 5 },

    { "operand2", 10 },

    { "operation", "add" } });

Assert.AreEqual<int>(15, (int)outputs["result"], "Resultado incorreto retornado");

Figura 23: Invocando o fluxo de trabalho "XAML solto" com argumentos de entrada e saída

Observe neste exemplo que a Atividade é carregada de um arquivo XAML e ainda pode aceitar e retornar argumentos.  O fluxo de trabalho foi desenvolvido usando o designer no Visual Studio para facilitar, mas pode ser desenvolvido em um designer personalizado e armazenado em qualquer lugar.  O XAML pode ser carregado de um banco de dados, biblioteca do SharePoint ou algum outro repositório antes de ser entregue ao runtime para execução.  

O uso do WorkflowInvoker fornece o mecanismo mais simples para executar fluxos de trabalho de curta duração.  Essencialmente, ele faz com que o fluxo de trabalho aja como uma chamada de método em seu aplicativo, permitindo que você aproveite com mais facilidade todos os benefícios do WF sem precisar fazer muito trabalho para hospedar o próprio WF.  Isso é especialmente útil ao testar suas atividades e fluxos de trabalho, pois reduz a configuração de teste necessária para exercer um componente em teste. 

Outra classe de hospedagem comum é o WorkflowApplication, que fornece um identificador seguro para um fluxo de trabalho que está sendo executado no runtime e permite que você gerencie fluxos de trabalho de execução prolongada com mais facilidade.  Com o WorkflowApplication, você ainda pode passar argumentos para o fluxo de trabalho da mesma forma que com o WorkflowInvoker, mas você usa o método Run para realmente iniciar a execução do fluxo de trabalho.  Neste ponto, o fluxo de trabalho começa a ser executado em outro thread e o controle retorna ao código de chamada. 

Como o fluxo de trabalho agora está sendo executado de forma assíncrona, no código de hospedagem, você provavelmente desejará saber quando o fluxo de trabalho for concluído ou se ele gerar uma exceção etc. Para esses tipos de notificações, a classe WorkflowApplication tem um conjunto de propriedades do tipo Action<T> que podem ser usadas como eventos para adicionar código para reagir a determinadas condições da execução do fluxo de trabalho, incluindo: anulação, exceção sem tratamento, concluída, ociosa e descarregada.  Ao executar um fluxo de trabalho usando WorkflowApplication, você pode usar um código semelhante ao mostrado na Figura 24 usando ações para lidar com os eventos. 

WorkflowApplication wf = new WorkflowApplication(new Flowchart1());

Wf. Concluído = delegate(WorkflowApplicationCompletedEventArgs e)

{

    Console.WriteLine("Fluxo de {0} trabalho concluído", e.InstanceId);

};

Wf. Abortado = delegate(WorkflowApplicationAbortedEventArgs e)

{

    Console.WriteLine(e.Reason);

};

Wf. OnUnhandledException =

  delegate(WorkflowApplicationUnhandledExceptionEventArgs e)

  {

      Console.WriteLine(e.UnhandledException.ToString());

      retornar UnhandledExceptionAction.Terminate;

  };

Wf. Run();

Figura 24: WorkflowApplication actions

Neste exemplo, a meta do código host, um aplicativo de console simples, é notificar o usuário por meio do console quando o fluxo de trabalho for concluído ou se ocorrer um erro.  Em um sistema real, o host estará interessado nesses eventos e provavelmente reagirá a eles de forma diferente para fornecer aos administradores informações sobre falhas ou para gerenciar melhor as instâncias.

Extensões de fluxo de trabalho

Um dos principais recursos do WF, desde o WF3, é que ele é leve o suficiente para ser hospedado em qualquer domínio de aplicativo .NET.  Como o runtime pode ser executado em domínios diferentes e pode precisar de semântica de execução personalizada, vários aspectos dos comportamentos de runtime precisam ser externalizados do runtime.  É aí que as extensões de fluxo de trabalho entram em jogo.  As extensões de fluxo de trabalho permitem que você, como desenvolvedor, escreva o código do host, se preferir, para adicionar comportamento ao runtime estendendo-o com código personalizado. 

Dois tipos de extensão que o runtime do WF está ciente são as extensões de persistência e acompanhamento. A extensão de persistência fornece a funcionalidade principal para salvar o estado de um fluxo de trabalho em um repositório durável e recuperar esse estado quando necessário.  O envio de extensão de persistência com a estrutura inclui suporte para o Microsoft SQL Server, mas extensões podem ser gravadas para dar suporte a outros sistemas de banco de dados ou repositórios duráveis. 

Persistência

Para usar a extensão de persistência, primeiro você deve configurar um banco de dados para manter o estado, o que pode ser feito usando os scripts SQL encontrados em %windir%\Microsoft.NET\Framework\v4.0.30319\sql\<language> (por exemplo, c:\windows\microsoft.net\framework\v4.0.30319\sql\en\). Depois de criar um banco de dados, você executa os dois scripts para criar a estrutura (SqlWorkflowInstanceStoreSchema.sql) e os procedimentos armazenados (SqlWorkflowInstanceStoreLogic.sql) no banco de dados. 

Depois que o banco de dados for configurado, você usará o SqlWorkflowInstanceStore junto com a classe WorkflowApplication.  Primeiro, crie o repositório e configure-o e, em seguida, forneça-o ao WorkflowApplication, conforme mostrado na Figura 25.

Aplicativo WorkflowApplication = novo WorkflowApplication(activity);

InstanceStore instanceStore = new SqlWorkflowInstanceStore(

    @"Data Source=.\\SQLEXPRESS;Integrated Security=True");

Exibição InstanceView = instanceStore.Execute(

    instanceStore.CreateInstanceHandle(), novo CreateWorkflowOwnerCommand(),

    TimeSpan.FromSeconds(30));

instanceStore.DefaultInstanceOwner = view. Instanceowner;

Aplicativo. InstanceStore = instanceStore;

Figura 25: adicionando o provedor de persistência do SQL

Há duas maneiras pelas quais o fluxo de trabalho pode ser persistido.  A primeira é por meio do uso direto da atividade Persist em um fluxo de trabalho.  Quando essa atividade é executada, faz com que o estado do fluxo de trabalho seja persistido no banco de dados, salvando o estado atual do fluxo de trabalho.  Isso oferece o controle do autor do fluxo de trabalho sobre quando é importante salvar o estado atual do fluxo de trabalho.  A segunda opção permite que o aplicativo de hospedagem persista o estado do fluxo de trabalho quando vários eventos ocorrem na instância de fluxo de trabalho; provavelmente quando o fluxo de trabalho está ocioso.  Ao registrar-se para a ação PersistableIdle no WorkflowApplication, o código do host pode responder ao evento para indicar se a instância deve ser persistente, descarregada ou nenhuma delas.  A Figura 26 mostra um aplicativo host se registrando para ser notificado quando o fluxo de trabalho está ocioso e fazendo com que ele persista. 

Wf. PersistableIdle = (waie) => PersistableIdleAction.Persist;

Figura 26: Descarregando o fluxo de trabalho quando ele está ocioso

Depois que um fluxo de trabalho estiver ocioso e persistir, o runtime do WF poderá descarregá-lo da memória, liberando recursos para outros fluxos de trabalho que precisam de processamento.  Você pode optar por descarregar sua instância de fluxo de trabalho retornando a enumeração apropriada da ação PersistableIdle.  Depois que um fluxo de trabalho for descarregado, em algum momento o host desejará carregá-lo de volta do repositório de persistência para retomá-lo.  Para fazer isso, a instância de fluxo de trabalho deve ser carregada usando um repositório de instâncias e o identificador de instância. Isso, por sua vez, fará com que o repositório de instâncias seja solicitado a carregar o estado.  Depois que o estado é carregado, o método Run no WorkflowApplication pode ser usado ou um indicador pode ser retomado, conforme mostrado na Figura 27.  Para obter mais informações sobre indicadores, consulte a seção atividades personalizadas. 

Aplicativo WorkflowApplication = novo WorkflowApplication(activity);

Aplicativo. InstanceStore = instanceStore;

Aplicativo. Load(id);

Aplicativo. ResumeBookmark(readLineBookmark, input);

Figura 27: Retomando um fluxo de trabalho persistente

Uma das alterações no WF4 é como o estado dos fluxos de trabalho é persistente e depende muito das novas técnicas de gerenciamento de dados de argumentos e variáveis.  Em vez de serializar toda a árvore de atividades e manter o estado durante o tempo de vida do fluxo de trabalho, somente em variáveis de escopo e valores de argumento, juntamente com alguns dados de runtime, como informações de indicador, são persistidos.  Observe na Figura 27 que, quando a instância persistente é recarregada, além de usar o repositório de instâncias, a Atividade que define o fluxo de trabalho também é passada. Essencialmente, o estado do banco de dados é aplicado à Atividade fornecida.  Essa técnica permite uma flexibilidade muito maior para o controle de versão e resulta em um melhor desempenho, pois o estado tem um volume de memória menor. 

Acompanhamento

A persistência permite que o host dê suporte a processos de execução longa, instâncias de balanceamento de carga entre hosts e outros comportamentos tolerantes a falhas.  No entanto, depois que a instância de fluxo de trabalho for concluída, o estado no banco de dados geralmente é excluído, pois não é mais útil.  Em um ambiente de produção, ter informações sobre o que um fluxo de trabalho está fazendo no momento e o que ele fez é fundamental para gerenciar fluxos de trabalho e obter insights sobre o processo de negócios.  Ser capaz de acompanhar o que está acontecendo em seu aplicativo é um dos recursos atraentes do uso do runtime do WF.  

O acompanhamento consiste em dois componentes principais: acompanhamento de participantes e perfis de acompanhamento.  Um perfil de acompanhamento define quais eventos e dados você deseja que o runtime rastreie. Os perfis podem incluir três tipos principais de consultas:

  • ActivityStateQuery – usado para identificar estados de atividade (por exemplo, fechados) e variáveis ou argumentos para extrair dados
  • WorkflowInstanceQuery – usado para identificar eventos de fluxo de trabalho
  • CustomTrackingQuery – usado para identificar chamadas explícitas para rastrear dados, geralmente dentro de atividades personalizadas

Por exemplo, a Figura 28 mostra um TrackingProfile sendo criado que inclui um ActivityStateQuery e um WorkflowInstanceQuery.   Observe que as consultas indicam quando coletar informações e também quais dados coletar.  Para ActivityStateQuery, incluí uma lista de variáveis que devem ter seu valor extraído e adicionado aos dados rastreados. 

Perfil TrackingProfile = novo TrackingProfile

{

    Name = "SimpleProfile",

    Consultas = {

        new WorkflowInstanceQuery {

            States = { "*" }

        },

        new ActivityStateQuery {

            ActivityName = "WriteLine",

            States={ "*" },

            Variáveis = {"Text" }

        }

    }

};

Figura 28: Criando um perfil de acompanhamento

Um participante de rastreamento é uma extensão que pode ser adicionada ao runtime e é responsável por processar registros de rastreamento conforme eles são emitidos.  A classe base TrackingParticipant define uma propriedade para fornecer um TrackingProfile e um método Track que manipula o rastreamento.  A Figura 29 mostra um participante de acompanhamento personalizado limitado que grava dados no console.  Para usar o participante de rastreamento, ele deve ser inicializado com um perfil de acompanhamento e, em seguida, adicionado à coleção de extensões na instância de fluxo de trabalho. 

classe pública ConsoleTrackingParticipant : TrackingParticipant

{

   protected override void Track(TrackingRecord record, TimeSpan timeout)

   {

      ActivityStateRecord aRecord = record como ActivityStateRecord;

      if (aRecord != null)

      {

         Console.WriteLine("{0} estado {1}inserido ",

            aRecord.Activity.Name, aRecord.State);

         foreach (item var em aRecord.Arguments)

         {

            Console.ForegroundColor = ConsoleColor.Cyan;

            Console.WriteLine("Variável:{0} tem valor: {1}",

                Item. Chave, item. Valor);

            Console.ResetColor();

         }

      }

   }

}

Figura 29: Participante do acompanhamento do console

O WF é fornecido com um EtwTrackingParticipant (ETW = Enterprise Trace para Windows) que você pode adicionar ao runtime para habilitar o rastreamento de dados de alto desempenho.  O ETW é um sistema de rastreamento que é um componente nativo no Windows e é usado por muitos componentes e serviços no sistema operacional, incluindo drivers e outros códigos de nível de kernel.  Os dados gravados no ETW podem ser consumidos usando código personalizado ou usando ferramentas como o próximo AppFabric do Windows Server.  Para os usuários que migram do WF3, isso será uma alteração, pois não haverá envio de participantes do acompanhamento do SQL como parte da estrutura.  No entanto, o Windows Server AppFabric enviará com consumidores ETW que coletarão os dados ETW e os armazenarão em um banco de dados SQL.  Da mesma forma, você pode criar um consumidor ETW e armazenar os dados em qualquer formato que preferir ou escrever seu próprio participante de acompanhamento, o que fizer mais sentido para seu ambiente. 

Criando atividades personalizadas

Embora a biblioteca de atividades base inclua uma rica paleta de atividades para interagir com serviços, objetos e coleções, ela não fornece atividades para interagir com subsistemas, como bancos de dados, servidores de email ou seus objetos e sistemas de domínio personalizados.  Parte da introdução ao WF4 será descobrir quais atividades principais você pode precisar ou deseja ao criar fluxos de trabalho.  A grande coisa é que, à medida que você cria unidades principais de trabalho, você pode combiná-las em atividades granulares mais grosseiras que os desenvolvedores podem usar em seus fluxos de trabalho. 

Hierarquia da classe de atividade

Embora uma atividade seja uma atividade, não é verdade que todas as atividades tenham os mesmos requisitos ou necessidades.  Por esse motivo, em vez de todas as atividades derivadas diretamente da Atividade, há uma hierarquia de classes base de atividade, mostrada na Figura 30, que você pode escolher ao criar uma atividade personalizada. 

Figura 30: Hierarquia da classe de atividade

Para a maioria das atividades personalizadas, você derivará de AsyncCodeActivity, CodeActivity ou NativeActivity (ou uma das variantes genéricas) ou modelará sua atividade declarativamente.  Em um alto nível, as quatro classes base podem ser descritas da seguinte maneira:

  • Atividade – usada para modelar atividades compondo outras atividades, geralmente definidas usando XAML.
  • CodeActivity – uma classe base simplificada quando você precisa escrever algum código para realizar o trabalho.
  • AsyncCodeActivity – usado quando sua atividade executa algum trabalho de forma assíncrona.
  • NativeActivity – quando sua atividade precisa de acesso aos internos de runtime, por exemplo, para agendar outras atividades ou criar indicadores.

Nas seções a seguir, criarei várias atividades para ver como usar cada uma dessas classes base.  Em geral, conforme você pensa sobre qual classe base usar, você deve começar com a classe base Atividade e ver se você pode criar sua atividade usando a lógica declarativa, conforme mostrado na próxima seção.  Em seguida, o CodeActivity fornecerá um modelo simplificado se você determinar que precisa escrever algum código .NET para realizar sua tarefa e o AsyncCodeActivity se desejar que sua atividade seja executada de forma assíncrona.  Por fim, se você estiver escrevendo atividades de fluxo de controle como as encontradas na estrutura (por exemplo, Enquanto, Alternar, Se), precisará derivar da classe base NativeActivity para gerenciar as atividades filho. 

Compondo atividades usando o Designer de atividades

Quando você cria um novo projeto de biblioteca de atividades ou quando adiciona um novo item a um projeto do WF e seleciona o modelo Atividade, o que você obtém é um arquivo XAML com um elemento Activity vazio nele.  No designer, isso se apresenta como uma superfície de design em que você pode criar o corpo da atividade.  Para começar a usar uma atividade que terá mais de uma etapa, geralmente ajuda a arrastar uma atividade Sequence como o Corpo e, em seguida, preenchê-la com sua lógica de atividade real, pois o corpo representa uma única atividade filho. 

Você pode pensar na atividade como faria com um método em um componente com argumentos.  Na própria atividade, você pode definir argumentos, juntamente com sua direcionalidade, para definir a interface da atividade.  As variáveis que você deseja usar dentro da atividade precisarão ser definidas nas atividades que compõem o corpo, como a sequência raiz mencionada anteriormente.  Por exemplo, você pode criar uma atividade NotifyManager que compõe duas atividades mais simples: GetManager e SendMail. 

Primeiro, crie um novo projeto ActivityLibrary no Visual Studio 2010 e renomeie o arquivo Activity1.xaml para NotifyManager.xaml.  Em seguida, arraste uma atividade Sequence da caixa de ferramentas e adicione-a ao designer.  Depois que a Sequência estiver em vigor, você poderá preenchê-la com atividades filho, conforme mostrado na Figura 31. (Observe que as atividades usadas neste exemplo são atividades personalizadas em um projeto referenciado e não estão disponíveis na estrutura.)

Figura 31: Notificar a atividade do gerenciador

Essa atividade precisa usar argumentos para o nome do funcionário e o corpo da mensagem. A atividade GetManager procura o gerente do funcionário e fornece o email como uma cadeia de caracteres OutArgument<>.  Por fim, a atividade SendMail envia uma mensagem ao gerente.  A próxima etapa é definir os argumentos para a atividade expandindo a janela Argumentos na parte inferior do designer e inserindo as informações para os dois argumentos de entrada necessários. 

Para compor esses itens, você precisa ser capaz de passar os argumentos de entrada especificados na atividade NotifyManager para as atividades filho individuais.  Para a atividade GetManager, você precisa do nome do funcionário que é um argumento in na atividade. Você pode usar o nome do argumento na caixa de diálogo de propriedade para o argumento employeeName na atividade GetManager, como visto na Figura 31.  A atividade SendMail precisa do endereço de email do gerente e da mensagem.  Para a mensagem, você pode inserir o nome do argumento que contém a mensagem.  No entanto, para o endereço de email, você precisa de uma maneira de passar o argumento de saída da atividade GetManager para o argumento in para a atividade SendMail.  Para isso, você precisa de uma variável. 

Depois de realçar a atividade Sequência, você pode usar a janela Variáveis para definir uma variável chamada "mgrEmail".  Agora você pode inserir esse nome de variável para o argumento ManagerEmail out na atividade GetManager e o argumento Para no na atividade SendMail.  Quando a atividade GetManager for executada, os dados de saída serão armazenados nessa variável e, quando a atividade SendMail for executada, ela lerá dados dessa variável como o argumento in. 

A abordagem descrita é um modelo puramente declarativo para definir o corpo da atividade.  Em algumas circunstâncias, talvez você prefira especificar o corpo da atividade no código.  Por exemplo, sua atividade pode incluir uma coleção de propriedades que, por sua vez, conduzem um conjunto de atividades filho; um conjunto de valores nomeados que impulsionam a criação de um conjunto de atividades de atribuição seria um caso em que o uso do código seria preferencial.  Nesses casos, você pode escrever uma classe derivada da atividade e escrever código na propriedade Implementação para criar uma Atividade (e quaisquer elementos filho) para representar a funcionalidade de sua atividade.  O resultado final é o mesmo em ambos os casos: sua lógica é definida pela composição de outras atividades, apenas o mecanismo pelo qual o corpo é definido é diferente.  A Figura 32 mostra a mesma atividade NotifyManager que está sendo definida no código. 

classe pública NotifyManager : Atividade

{

    public InArgument<string> EmployeeName { get; set; }

    public InArgument<string> Message { get; set; }

    Implementação de atividade> func<de substituição protegida

    {

        get

        {

            return () =>

            {

                Variável<string> mgrEmail =

                new Variable string<> { Name = "mgrEmail" };

                Sequência s = nova sequência

                {

                    Variáveis = { mgrEmail },

                    Atividades = {

                        new GetManager {

                            EmployeeName =

                                nova cadeia de caracteres VisualBasicValue<>("EmployeeName"),

                                Resultado = mgrEmail,

                        },

                        new SendMail {

                            ToAddress = mgrEmail,

                            MailBody = nova cadeia de caracteres VisualBasicValue<>("Message"),

                            De = "test@contoso.com",

                            Assunto = "Email automatizado"

                        }

                    }

                };

                return s;

            };

        }

        set { base. Implementação = valor; }

    }

}

Figura 32: Composição de atividade usando código

Observe que a classe base é Atividade e a propriedade Implementation é uma Atividade> Func<para retornar uma única Atividade. Esse código é muito semelhante ao mostrado anteriormente ao criar fluxos de trabalho e isso não deve ser surpreendente, pois a meta em ambos os casos é criar uma Atividade.  Além disso, nos argumentos de abordagem de código podem ser definidos com variáveis ou você pode usar expressões para conectar um argumento a outro, como é mostrado para os argumentos EmployeeName e Message, pois eles são usados nas duas atividades filho. 

Escrevendo classes de atividade personalizadas

Se você determinar que sua lógica de atividade não pode ser realizada compondo outras atividades, você poderá escrever uma atividade baseada em código.  Ao escrever atividades no código, você deriva da classe apropriada, define argumentos e, em seguida, substitui o método Execute.  O método Execute é chamado pelo runtime quando é hora da atividade fazer seu trabalho.

Para criar a atividade SendMail usada no exemplo anterior, primeiro preciso escolher o tipo base.  Embora seja possível criar a funcionalidade da atividade SendMail usando a classe base Atividade e compondo as atividades TryCatch<T> e InvokeMethod, para a maioria dos desenvolvedores será mais natural escolher entre as classes base CodeActivity e NativeActivity e escrever código para a lógica de execução.  Como essa atividade não precisa criar indicadores ou agendar outras atividades, posso derivar da classe base CodeActivity.  Além disso, como essa atividade retornará um único argumento de saída, ela deve derivar de CodeActivity<TResult> , que fornece um OutArgument<TResult>.  A primeira etapa é definir vários argumentos de entrada para as propriedades de email.  Em seguida, você substitui o método Execute para implementar a funcionalidade da atividade.  A Figura 33 mostra a classe de atividade concluída. 

classe pública SendMail: CodeActivity<SmtpStatusCode>

{

    public InArgument<string> To { get; set; }

    public InArgument<string> From { get; set; }

    public InArgument<string> Subject { get; set; }

    public InArgument<string> Body { get; set; }

    protected override SmtpStatusCode Execute(

    Contexto CodeActivityContext)

    {

        experimentar

        {

            Cliente SmtpClient = novo SmtpClient();

            Cliente. Send(

            From.Get(context), ToAddress.Get(context),

            Subject.Get(context), MailBody.Get(context));

        }

        catch (SmtpException smtpEx)

        {

            retornar smtpEx.StatusCode;

        }

        retornar SmtpStatusCode.Ok;

    }

}

Figura 33: Atividade personalizada do SendMail

Há várias coisas a observar sobre o uso de argumentos.  Declarei várias propriedades padrão do .NET usando os tipos InArgument<T>.  É assim que uma atividade baseada em código define seus argumentos de entrada e saída.  No entanto, dentro do código, não consigo simplesmente referenciar essas propriedades para obter o valor do argumento, preciso usar o argumento como uma espécie de identificador para recuperar o valor usando ActivityContext fornecido; nesse caso, um CodeActivityContext.  Você usa o método Get da classe de argumento para recuperar o valor e o método Set para atribuir um valor a um argumento. 

Depois que a atividade conclui o método Execute, o runtime detecta isso e pressupõe que a atividade seja feita e a feche.  Para trabalhos assíncronos ou de execução longa, há maneiras de alterar esse comportamento que são explicados em uma seção futura, mas nesse caso, depois que o email é enviado, o trabalho é concluído. 

Agora vamos examinar uma atividade usando a classe base NativeActivity para gerenciar atividades filho.  Criarei uma atividade iterador que executa algumas atividades filho um determinado número de vezes.  Primeiro, preciso de um argumento para manter o número de iterações a serem executadas, uma propriedade para a Atividade a ser executada e uma variável para manter o número atual de iterações. O método Execute chama um método BeginIteration para iniciar a iteração inicial e as iterações subsequentes são invocadas da mesma maneira.  Observe na Figura 34 que as variáveis são manipuladas da mesma maneira que os argumentos que usam o método Get e Set. 

classe pública Iterador: NativeActivity

{

    public Activity Body { get; set; }

    public InArgument<int> RequestedIterations { get; set; }

    public Variable<int> CurrentIteration { get; set; }

    public Iterator()

    {

        CurrentIteration = new Variable<int> { Default = 0 };

    }

    protected override void Execute(NativeActivityContext context)

    {

        BeginIteration(context);

    }

    private void BeginIteration(NativeActivityContext context)

    {

        if (RequestedIterations.Get(context) > CurrentIteration.Get(context))

        {

            Contexto. ScheduleActivity(Body,

            new CompletionCallback(ChildComplete),

            new FaultCallback(ChildFaulted));

        }

    }

}

Figura 34: Atividade nativa do iterador

Se você examinar atentamente o método BeginIteration, observará a chamada do método ScheduleActivity.  É assim que uma atividade pode interagir com o runtime para agendar outra atividade para execução e é porque você precisa dessa funcionalidade derivada de NativeActivity.  Observe também que, ao chamar o método ScheduleActivity no ActivityExecutionContext, dois métodos de retorno de chamada são fornecidos.  Como você não sabe qual atividade será usada como o corpo ou quanto tempo levará para ser concluída e como o WF é um ambiente de programação fortemente assíncrono, os retornos de chamada são usados para notificar sua atividade quando uma atividade filho for concluída, permitindo que você escreva código para reagir.

O segundo retorno de chamada é um FaultCallback que será invocado se a atividade filho gerar uma exceção.  Nesse retorno de chamada, você recebe a exceção e tem a capacidade, por meio do ActivityFaultContext, de indicar para o runtime que o erro foi tratado, o que impede que ele seja propagado para cima e para fora da atividade. A Figura 35 mostra os métodos de retorno de chamada para a atividade iterador em que tanto agendam a próxima iteração quanto FaultCallback manipula o erro para permitir que a execução continue para a próxima iteração.

private void ChildComplete(NativeActivityContext context,

Instância activityInstance)

{

    CurrentIteration.Set(context, CurrentIteration.Get(context) + 1);

    BeginIteration(context);

}

private void ChildFaulted(NativeActivityFaultContext context, Exception ex,

Instância activityInstance)

{

    CurrentIteration.Set(context, CurrentIteration.Get(context) + 1);

    Contexto. HandleFault();

}

Figura 35: Retornos de chamada de atividade

Quando sua atividade precisa fazer um trabalho que pode ser de execução prolongada, o ideal é que esse trabalho possa ser entregue a outro thread para permitir que o thread de fluxo de trabalho seja usado para continuar processando outras atividades ou usado para processar outros fluxos de trabalho.  No entanto, simplesmente iniciar threads e retornar o controle para o runtime pode causar problemas, pois o runtime não monitora os threads que você cria.  Se o runtime determinar que o fluxo de trabalho estava ocioso, o fluxo de trabalho poderá descarregar, descartar seus métodos de retorno de chamada ou o runtime poderá assumir que sua atividade foi feita e passar para a próxima atividade no fluxo de trabalho.  Para dar suporte à programação assíncrona em sua atividade, você deriva do AsyncCodeActivity ou asyncCodeActivity<T>. 

A Figura 36 mostra um exemplo de uma atividade assíncrona que carrega um RSS feed.  Essa assinatura do método execute é diferente para as atividades assíncronas em que ele retorna um IAsyncResult.  Você escreve o código no método execute para iniciar sua operação assíncrona.  Substitua o método EndExecute para manipular o retorno de chamada quando a operação assíncrona for concluída e retornar o resultado da execução. 

classe pública selada GetFeed : AsyncCodeActivity<SyndicationFeed>

{

    public InArgument<Uri> FeedUrl { get; set; }

    protected override IAsyncResult BeginExecute(

    Contexto asyncCodeActivityContext, retorno de chamada de AsyncCallback,

    estado do objeto)

    {

        var req = (HttpWebRequest)HttpWebRequest.Create(

        FeedUrl.Get(context));

        Req. Método = "GET";

        Contexto. UserState = req;

        return req. BeginGetResponse(new AsyncCallback(callback), state);

    }

    protected override SyndicationFeed EndExecute(

    Contexto AsyncCodeActivityContext, resultado de IAsyncResult)

    {

        HttpWebRequest req = context. UserState como HttpWebRequest;

        WebResponse wr = req. EndGetResponse(result);

        SyndicationFeed localFeed = SyndicationFeed.Load(

        XmlReader.Create(wr. GetResponseStream()));

        retornar localFeed;

    }

}

Figura 36: Criando atividades assíncronas

Conceitos de atividades adicionais

Agora que você viu os conceitos básicos de criação de atividades usando as classes base, argumentos e variáveis e gerenciando a execução; Tocarei brevemente em alguns recursos mais avançados que você pode usar no desenvolvimento de atividades. 

Indicadores permitem que um autor de atividade crie um ponto de retomada na execução de um fluxo de trabalho.  Depois que um indicador tiver sido criado, ele poderá ser retomado para continuar o processamento do fluxo de trabalho de onde parou.  Os indicadores são salvos como parte do estado, portanto, ao contrário das atividades assíncronas, depois que um indicador é criado, não é um problema para a instância de fluxo de trabalho ser persistente e descarregada.  Quando o host deseja retomar o fluxo de trabalho, ele carrega a instância de fluxo de trabalho e chama o método ResumeBookmark para retomar a instância de onde parou.  Ao retomar indicadores, os dados também podem ser passados para a atividade.   A Figura 37 mostra uma atividade ReadLine que cria um indicador para receber entrada e registra um método de retorno de chamada a ser invocado quando os dados chegam.  O runtime sabe quando uma atividade tem indicadores pendentes e não fechará a atividade até que o indicador seja retomado.  O método ResumeBookmark pode ser usado na classe WorkflowApplication para enviar dados para o indicador nomeado e sinalizar o BookmarkCallback.  

classe pública ReadLine: cadeia de caracteres NativeActivity<>

{

    public OutArgument<string> InputText { get; set; }

    protected override void Execute(NativeActivityContext context)

    {

        Contexto. CreateBookmark("ReadLine",

        new BookmarkCallback(BookmarkResumed));

    }

    private void BookmarkResumed(NativeActivityContext context,

        Indicador bk, estado do objeto)

    {

        Result.Set(context, state);

    }

}

Figura 37: Criando um indicador

Outro recurso poderoso para autores de atividade é o conceito de uma ActivityAction.  ActivityAction é o equivalente de fluxo de trabalho da classe Action no código imperativo: descrevendo um delegado comum; mas, para alguns, a ideia de um modelo pode ser mais fácil de entender.  Considere a atividade T ForEach<> que permite iterar em um conjunto de dados e agendar uma atividade filho para cada item de dados.  Você precisa de alguma maneira para permitir que o consumidor de sua atividade defina o corpo e seja capaz de consumir o item de dados do tipo T. Essencialmente, você precisa de alguma atividade que possa aceitar um item do tipo T e agir sobre ele, ActivityAction<T> é usado para habilitar esse cenário e fornece uma referência ao argumento e à definição de uma Atividade para processar o item.  Você pode usar ActivityAction em suas atividades personalizadas para permitir que os consumidores de sua atividade adicionem suas próprias etapas em pontos apropriados.  Isso permite que você crie modelos de fluxo de trabalho ou de atividade de uma classificação, em que um consumidor pode preencher as partes relevantes para personalizar a execução para seu uso.  Você também pode usar o ActivityFunc<TResult> e as alternativas relacionadas quando o delegado que sua atividade precisa invocar retornará um valor.  

Por fim, as atividades podem ser validadas para garantir que elas sejam definidas corretamente verificando configurações individuais, restringindo atividades filho permitidas etc. Para a necessidade comum de exigir um argumento específico, você pode adicionar um atributo RequiredArgument à declaração de propriedade na atividade.  Para uma validação mais envolvida, no construtor de sua atividade, crie uma restrição e adicione-a à coleção de restrições exibidas na classe Activity.  Uma restrição é uma atividade gravada para inspecionar a atividade de destino e garantir a validade.  A Figura 38 mostra o construtor para a atividade Iterator, que valida que a propriedade RequestedIterations está definida. Por fim, substituindo o método CacheMetadata, você pode invocar o método AddValidationError no parâmetro ActivityMetdata para adicionar erros de validação para sua atividade. 

var act = new DelegateInArgument<Iterator> { Name = "constraintArg" };

var vctx = new DelegateInArgument<ValidationContext>();

Constraint<Iterator> cons = new Constraint<Iterator>

{

    Body = new ActivityAction<Iterator, ValidationContext>

    {

        Argument1 = act,

        Argument2 = vctx,

        Manipulador = novo AssertValidation

        {

            Mensagem = "Iteração deve ser fornecida",

            PropertyName = "RequestedIterations",

            Assertion = new InArgument<bool>(

                (e) => act. Get(e). RequestedIterations != null)

        }

    }

};

Base. Constraints.Add(cons);

Figura 38: Restrições

Designers de atividade

Um dos benefícios do WF é que ele permite programar sua lógica de aplicativo declarativamente, mas a maioria das pessoas não deseja escrever XAML manualmente, razão pela qual a experiência do designer no WF é tão importante.  Ao criar atividades personalizadas, você também provavelmente desejará criar um designer para fornecer a experiência de exibição e interação visual para os consumidores de sua atividade.  O designer para WF baseia-se em Windows Presentation Foundation o que significa que você tem todo o poder de estilo, gatilhos, vinculação de dados e todas as outras ótimas ferramentas para criar uma interface do usuário avançada para seu designer.  Além disso, o WF fornece alguns controles de usuário que você pode usar em seu designer para simplificar a tarefa de exibir uma atividade filho individual ou uma coleção de atividades.  Os quatro controles principais são:

  • ActivityDesigner – controle WPF raiz usado em designers de atividade
  • WorkflowItemPresenter – usado para exibir uma única Atividade
  • WorkflowItemsPresenter – usado para exibir uma coleção de atividades filho
  • ExpressionTextBox – usado para habilitar a edição in-loco de expressões, como argumentos

O WF4 contém um modelo de projeto ActivityDesignerLibrary e um modelo de item ActivityDesigner que podem ser usados para criar os artefatos inicialmente.  Depois de inicializar o designer, você pode começar a usar os elementos acima para personalizar a aparência da atividade, definindo como exibir dados e representações visuais de atividades filho.  No designer, você tem acesso a um ModelItem que é uma abstração sobre a atividade real e mostra todas as propriedades da atividade. 

O controle WorkflowItemPresenter pode ser usado para exibir uma única Atividade que faz parte de sua atividade.  Por exemplo, para fornecer a capacidade de arrastar uma atividade para a atividade iterador mostrada anteriormente e exibir a atividade contida na atividade Iteractor, você pode usar o XAML mostrado na Figura 39, associando o WorkflowItemPresenter ao ModelItem.Body.  A Figura 40 mostra o designer em uso em uma superfície de design de fluxo de trabalho. 

<sap:ActivityDesigner x:Class="CustomActivities.Presentation.IteratorDesigner"

  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

  xmlns:sap="clr-namespace:System.Activities.Presentation;

assembly=System.Activities.Presentation"

  xmlns:sapv="clr-namespace:System.Activities.Presentation.View;

assembly=System.Activities.Presentation">

  <Grade>

    <Border BorderBrush="MidnightBlue" BorderThickness="3" CornerRadius="10">

<sap:WorkflowItemPresenter MinHeight="50"

Item="{Binding Path=ModelItem.Body, Mode=TwoWay}"

HintText="Adicionar corpo aqui" />

    </Fronteira>

  </Grade>

</sap:ActivityDesigner>

Figura 39: WorkflowItemPresenter no designer de atividades

Figura 40: Designer de iterador

WorkflowItemsPresenter fornece funcionalidade semelhante para exibir vários itens, como os filhos em uma sequência. Você pode definir um modelo e orientação de como deseja exibir os itens, bem como um modelo de espaçador para adicionar elementos visuais entre atividades como conectores, setas ou algo mais interessante.  A Figura 41 mostra um designer simples para uma atividade de sequência personalizada que exibe as atividades filho com uma linha horizontal entre cada uma.

<sap:ActivityDesigner x:Class="CustomActivities.Presentation.CustomSequenceDesigner"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:sap="clr-namespace:System.Activities.Presentation;

assembly=System.Activities.Presentation"

    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;

assembly=System.Activities.Presentation">

    <Grade>

      <StackPanel>

         <Border BorderBrush="Goldenrod" BorderThickness="3">

     <sap:WorkflowItemsPresenter HintText="Drop Activities Here"

  Items="{Binding Path=ModelItem.ChildActivities}">

     <sap:WorkflowItemsPresenter.SpacerTemplate>

 <DataTemplate>

    <Retângulo Height="3" Width="40"

 Fill="MidnightBlue" Margin="5" />

                     </Datatemplate>

                  </sap:WorkflowItemsPresenter.SpacerTemplate>

                  <sap:WorkflowItemsPresenter.ItemsPanel>

                      <Itemspaneltemplate>

                          <Orientação de StackPanel="Vertical"/>

                      </Itemspaneltemplate>

                  </sap:WorkflowItemsPresenter.ItemsPanel>

            </sap:WorkflowItemsPresenter>

        </Fronteira>

      </Stackpanel>

    </Grade>

</sap:ActivityDesigner>

Figura 41: Designer de sequência personalizado usando WorkflowItemsPresenter

Figura 42: Designer de sequência

Por fim, o controle ExpressionTextBox permite a edição in-loco de argumentos de atividade dentro do designer, além da grade de propriedades. No Visual Studio 2010, isso inclui o suporte do intellisense para as expressões que estão sendo inseridas. A Figura 43 mostra um designer para a atividade GetManager criada anteriormente, habilitando a edição dos argumentos EmployeeName e ManagerEmail no designer. O designer real é mostrado na Figura 44. 

<sap:ActivityDesigner x:Class="CustomActivities.Presentation.GetManagerDesigner"

    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

    xmlns:sap="clr-namespace:System.Activities.Presentation;

assembly=System.Activities.Presentation"

    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;

assembly=System.Activities.Presentation"

xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;

assembly=System.Activities.Presentation">

    <sap:ActivityDesigner.Resources>

        <sapc:ArgumentToExpressionConverter

x:Key="ArgumentToExpressionConverter"

x:Uid="swdv:ArgumentToExpressionConverter_1" />

    </sap:ActivityDesigner.Resources>

    <Grade>

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="50" />

            <ColumnDefinition Width="*" />

        </Grid.ColumnDefinitions>

        <Grid.RowDefinitions>

            <Rowdefinition/>

            <Rowdefinition/>

            <Rowdefinition/>

        </Grid.RowDefinitions>

        <TextBlock VerticalAlignment="Center"

 HorizontalAlignment="Center">Employee:</TextBlock>

        <sapv:ExpressionTextBox Grid.Column="1"

              Expression="{Binding Path=ModelItem.EmployeeName, Mode=TwoWay,

       Converter={StaticResource ArgumentToExpressionConverter},

       ConverterParameter=In}" OwnerActivity="{Binding Path=ModelItem}"

MinLines="1" MaxLines="1" MinWidth="50"

HintText="&lt; Nome&do Funcionário gt;" />

        <Grade TextBlock.Row="1" VerticalAlignment="Center"

              HorizontalAlignment="Center">Mgr Email:</TextBlock>

        <sapv:ExpressionTextBox Grid.Column="2" Grid.Row="1"

    UseLocationExpression="True"

           Expression="{Binding Path=ModelItem.ManagerEmail, Mode=TwoWay,

    Converter={StaticResource ArgumentToExpressionConverter},

    ConverterParameter=Out}" OwnerActivity="{Binding Path=ModelItem}"

    MinLines="1" MaxLines="1" MinWidth="50" />

    </Grade>

</sap:ActivityDesigner>

Figura 43: Usando ExpressionTextBox para editar argumentos no designer

Figura 44: Designer de atividade GetManager

Os designers de exemplo apresentados aqui eram simples para realçar os recursos específicos, mas todo o poder do WPF está à sua disposição para facilitar a configuração e o uso mais natural para seus consumidores.   Depois de criar um designer, há duas maneiras de associá-lo à atividade: aplicar um atributo à classe de atividade ou implementar a interface IRegisterMetadata.  Para permitir que a definição de atividade impulsione a escolha do designer, basta aplicar o DesignerAttribute à classe de atividade e fornecer o tipo para o designer; isso funciona exatamente como no WF3.  Para uma implementação mais flexívelmente acoplada, você pode implementar a interface IRegisterMetadata e definir os atributos que deseja aplicar à classe de atividade e às propriedades.  A Figura 45 mostra um exemplo de implementação para aplicar os designers a duas atividades personalizadas. O Visual Studio descobrirá sua implementação e a invocará automaticamente, enquanto um host de designer personalizado, discutido em seguida, chamará o método explicitamente.

Metadados da classe pública : IRegisterMetadata

{

    public void Register()

    {

        AttributeTableBuilder builder = new AttributeTableBuilder();

        Construtor. AddCustomAttributes(typeof(SendMail), new Attribute[] {

            new DesignerAttribute(typeof(SendMailDesigner)});

        Construtor. AddCustomAttributes(typeof(GetManager), new Attribute[]{

            new DesignerAttribute(typeof(GetManagerDesigner)});

        MetadataStore.AddAttributeTable(builder. CreateTable());

    }

}

Figura 45: Registrando designers para atividades

Hospedar novamente o designer de fluxo de trabalho

No passado, os desenvolvedores geralmente queriam fornecer aos usuários a capacidade de criar ou editar fluxos de trabalho.  Em versões anteriores do Windows Workflow Foundation, isso era possível, mas não uma tarefa trivial.  Com a mudança para o WPF vem um novo designer e a hospedagem foi um caso de uso privilegiado para a equipe de desenvolvimento.  Você pode hospedar o designer em um aplicativo WPF personalizado como o mostrado na Figura 46 com algumas linhas de XAML e algumas linhas de código. 

Figura 46: Designer de fluxo de trabalho hospedado novamente

O XAML da janela, Figura 47, usa uma grade para layout de três colunas e, em seguida, coloca um controle de borda em cada célula.  Na primeira célula, a caixa de ferramentas é criada declarativamente, mas também pode ser criada em código.  Para a própria exibição do designer e a grade de propriedades, há dois controles de borda vazios em cada uma das células restantes.  Esses controles são adicionados no código. 

<Janela x:Class="WorkflowEditor.MainWindow"

        xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"

        xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"

        xmlns:sys="clr-namespace:System;assembly=mscorlib"

 xmlns:sapt="clr-namespace:System.Activities.Presentation.Toolbox;

assembly=System.Activities.Presentation"

        Title="Editor de Fluxo de Trabalho" Height="500" Width="700" >

    <Window.Resources>

        <sys:String x:Key="AssemblyName">System.Activities, Version=4.0.0.0,

Culture=neutral, PublicKeyToken=31bf3856ad364e35</sys:String>

        <sys:String x:Key="MyAssemblyName">CustomActivities</sys:String>

    </Window.Resources>

    <Grade x:Name="DesignerGrid">

        <Grid.ColumnDefinitions>

            <ColumnDefinition Width="2*" />

            <ColumnDefinition Width="7*" />

            <ColumnDefinition Width="3*" />

        </Grid.ColumnDefinitions>

        <Borda>

            <sapt:ToolboxControl>

                <sapt:ToolboxControl.Categories>

                    <sapt:ToolboxCategory CategoryName="Basic">

                        <sapt:ToolboxItemWrapper

  AssemblyName="{StaticResource AssemblyName}" >

                            <sapt:ToolboxItemWrapper.ToolName>

                                System.Activities.Statements.Sequence

                            </sapt:ToolboxItemWrapper.ToolName>

                        </sapt:ToolboxItemWrapper>

                        <sapt:ToolboxItemWrapper

AssemblyName="{StaticResource MyAssemblyName}">

                            <sapt:ToolboxItemWrapper.ToolName>

                                CustomActivities.SendMail

                            </sapt:ToolboxItemWrapper.ToolName>

                        </sapt:ToolboxItemWrapper>

                    </sapt:ToolboxCategory>

                </sapt:ToolboxControl.Categories>

            </sapt:ToolboxControl>

        </Fronteira>

        <Border Name="DesignerBorder" Grid.Column="1"

BorderBrush="Salmon" BorderThickness="3" />

        <Border x:Name="PropertyGridExpander" Grid.Column="2" />

    </Grade>

</Janela>

Figura 47: Designer rehosting XAML

O código para inicializar o designer e a grade de propriedades é mostrado na Figura 48.  A primeira etapa, no método OnInitialized, é registrar os designers para todas as atividades que serão usadas no designer de fluxo de trabalho.  A classe DesignerMetadata registra os designers associados para as atividades que são enviadas com estrutura e a classe Metadados registra os designers para minhas atividades personalizadas.  Em seguida, crie a classe WorkflowDesigner e use as propriedades View e PropertyInspectorView para acessar os objetos UIElement necessários para renderizar.  O designer é inicializado com uma Sequência vazia, mas você também pode adicionar menus e carregar o XAML de fluxo de trabalho de uma variedade de fontes, incluindo o sistema de arquivos ou um banco de dados. 

public MainWindow()

{

    InitializeComponent();

}

protected override void OnInitialized(EventArgs e) {

    Base. OnInitialized(e);

    RegisterDesigners();

    PlaceDesignerAndPropGrid();

}

private void RegisterDesigners() {

    new DesignerMetadata(). Register();

    new CustomActivities.Presentation.Metadata(). Register();

}

private void PlaceDesignerAndPropGrid() {

    Designer WorkflowDesigner = novo WorkflowDesigner();

    Designer. Load(new System.Activities.Statements.Sequence());

    DesignerBorder.Child = designer. Ver;

    PropertyGridExpander.Child = designer. PropertyInspectorView;

}

Figura 48: Designer rehosting code

Com esse pouco de código e marcação, você pode editar um fluxo de trabalho, arrastar e soltar atividades da caixa de ferramentas, configurar variáveis no fluxo de trabalho e manipular argumentos nas atividades.  Você também pode obter o texto do XAML chamando Flush no designer e referenciando a propriedade Text do WorkflowDesigner.  Há muito mais que você pode fazer, e começar é muito mais fácil do que antes. 

Serviços de fluxo de trabalho

Conforme mencionado anteriormente, uma das grandes áreas de foco nesta versão do Windows Workflow Foundation é uma integração mais rigorosa com o Windows Communication Foundation.  O exemplo no passo a passo da atividade mostrou como chamar um serviço de um fluxo de trabalho e, nesta seção, discutirei como expor um fluxo de trabalho como um serviço WCF. 

Para começar, crie um novo projeto e selecione o projeto serviço de fluxo de trabalho do WCF. Depois que o modelo for concluído, você encontrará um projeto com um arquivo web.config e um arquivo com uma extensão XAMLX.  O arquivo XAMLX é um serviço declarativo ou de fluxo de trabalho e, como outros modelos do WCF, fornece uma implementação de serviço funcional, embora simples, que recebe uma solicitação e retorna uma resposta.  Neste exemplo, alterarei o contrato para criar uma operação para receber um relatório de despesas e retornar um indicador de que o relatório foi recebido.  Para começar, adicione uma classe ExpenseReport ao projeto como a mostrada na Figura 49.

[DataContract]

classe pública ExpenseReport

{

    [DataMember]

    public DateTime FirstDateOfTravel { get; set; }

    [DataMember]

    public double TotalAmount { get; set; }

    [DataMember]

    public string EmployeeName { get; set; }

}

Figura 49: Tipo de contrato de relatório de despesas

De volta ao designer de fluxo de trabalho, altere o tipo da variável "data" nas variáveis para o tipo ExpenseReport definido (talvez seja necessário criar o projeto para que o tipo apareça na caixa de diálogo do seletor de tipos). Atualize a atividade Receive clicando no botão Conteúdo e alterando o tipo na caixa de diálogo para o tipo ExpenseReport para corresponder.  Atualize as propriedades de atividade para definir um novo OperationName e ServiceContractName, conforme mostrado na Figura 50. 

Figura 50: Configurando o local de recebimento

Agora, a atividade SendReply pode ter o Valor definido como True para retornar o indicador de recibo.  Obviamente, em um exemplo mais complicado, você pode usar todo o poder do fluxo de trabalho para salvar informações no banco de dados, fazer a validação do relatório etc. antes de determinar a resposta. Navegar até o serviço com o aplicativo WCFTestClient mostra como a configuração de atividade define o contrato de serviço exposto, conforme mostrado na Figura 51. 

Figura 51: Contrato gerado do serviço de fluxo de trabalho

Depois de criar um serviço de fluxo de trabalho, você precisará hospedá-lo, assim como faria com qualquer serviço WCF.  O exemplo anterior usou a opção mais simples para hospedagem: IIS.  Para hospedar um serviço de fluxo de trabalho em seu próprio processo, você desejará usar a classe WorkflowServiceHost encontrada no assembly System.ServiceModel.Activities.  Observe que há outra classe com esse mesmo nome no assembly System.WorkflowServices que é usado para hospedar serviços de fluxo de trabalho criados usando as atividades do WF3.  No caso mais simples de auto-hospedagem, você pode usar um código como o mostrado na Figura 52 para hospedar seu serviço e adicionar pontos de extremidade. 

WorkflowServiceHost host = new

    WorkflowServiceHost(XamlServices.Load("ExpenseReportService.xamlx"),

    new Uri("https://localhost:9897/Services/Expense"));

Host. AddDefaultEndpoints();

Host. Description.Behaviors.Add(

    new ServiceMetadataBehavior { HttpGetEnabled = true });

Host. Open();

Console.WriteLine("O host está aberto");

Console.ReadLine();

Figura 52: Hospedagem automática do serviço de fluxo de trabalho

Se você quiser aproveitar as opções de hospedagem gerenciada no Windows, poderá hospedar seu serviço no IIS copiando o arquivo XAMLX para o diretório e especificando as informações de configuração necessárias para comportamentos, associações, pontos de extremidade etc. no arquivo web.config.  Na verdade, como visto anteriormente, quando você cria um novo projeto no Visual Studio usando um dos modelos de projeto do Serviço de Fluxo de Trabalho Declarativo, você obterá um projeto com um arquivo web.config e o serviço de fluxo de trabalho definido em um XAMLX, pronto para implantação. 

Ao usar a classe WorkflowServiceHost para hospedar seu serviço de fluxo de trabalho, você ainda precisa ser capaz de configurar e controlar as instâncias de fluxo de trabalho.  Para controlar o serviço, há um novo ponto de extremidade padrão chamado WorkflowControlEndpoint.  Uma classe complementar, WorkflowControlClient, fornece um proxy de cliente predefinido.  Você pode expor um WorkFlowControlEndpoint em seu serviço e usar o WorkflowControlClient para criar e executar novas instâncias de fluxos de trabalho ou controlar fluxos de trabalho em execução.  Você usa esse mesmo modelo para gerenciar serviços no computador local ou pode usá-lo para gerenciar serviços em um computador remoto.  A Figura 53 mostra um exemplo atualizado de exposição do ponto de extremidade de controle de fluxo de trabalho em seu serviço. 

WorkflowServiceHost host = new

    WorkflowServiceHost("ExpenseReportService.xamlx",

    new Uri("https://localhost:9897/Services/Expense"));

Host. AddDefaultEndpoints();

WorkflowControlEndpoint wce = new WorkflowControlEndpoint(

    new NetNamedPipeBinding(),

    new EndpointAddress("net.pipe://localhost/Expense/WCE"));

Host. AddServiceEndpoint(wce);

Host. Open();

Figura 53: Ponto de extremidade de controle de fluxo de trabalho

Como você não está criando o WorkflowInstance diretamente, há duas opções para adicionar extensões ao runtime.  A primeira é que você pode adicionar algumas extensões ao WorkflowServiceHost e elas serão inicializados corretamente com suas instâncias de fluxo de trabalho.  A segunda é usar a configuração .  O sistema de acompanhamento, por exemplo, pode ser totalmente definido no arquivo de configuração do aplicativo.   Você define os participantes de rastreamento e os perfis de acompanhamento no arquivo de configuração e, usando um comportamento, aplica um participante específico a um serviço, conforme mostrado na Figura 54. 

<system.serviceModel>

    <tracking>

      <participants>

        <add name="EtwTrackingParticipant"

             type="System.Activities.Tracking.EtwTrackingParticipant, System.Activities, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"

             profileName="HealthMonitoring_Tracking_Profile"/>

      </Participantes>

      <perfis>

        <trackingProfile name="HealthMonitoring_Tracking_Profile">

          <workflow activityDefinitionId="*">

            <workflowInstanceQuery>

              <states>

                <state name="Started"/>

                <state name="Completed"/>

              </Estados>

            </Workflowinstancequery>

          </Workflow>

        </Trackingprofile>

      </Perfis>

</Rastreamento>

. . .

<behaviors>

      <serviceBehaviors>

        <behavior name="SampleTrackingSample.SampleWFBehavior">

          <etwTracking profileName=" HealthMonitoring_Tracking_Profile" />

        </Comportamento>

      </Servicebehaviors>

</Comportamentos>

. . .

Figura 54: Configurando o acompanhamento de serviços

Há muitas melhorias novas na hospedagem e na configuração para serviços WCF que você pode aproveitar em seus fluxos de trabalho, como serviços ".svc-less" ou serviços que não precisam de uma extensão de arquivo, associações padrão e pontos de extremidade padrão apenas para citar alguns.  Para obter mais informações sobre esses e outros recursos no WCF4, consulte o artigo complementar sobre o WCF encontrado na seção Recursos Adicionais. 

Além das melhorias de hospedagem, o Windows Workflow Foundation aproveita outros recursos no WCF; alguns diretamente e outros indiretamente.  Por exemplo, a correlação de mensagens agora é um recurso fundamental na criação de serviços que permite relacionar mensagens a uma determinada instância de fluxo de trabalho com base em dados na mensagem, como um número de pedido ou id de funcionário. A correlação pode ser configurada nas várias atividades de mensagens para a solicitação e a resposta e dá suporte à correlação em vários valores na mensagem.  Novamente, mais detalhes sobre esses novos recursos podem ser encontrados no artigo complementar no WCF4.   

Conclusão

A nova tecnologia do Windows Workflow Foundation que acompanha o .NET Framework 4 representa uma grande melhoria no desempenho e na produtividade do desenvolvedor e realiza uma meta de desenvolvimento declarativo de aplicativos para lógica de negócios.  A estrutura fornece as ferramentas para que unidades curtas de trabalho complicado sejam simplificadas e para a criação de serviços complexos de execução longa com mensagem correlacionada, estado persistente e visibilidade avançada do aplicativo por meio do acompanhamento. 

Sobre o autor

Matt é membro da equipe técnica da Pluralsight, onde se concentra em tecnologias de sistemas conectados (WCF, WF, BizTalk, AppFabric e a Plataforma de Serviços do Azure). Matt também é um consultor independente especializado em design e desenvolvimento de aplicativos Microsoft .NET. Como escritor, Matt contribuiu para várias revistas e revistas, incluindo a MSDN Magazine, onde atualmente é autor do conteúdo do fluxo de trabalho para a coluna Foundations. Matt compartilha regularmente seu amor pela tecnologia falando em conferências locais, regionais e internacionais, como Tech Ed. A Microsoft reconheceu Matt como MVP por sua comunidade contribuições em relação à tecnologia de sistemas conectados. Entre em contato com Matt por meio de seu blog: http://www.pluralsight.com/community/blogs/matt/default.aspx.

Recursos adicionais