Windows Foundation 4

Criando atividades personalizadas de fluxo de controle no WF 4

Leon Welicki

Fluxo de controle é a maneira na qual instruções individuais de um programa são organizadas e executadas. No Windows Workflow Foundation 4 (WF 4), as atividades de fluxo de controle regem a semântica de execução de uma ou mais atividades filho. Alguns exemplos da caixa de ferramentas de atividades do WF 4 incluem Sequence, Parallel, If, ForEach, Pick, Flowchart e Switch, entre outras.

O tempo de execução do WF não tem conhecimento de primeira classe de nenhum fluxo de controle, como Sequence ou Parallel. Da perspectiva do WF, todas as coisas são apenas atividades. O tempo de execução apenas impõe algumas regras simples (por exemplo, “uma atividade não poderá ser concluída se qualquer atividade filho ainda estiver em execução”). O fluxo de controle do WF é baseado em hierarquia, um programa do WF é uma árvore de atividades.

As opções de fluxo de controle do WF 4 não estão limitadas às atividades fornecidas na estrutura. Você pode escrever suas próprias atividades e usá-las em conjunto com as fornecidas no produto, e isso é o que é discutido neste artigo. Você aprenderá como escrever suas próprias atividades de fluxo de controle por meio de uma abordagem “engatinhar, andar e correr”: começaremos com uma atividade muito simples de fluxo de controle e adicionaremos sofisticação conforme progredirmos, terminando com uma atividade de fluxo de controle nova e útil. O código-fonte de todos os nossos exemplos está disponível para download.

Mas, primeiro, vamos começar com conceitos básicos de atividades para definir uma base comum.

Atividades

Atividades são a unidade básica de execução em um programa do WF. Um programa de fluxo de controle é uma árvore de atividades que são executadas pelo tempo de execução do WF. O WF 4 inclui mais de 35 atividades, um conjunto abrangente que pode ser usado para modelar processos ou criar novas atividades. Algumas dessas atividades regem a semântica de como outras atividades são executadas (como Sequence, Flowchart, Parallel e ForEach) e são conhecidas como atividades compostas. Outras executam uma tarefa atômica simples (WriteLine, InvokeMethod e assim por diante). Chamamos essas atividades de folha

As atividades do WF são implementadas como tipos CLR e, portanto, são derivadas de outros tipos existentes. É possível criar atividades visualmente e declarativamente por meio do designer do WF, ou imperativamente por meio da escrita de código CLR. Os tipos básicos disponíveis para criar sua própria atividade personalizada são definidos na hierarquia de tipos de atividade Figura 1. Uma explicação detalhada dessa hierarquia de tipos é encontrada na Biblioteca MSDN em msdn.microsoft.com/library/dd560893.

image: Activity Type Hierarchy

Figura 1 Hierarquia de tipos de atividade

Neste artigo, focalizarei as atividades que são derivadas da NativeActivity, a classe base que fornece acesso a toda a extensão do tempo de execução do WF. As atividades de fluxo de controle são atividades compostas que são derivadas do tipo NativeActivity porque precisam interagir com o tempo de execução do WF. Normalmente, isso é para agendar outras atividades, (por exemplo, Sequence, Parallel ou Flowchart), mas também pode incluir a implementação de cancelamento personalizado por meio de CancellationScope ou Pick, a criação de indicadores com Receive e a persistência com o uso de Persist.

O modelos de dados de atividades definem um modelo claro para a lógica dos dados ao criar e consumir atividades. Os dados são definidos com argumentos e variáveis. Os argumentos são os terminais de associação de uma atividade e definem sua assinatura pública em termos de quais dados podem ser passados para a atividade (argumentos de entrada) e quais dados serão retornados pela atividade quando sua execução for concluída (argumentos de saída). As variáveis representam o armazenamento temporário de dados.

Os autores de atividades usam argumentos para definir a maneira como os dados fluem para dentro e para fora de uma atividade, e as variáveis são usadas de duas maneiras:

  • Para expor uma coleção de variáveis editáveis pelo usuário em uma definição de atividade que pode ser usada para compartilhar variáveis entre várias atividades (como uma coleção de variáveis em Sequence e Flowchart).
  • Para modelar o estado interno de uma atividade.

Os autores de fluxo de trabalho usam argumentos para associar atividades ao ambiente por meio da criação de expressões, e declaram variáveis em diferentes escopos no fluxo de trabalho para compartilhar dados entre atividades. As variáveis e argumentos são combinados para fornecer um modelo previsível de comunicação entre atividades.

Agora que abordei alguns conceitos básicos de atividades, vamos começar com a primeira atividade de fluxo de controle.

Uma atividade simples de fluxo de controle

Começarei criando uma atividade simples de fluxo de controle chamada ExecuteIfTrue. Não há nada complicado nesta atividade: Ela executará uma atividade contida se uma condição for verdadeira. O WF 4 fornece uma atividade If que inclui atividades filho Then e Else. Com frequência, queremos fornecer apenas a Then, e a atividade Else é apenas uma sobrecarga. Nesses casos, queremos uma atividade que execute outra atividade com base no valor de uma condição booliana.

Essa atividade deve funcionar da seguinte maneira:

  • O usuário da atividade deve fornecer uma condição booliana. Esse argumento é necessário.
  • O usuário da atividade pode fornecer um corpo, a atividade a ser executada se a condição for verdadeira.
  • Em tempo de execução: Se a condição for verdadeira e o corpo não for nulo, executar o corpo.

A implementação a seguir é para uma atividade ExecuteIfTrue que se comporta exatamente dessa maneira:

public class ExecuteIfTrue : NativeActivity
{
  [RequiredArgument]
  public InArgument<bool> Condition { get; set; }

  public Activity Body { get; set; }

  public ExecuteIfTrue() { }  

  protected override void Execute(NativeActivityContext context)
  {            
    if (context.GetValue(this.Condition) && this.Body != null)
      context.ScheduleActivity(this.Body);
  }
}

Esse código é muito simples, mas há mais aqui do que se pode notar à primeira vista. ExecuteIfTrue executará uma atividade filho se a condição for verdadeira, portanto, é necessário agendar outra atividade. Portanto, ela é derivada da NativeActivity pois precisa interagir com o tempo de execução do WF para agendar filhos.

Depois de decidir a classe base de uma atividade, você precisa definir sua assinatura pública. Em ExecuteIfTrue, isso consiste em um argumento de entrada booliano do tipo InArgument<bool> denominado Condition que contém a condição a ser avaliada, e uma propriedade de tipo Activity denominada Body com a atividade a ser executada se a condição for verdadeira. O argumento Condition é decorado com o atributo RequiredArgument que indica para o tempo de execução do WF que ele deve ser definido com uma expressão. O tempo de execução do WF reforçará essa validação ao preparar a atividade para execução:

[RequiredArgument]
public InArgument<bool> Condition { get; set; }

public Activity Body { get; set; }

A parte mais interessante do código nessa atividade é o método Execute, que está onde a “ação” ocorre. Todas as NativeActivities devem substituir esse método. O método Execute recebe um argumento NativeActivityContext, que é o nosso ponto de interação com o tempo de execução do WF como autores de atividades. Em ExecuteIfTrue, esse contexto é usado para recuperar o valor do argumento Condition (context.GetValue(this.Condition)) e para agendar o Body por meio do método ScheduleActivity. Observe que digo agendar e não executar. O tempo de execução do WF não executa as atividades imediatamente. Em vez disso, ele as adiciona a uma lista de itens de trabalho a serem agendados para execução:

protected override void Execute(NativeActivityContext context)
{
    if (context.GetValue(this.Condition) && this.Body != null)
        context.ScheduleActivity(this.Body);
}

Observe também que o tipo foi designado de acordo com o padrão criar-definir-usar. A sintaxe XAML é baseada nesse padrão para designar tipos, onde o tipo tem um construtor padrão público e propriedades de leitura/gravação públicas. Isso significa que o tipo será amigável para a serialização XAML.

O trecho de código a seguir mostra como usar essa atividade. Neste exemplo, se o dia atual for sábado, você escreverá a cadeia de caracteres “Rest!” no console:

var act = new ExecuteIfTrue
{
  Condition = new InArgument<bool>(c => DateTime.Now.DayOfWeek == DayOfWeek.Tuesday),
  Body = new WriteLine { Text = "Rest!" }
};

WorkflowInvoker.Invoke(act);

A primeira atividade de fluxo de controle foi criada em 15 linhas de código. Mas não se engane com a simplicidade desse código, na verdade, é uma atividade de fluxo de controle totalmente funcional!

Agendando vários filhos

O próximo desafio é escrever uma versão simplificada da atividade Sequence. O objetivo deste exercício é aprender a escrever uma atividade de fluxo de controle que agende várias atividades filho e execute em vários episódios. Essa atividade é quase equivalente funcionalmente à Sequence fornecida no produto.

Essa atividade deve funcionar da seguinte maneira:

  • O usuário da atividade deve fornecer uma coleção de filhos a serem executados sequencialmente por meio da propriedade Activities.
  • Em tempo de execução:
    • A atividade contém uma variável interna com o índice do último item da coleção que foi executado.
    • Se houver itens na coleção de filhos, agende o primeiro filho.
    • Quando o filho for concluído:
      • Incremente o índice do último item executado.
      • Se o índice ainda estiver dentro dos limites da coleção de filhos, agende o próximo filho.
      • Repita.

O código na Figura 2 implementa uma atividade SimpleSequence que se comporta exatamente como descrito.

Figura 2 A atividade SimpleSequence

public class SimpleSequence : NativeActivity
{
  // Child activities collection
  Collection<Activity> activities;
  Collection<Variable> variables;

  // Pointer to the current item in the collection being executed
  Variable<int> current = new Variable<int>() { Default = 0 };
     
  public SimpleSequence() { }

  // Collection of children to be executed sequentially by SimpleSequence
  public Collection<Activity> Activities
  {
    get
    {
      if (this.activities == null)
        this.activities = new Collection<Activity>();

      return this.activities;
    }
  }

  public Collection<Variable> Variables 
  { 
    get 
    {
      if (this.variables == null)
        this.variables = new Collection<Variable>();

      return this.variables; 
    } 
  }

  protected override void CacheMetadata(NativeActivityMetadata metadata)
  {
    metadata.SetChildrenCollection(this.activities);
    metadata.SetVariablesCollection(this.variables);
    metadata.AddImplementationVariable(this.current);
  }

  protected override void Execute(NativeActivityContext context)
  {
    // Schedule the first activity
    if (this.Activities.Count > 0)
      context.ScheduleActivity(this.Activities[0], this.OnChildCompleted);
  }

  void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
  {
    // Calculate the index of the next activity to scheduled
    int currentExecutingActivity = this.current.Get(context);
    int next = currentExecutingActivity + 1;

    // If index within boundaries...
    if (next < this.Activities.Count)
    {
      // Schedule the next activity
      context.ScheduleActivity(this.Activities[next], this.OnChildCompleted);

      // Store the index in the collection of the activity executing
      this.current.Set(context, next);
    }
  }
}

Mais uma vez, uma atividade de fluxo de controle totalmente funcional foi escrita em apenas algumas linhas de código, neste caso, em 50 linhas. O código é simples, mas introduz alguns conceitos interessantes.

SimpleSequence executa uma coleção de atividades filho em ordem sequencial, portanto ela precisa agendar outras atividades. Portanto, ela é derivada da NativeActivity pois precisa interagir com o tempo de execução para agendar filhos.

A próxima etapa é definir a assinatura pública de SimpleSequence. Neste caso, ela consiste em uma coleção de atividades (do tipo Collection<Activity>) expostas por meio da propriedade Activities e em uma coleção de variáveis (do tipo Collection<Variable>) expostas por meio da propriedade Variables. As variáveis permitem compartilhar dados entre todas as atividades filho. Observe que essas propriedades têm apenas “getters” que expõem as coleções por meio de uma abordagem de “instanciação lenta” (consulte a Figura 3), portanto, o acesso a essas propriedades nunca resulta em uma referência nula. Isso torna essas propriedades compatíveis com o padrão de criar-definir-usar.

Figura 3 Uma abordagem de instanciação lenta

public Collection<Activity> Activities
{
  get
  {
    if (this.activities == null)
      this.activities = new Collection<Activity>();

    return this.activities;
  }
}

public Collection<Variable> Variables 
{ 
  get 
  {
    if (this.variables == null)
      this.variables = new Collection<Variable>();

    return this.variables; 
  } 
}

Há um membro privado na classe que não faz parte da assinatura: uma Variable<int> denominada “current” que contém o índice da atividade que está sendo executada:

// Pointer to the current item in the collection being executed
Variable<int> current = new Variable<int>() { Default = 0 };

Como essas informações fazem parte do estado da execução interna de SimpleSequence, você deseja que elas permaneçam privadas e que não sejam expostas aos usuários de SimpleSequence. Você também deseja que elas sejam salvas e restauradas quando a atividade for persistida. Para isso, você usa uma ImplementationVariable.

A variáveis de implementação são variáveis que são internas a uma atividade. Elas têm o objetivo de serem consumidas pelo autor da atividade, não pelo usuário da atividade. As variáveis de implementação são persistidas, quando a atividade é persistida, e restauradas, quando a atividade é recarregada, sem necessidade de nenhum trabalho de sua parte. Para tornar isso claro, e para continuar com o exemplo de Sequence, se uma instância de SimpleSequence for persistida, quando acordar, ela se “lembrará” do índice da última atividade que foi executada.

O tempo de execução do WF não pode saber automaticamente sobre variáveis de implementação. Se você desejar usar uma ImplementationVariable em uma atividade, será necessário informar explicitamente ao tempo de execução do WF. Isso é feito durante a execução do método CacheMetadata.

Apesar de seu nome assustador, o CacheMetadata não é assim tão difícil. Conceitualmente, ele é realmente simples: É o método onde uma atividade “apresenta-se” para o tempo de execução. Pense na atividade If por um momento. No CacheMetadata essa atividade diria: “Olá, eu sou a atividade If e tenho um argumento de entrada denominado Condition e dois filhos: Then e Else.” No caso de SimpleSequence, SimpleSequence está dizendo: “Olá, eu sou a SimpleSequence e tenho uma coleção de atividades filho, uma coleção de variáveis e uma variável de implementação.” Não há nada além disso no código de SimpleSequence para o CacheMetadata:

protected override void CacheMetadata(NativeActivityMetadata metadata)
{
  metadata.SetChildrenCollection(this.activities);
  metadata.SetVariablesCollection(this.variables);
  metadata.AddImplementationVariable(this.current);
}

A implementação padrão de CacheMetadata usa reflexão para obter esses dados da atividade. No exemplo ExecuteIfTrue, não implementei CacheMetadata e confiei na implementação padrão para refletir em membros públicos. Por outro lado, para SimpleSequence, precisei implementá-lo porque a implementação padrão não pode “adivinhar” meu desejo de usar variáveis de implementação.

A próxima parte interessante do código nessa atividade é o método Execute. Neste caso, se houver atividades na coleção, você informará ao tempo de execução do WF: “Execute a primeira atividade da coleção de atividades e, depois de concluir, invoque o método OnChildCompleted.” Você diz isso em termos do WF com o uso de NativeActivityContext.ScheduleActivity. Observe que ao agendar uma atividade, você fornece um segundo argumento que é um CompletionCallback. Em termos simples, esse é um método que será chamado quando a execução da atividade for concluída. Mais uma vez, é importante lembrar-se da diferença entre agendamento e execução. O CompletionCallback não será chamado quando a atividade for agendada, ele será chamado quando execução da atividade agendada tiver sido concluída:

protected override void Execute(NativeActivityContext context)
{
  // Schedule the first activity
  if (this.Activities.Count > 0)
    context.ScheduleActivity(this.Activities[0], this.OnChildCompleted);
}

O método OnChildCompleted é a parte mais interessante dessa atividade de uma perspectiva de aprendizado e, na verdade, é o motivo principal de eu ter incluído o SimpleSequence neste artigo. Esse método obtém e agenda a próxima atividade da coleção. Quando o próximo filho é agendado, um CompletionCallback é fornecido, que, neste caso, aponta para esse mesmo método. Portanto, quando um filho é concluído, esse método é executado novamente para procurar e executar o próximo filho. Claramente, a execução está ocorrendo em pulsos ou em episódios. Como os fluxos de trabalho podem ser persistidos e descarregados da memória, pode haver uma grande diferença de tempo entre os pulsos de execução. Além do mais, esses pulsos podem ser executados em diferentes threads, processos ou mesmo computadores (uma vez que as instâncias persistidas de um fluxo de trabalho podem ser recarregadas em um processo ou computador diferente). Aprender como programar para vários pulsos de execução é um dos maiores desafios para se tornar um especialista na criação de atividades de fluxo de controle:

void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
{
  // Calculate the index of the next activity to scheduled
  int currentExecutingActivity = this.current.Get(context);
  int next = currentExecutingActivity + 1;

  // If index within boundaries...
  if (next < this.Activities.Count)
  {
    // Schedule the next activity
    context.ScheduleActivity(this.Activities[next], this.OnChildCompleted);

    // Store the index in the collection of the activity executing
    this.current.Set(context, next);
  }
}

O trecho de código a seguir mostra como usar essa atividade. Neste exemplo, estou escrevendo três cadeias de caracteres no console ("Hello," "Workflow" e "!"):

var act = new SimpleSequence()
{
  Activities = 
  {
    new WriteLine { Text = "Hello" },
    new WriteLine { Text = "Workflow" },
    new WriteLine { Text = "!" }
  }
};

WorkflowInvoker.Invoke(act);

Criei minha própria SimpleSequence! Agora está na hora de avançar para o próximo desafio.

Implementando um novo padrão de fluxo de controle

Em seguida, criarei uma atividade complexa de fluxo de controle. Conforme mencionei anteriormente, você não está limitado às atividades de fluxo de controle fornecidas no WF 4. Esta seção demonstra como criar sua própria atividade de fluxo de controle para dar suporte a um padrão de fluxo de controle que não tem suporte pronto para uso pelo WF 4.

A nova atividade de fluxo de controle será chamada Series. Sua meta é simples: fornecer uma Sequence com suporte para GoTos, onde a próxima atividade a ser executada pode ser manipulada explicitamente de dentro do fluxo de trabalho (por meio da atividade GoTo) ou no host (com a continuação de um indicador bem conhecido)

Para implementar esse novo fluxo de trabalho, preciso criar duas atividades: Series, uma atividade composta que contém uma coleção de atividades e as executa sequencialmente (mas que permite saltar para qualquer item da sequência), e GoTo, uma atividade folha que usarei dentro de Series para modelar explicitamente os saltos.

Para recapitular, enumerarei as metas e os requisitos para a atividade de controle personalizada:

  1. É uma sequência de atividades.
  2. Pode conter atividades GoTo (em qualquer profundidade), que alteram o ponto de execução de qualquer filho direto de Series.
  3. Pode receber mensagens externas de GoTo (por exemplo, de um usuário), que podem alterar o ponto de execução para qualquer filho direto de Series.

Começarei implementando a atividade Series. Esta é a semântica de execução em termos simples:

  • O usuário da atividade deve fornecer uma coleção de filhos a serem executados sequencialmente por meio da propriedade Activities.
  • No método de execução:
    • Crie um indicador para GoTo de forma que fique disponível para atividades filho.
    • A atividade contém uma variável interna com a instância da atividade que está sendo executada.
    • Se houver itens na coleção de filhos, agende o primeiro filho.
    • Quando o filho for concluído:
      • Procure a atividade concluída na coleção de atividades.
      • Incremente o índice do último item executado.
      • Se o índice ainda estiver dentro dos limites da coleção de filhos, agende o próximo filho.
      • Repita.
  • Se o indicador GoTo for continuado:
    • Obtenha o nome da atividade para a qual desejamos ir.
    • Localize essa atividade na coleção de atividades.
    • Agende a atividade de destino no conjunto para execução e registre um retorno de chamada da conclusão que agendará a próxima atividade.
    • Cancele a atividade que está em execução no momento.
    • Armazene a atividade que está sendo executada na variável “current”.

O exemplo de código da Figura 4 mostra a implementação de uma atividade Series que se comporta exatamente como descrito.

Figura 4 A atividade Series

public class Series : NativeActivity
{
  internal static readonly string GotoPropertyName = 
    "Microsoft.Samples.CustomControlFlow.Series.Goto";

  // Child activities and variables collections
  Collection<Activity> activities;
  Collection<Variable> variables;

  // Activity instance that is currently being executed
  Variable<ActivityInstance> current = new Variable<ActivityInstance>();
 
  // For externally initiated goto's; optional
  public InArgument<string> BookmarkName { get; set; }

  public Series() { }

  public Collection<Activity> Activities 
  { 
    get {
      if (this.activities == null)
        this.activities = new Collection<Activity>();
    
      return this.activities; 
    } 
  }

  public Collection<Variable> Variables 
  { 
    get {
      if (this.variables == null)
        this.variables = new Collection<Variable>();

      return this.variables; 
    } 
  }
    
  protected override void CacheMetadata(NativeActivityMetadata metadata)
  {                        
    metadata.SetVariablesCollection(this.Variables);
    metadata.SetChildrenCollection(this.Activities);
    metadata.AddImplementationVariable(this.current);
    metadata.AddArgument(new RuntimeArgument("BookmarkName", typeof(string), 
                                              ArgumentDirection.In));
  }

  protected override bool CanInduceIdle { get { return true; } }

  protected override void Execute(NativeActivityContext context)
  {
    // If there activities in the collection...
    if (this.Activities.Count > 0)
    {
      // Create a bookmark for signaling the GoTo
      Bookmark internalBookmark = context.CreateBookmark(this.Goto,
                BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);

      // Save the name of the bookmark as an execution property
      context.Properties.Add(GotoPropertyName, internalBookmark);

      // Schedule the first item in the list and save the resulting 
      // ActivityInstance in the "current" implementation variable
      this.current.Set(context, context.ScheduleActivity(this.Activities[0], 
                                this.OnChildCompleted));

      // Create a bookmark for external (host) resumption
      if (this.BookmarkName.Get(context) != null)
        context.CreateBookmark(this.BookmarkName.Get(context), this.Goto,
            BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);
    }
  }

  void Goto(NativeActivityContext context, Bookmark b, object obj)
  {
    // Get the name of the activity to go to
    string targetActivityName = obj as string;

    // Find the activity to go to in the children list
    Activity targetActivity = this.Activities
                                  .Where<Activity>(a =>  
                                         a.DisplayName.Equals(targetActivityName))
                                  .Single();

    // Schedule the activity 
    ActivityInstance instance = context.ScheduleActivity(targetActivity, 
                                                         this.OnChildCompleted);

    // Cancel the activity that is currently executing
    context.CancelChild(this.current.Get(context));

    // Set the activity that is executing now as the current
    this.current.Set(context, instance);
  }

  void OnChildCompleted(NativeActivityContext context, ActivityInstance completed)
  {
    // This callback also executes when cancelled child activities complete 
    if (completed.State == ActivityInstanceState.Closed)
    {
      // Find the next activity and execute it
      int completedActivityIndex = this.Activities.IndexOf(completed.Activity);
      int next = completedActivityIndex + 1;

      if (next < this.Activities.Count)
          this.current.Set(context, 
                           context.ScheduleActivity(this.Activities[next],
                           this.OnChildCompleted));
    }
  }
}

Parte desse código será familiar em relação aos exemplos anteriores. Discutirei a implementação dessa atividade.

Series é derivada de NativeActivity pois precisa interagir com o tempo de execução do WF para agendar atividades filho, criar indicadores, cancelar filhos e usar propriedades de execução.

Como anteriormente, a próxima etapa é definir a assinatura pública de Series. Como em SimpleSequence, há propriedades das coleções de Atividades e Variáveis. Há também um argumento de entrada de cadeia de caracteres denominado BookmarkName (do tipo InArgument<string>), com o nome do indicador a ser criado para continuidade do host. Mais uma vez, estou seguindo o padrão criar-definir-usar no tipo da atividade.

A atividade Series tem um membro privado denominado “current” que contém a ActivityInstance que está sendo executada, em vez de apenas um ponteiro para um item em uma coleção, como em SimpleSequence. Por que current é uma Variable<ActivityInstance> e não uma Variable<int>? Porque, mais tarde, preciso controlar o filho que está em execução no momento nesta atividade durante o método GoTo. Explicarei os detalhes reais mais tarde. Agora, o importante é entender que terei uma variável de implementação que mantém a instância da atividade que está sendo executada:

Variable<ActivityInstance> current = new Variable<ActivityInstance>();

Em CacheMetadata você fornece informações de tempo de execução sobre sua atividade: os filhos e coleções de variáveis, a variável de implementação com a instância da atividade atual e o argumento do nome do indicador. A única diferença do exemplo anterior é que estou registrando o argumento de entrada BookmarkName manualmente dentro do tempo de execução do WF, com a adição de uma nova instância de RuntimeArgument aos metadados da atividade:

protected override void CacheMetadata(NativeActivityMetadata metadata)
{                        
  metadata.SetVariablesCollection(this.Variables);
  metadata.SetChildrenCollection(this.Activities);
  metadata.AddImplementationVariable(this.current);
  metadata.AddArgument(new RuntimeArgument("BookmarkName",  
                                           typeof(string), ArgumentDirection.In));
}

A próxima novidade é a sobrecarga da propriedade CanInduceIdle. Essa sobrecarga é apenas mais metadados que a atividade fornece para o tempo de execução do WF. Quando essa propriedade retorna true, estou informando ao tempo de execução que essa atividade pode fazer com que o fluxo de trabalho se torne ocioso. Preciso substituir essa propriedade e retornar true para atividades que criam indicadores, uma vez que elas farão com que o fluxo de trabalho se torne ocioso ao aguardar por sua continuação. O valor padrão dessa propriedade é false. Se essa propriedade retornar false e criarmos um indicador, terei uma exceção InvalidOperationException ao executar a atividade:

protected override bool CanInduceIdle { get { return true; } }

As coisas se tornam mais interessantes no método Execute, onde crio um indicador (internalBookmark) e o armazeno em uma propriedade de execução. Antes de continuar, no entanto, permita-se introduzir indicadores e propriedades de execução.

Indicadores são o mecanismo pelo qual uma atividade pode aguardar passivamente para ser continuada. Quando uma atividade quer “bloquear” a pendência de um determinado evento, ela registra um indicador e retorna um status de execução de continuação. Isso sinaliza para o tempo de execução que, embora a execução da atividade não esteja concluída, ela não tem mais trabalho para executar como parte do item de trabalho atual. Ao usar indicadores, você pode criar suas atividades por meio de uma forma de execução reativa: quando o indicador é criado, a atividade rende, e quando o indicador é continuado, um bloco de código (o retorno de chamada da continuação do indicador) é invocado como reação à continuidade do indicador.

Ao contrário de programas direcionados diretamente para o CLR, os programas de fluxo de trabalho são árvores em escopo hierárquico que executam em um ambiente que desconhece threads. Isso sugere que os mecanismos de TLS (armazenamento local de threads) não podem ser utilizados diretamente para determinar qual contexto está em um escopo de um determinado item de trabalho. O contexto de execução do fluxo de trabalho introduz propriedades de execução para o ambiente de uma atividade, de forma que uma atividade pode declarar propriedades que estão no escopo de sua subárvore e compartilhá-las com seus filhos. Como resultado, uma atividade pode compartilhar dados com seus descendentes por meio dessas propriedades.

Agora que você tem um entendimento dos indicadores e propriedades de execução, vamos voltar ao código. O que estou fazendo no início do método Execute é criar um indicador (com o uso de context.CreateBookmark) e salvá-lo em uma propriedade de execução (como uso de context.Properties.Add). Esse indicador é um indicador de várias continuações, o que significa que ele pode ser continuado várias vezes e estará disponível enquanto sua atividade pai estiver em estado de execução. Ele também é NonBlocking, portanto, não impedirá que a atividade seja concluída quando terminar seu trabalho. Quando esse indicador for continuado, o método GoTo será chamado porque forneci um BookmarkCompletionCallback para CreateBookmark (o primeiro parâmetro). O motivo de salvá-lo em uma propriedade de execução é disponibilizá-lo para todas as atividades filho. Mais tarde, você verá como a atividade GoTo usa esse indicador. Observe que as propriedades de execução têm nomes. Como esse nome é uma cadeia de caracteres, defini uma constante (GotoPropertyName) com o nome da propriedade da atividade. Esse nome segue uma abordagem de nome totalmente qualificado. Esta é a prática recomendada:

internal static readonly string GotoPropertyName = 
                                "Microsoft.Samples.CustomControlFlow.Series.Goto";

...
...

// Create a bookmark for signaling the GoTo
Bookmark internalBookmark = context.CreateBookmark(this.Goto,                                         
                       BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);

// Save the name of the bookmark as an execution property
context.Properties.Add(GotoPropertyName, internalBookmark);

Depois de declarar o indicador, estou pronto para agendar minha primeira atividade. Já estou familiarizado, porque fiz isso em minhas atividades anteriores. Agendarei a primeira atividade da coleção e instruirei o tempo de execução a invocar o método OnChildCompleted quando a atividade for concluída (como fiz em SimpleSequence). Context.ScheduleActivity retorna uma ActivityInstance que representa uma instância de uma atividade que está sendo executada, que atribuo a nossa variável de implementação atual. Deixe-me esclarecer isso um pouco. A atividade é a definição, como uma classe. A ActivityInstance é a instância real, como um objeto. Podemos ter várias ActivityInstances da mesma Atividade:

// Schedule the first item in the list and save the resulting 
// ActivityInstance in the "current" implementation variable
this.current.Set(context, context.ScheduleActivity(this.Activities[0],  
                                                   this.OnChildCompleted));

Finalmente, criaremos um indicador que pode ser usado pelo host para saltar para qualquer atividade dentro da série. A mecânica disso é simples: como o host sabe o nome do indicador, ele pode continuá-lo com um salto para qualquer atividade dentro de Series:

// Create a bookmark for external (host) resumption
 if (this.BookmarkName.Get(context) != null)
     context.CreateBookmark(this.BookmarkName.Get(context), this.Goto,
                           BookmarkOptions.MultipleResume | BookmarkOptions.NonBlocking);

O método OnChildCompleted deve ser simples agora, uma vez que é muito semelhante ao método em SimpleSequence: Eu procuro o próximo elemento na coleção de atividades e o agendo. A principal diferença é que agendarei a próxima atividade apenas se a atividade atual tiver concluído sua execução com êxito (isto é, tiver atingido o estado de fechada e não tiver sido cancelada ou apresentado falha).

O método GoTo é indiscutivelmente o mais interessante. Esse é o método que é executado como o resultado do indicador GoTo que está sendo continuado. Ele recebe alguns dados como entrada, que são passados quando o indicador é continuado. Neste caso, os dados são o nome da atividade para a qual desejamos ir.

void Goto(NativeActivityContext context, Bookmark b, object data)
{
  // Get the name of the activity to go to
  string targetActivityName = data as string;
       
  ...
 }

O nome da atividade de destino é a propriedade DisplayName da atividade. Procuro a definição da atividade solicitada na coleção de “atividades”. Depois de localizar a atividade solicitada, agendo-a, indicando que quando a atividade for concluída, o método OnChildCompleted deverá ser executado:

// Find the activity to go to in the children list
Activity targetActivity = this.Activities
                              .Where<Activity>(a =>  
                                       a.DisplayName.Equals(targetActivityName))
                              .Single();
// Schedule the activity 
ActivityInstance instance = context.ScheduleActivity(targetActivity, 
                                                     this.OnChildCompleted);

Em seguida, cancelo a instância da atividade que está sendo executada no momento e defino a atividade atual que está sendo executada como a ActivityInstance agendada na etapa anterior. Para essas duas tarefas, uso a variável “current”. Primeiro, passo-a como um parâmetro do método CancelChild de NativeActivityContext e, em seguida, atualizo seu valor com a ActivityInstance que foi agendada no bloco de código anterior:

// Cancel the activity that is currently executing
context.CancelChild(this.current.Get(context));

// Set the activity that is executing now as the current
this.current.Set(context, instance);

A atividade GoTo

A atividade GoTo pode ser usada apenas dentro de uma atividade Series para saltar para uma atividade em sua coleção de atividades. Isso é semelhante a uma instrução GoTo em um programa imperativo. A maneira como ele funciona é muito simples: Ela continua o indicador GoTo criado pela atividade Series na qual está contida, indicando o nome da atividade para a qual desejamos ir. Quando o indicador é continuado, Series saltará para a atividade indicada.

Esta é uma descrição simples da semântica de execução:

  • O usuário da atividade deve fornecer uma cadeia de caracteres TargetActivityName. Esse argumento é necessário.
  • Em tempo de execução:
    • A atividade GoTo localizará o indicador “GoTo” criado pela atividade Series.
    • Se o indicador for localizado, ele continuará, passando o TargetActivityName.
    • Ele criará um indicador de sincronização, de forma que a atividade não é concluída.
      • Ela será cancelada por Series.

O código da Figura 5 mostra a implementação de uma atividade GoTo que se comporta exatamente como descrito.

Figura 5 A atividade GoTo

public class GoTo : NativeActivity
{
  public GoTo() 
  { }
       
  [RequiredArgument]
  public InArgument<string> TargetActivityName { get; set; }

  protected override bool CanInduceIdle { get { return true; } }
    
  protected override void Execute(NativeActivityContext context)
  {
    // Get the bookmark created by the parent Series
    Bookmark bookmark = context.Properties.Find(Series.GotoPropertyName) as Bookmark;

    // Resume the bookmark passing the target activity name
    context.ResumeBookmark(bookmark, this.TargetActivityName.Get(context));

    // Create a bookmark to leave this activity idle waiting when it does
    // not have any further work to do. Series will cancel this activity 
    // in its GoTo method
    context.CreateBookmark("SyncBookmark");
  }

}

GoTo é derivada de NativeActivity pois precisa interagir com o tempo de execução do WF para criar e continuar indicadores e usar propriedades de execução. Sua assinatura pública consiste no argumento de entrada da cadeia de caracteres TargetActivityName que contém o nome da atividade para a qual desejamos saltar. Eu decorei esse argumento com o atributo RequiredArgument, o que significa que os serviços de validação do WF irão impor que ele seja definido com uma expressão.

Eu conto com a implementação padrão de CacheMetadata que é refletida na superfície pública da atividade para localizar e registrar metadados em tempo de execução.

A parte mais importante está no método Execute. Primeiro, procuro o indicador criado pela atividade Series pai. Como o indicador foi armazenado como uma propriedade de execução, procuro-o em context.Properties. Quando localizo esse indicador, eu o continuo, passando o TargetActivityName como os dados de entrada. Essa continuação do indicador resultará na invocação do método Series.Goto (porque ele é o retorno de chamada do indicador fornecido quando o indicador foi criado). Esse método procurará e agendará a próxima atividade da coleção e cancelará a atividade que está em execução no momento.

// Get the bookmark created by the parent Series
Bookmark bookmark = context.Properties.Find(Series.GotoPropertyName) as Bookmark; 

// Resume the bookmark passing the target activity name
context.ResumeBookmark(bookmark, this.TargetActivityName.Get(context));

A linha final do código é a mais complicada: criar um indicador de sincronização que manterá a atividade GoTo em execução. Portanto, quando o método GoTo.Execute for concluído, essa atividade ainda estará em estado de execução, esperando um estímulo para continuar o indicador. Quando discuti o código de Series.Goto, mencionei que ele cancelou a atividade que estava sendo executada. Neste caso, Series.Goto está realmente cancelando uma instância da atividade Goto que está esperando que esse indicador seja continuado.

Para explicar em mais detalhes: A instância da atividade GoTo foi agendada pela Series. Quando essa atividade é concluída, o retorno de chamada de conclusão de Series (OnChildCompleted) procura e agenda a próxima atividade na coleção Series.Activities. Neste caso, não desejo agendar a próxima atividade, desejo agendar a atividade referenciada por TargetActivityName. Esse indicador permite isso porque mantém a atividade GoTo em um estado de execução enquanto a atividade de destino está sendo agendada. Quando GoTo é cancelada, não há nenhuma ação no retorno de chamada de Series.OnChildCompleted porque ele agenda a próxima atividade apenas se o estado de conclusão for Fechada (e, neste caso, o estado é Cancelada):

// Create a bookmark to leave this activity idle waiting when it does
// not have any further work to do. Series will cancel this activity 
// in its GoTo method
context.CreateBookmark("SyncBookmark");

A Figura 6 mostra um exemplo que usa essa atividade. Neste caso, estou criando um loopback para um estado anterior de acordo com o valor de uma variável. Esse é um exemplo simples para ilustrar o uso básico de Series, mas essa atividade pode ser usada para implementar cenários de negócios complexos do mundo real onde você precisa ignorar, refazer ou saltar para etapas em um processo sequencial.

Figura 6 Usando GoTo em uma série

var counter = new Variable<int>();

var act = new Series
{
  Variables = { counter},
  Activities =
  {
    new WriteLine 
    {
      DisplayName = "Start",
      Text = "Step 1"
    },
    new WriteLine
    {
      DisplayName = "First Step",
      Text = "Step 2"
    },
    new Assign<int>
    {
      To = counter,
      Value = new InArgument<int>(c => counter.Get(c) + 1)
    },
    new If 
    {
      Condition = new InArgument<bool>(c => counter.Get(c) == 3),
      Then = new WriteLine
      {
        Text = "Step 3"
      },
      Else = new GoTo { TargetActivityName = "First Step" }
    },
    new WriteLine 
    {
      Text = "The end!"
    }
  }
};

WorkflowInvoker.Invoke(act);

Referências

Windows Workflow Foundation 4 Developer Center
msdn.microsoft.com/netframework/aa663328

Endpoint.tv: Práticas recomendadas para criação de atividades
channel9.msdn.com/shows/Endpoint/endpointtv-Workflow-and-Custom-Activities-Best-Practices-Part-1/

Criando e implementando atividades personalizadas
msdn.microsoft.com/library/dd489425

Classe ActivityInstance
msdn.microsoft.com/library/system.activities.activityinstance

Classe RuntimeArgument
msdn.microsoft.com/library/dd454495

Siga o fluxo

Neste artigo, apresentei os aspectos gerais da criação de atividades de fluxo de controle personalizadas. No WF 4, o espectro de fluxos de controle não é fixo. A criação de atividades personalizadas foi drasticamente simplificada. Se as atividades prontas para uso fornecidas não atenderem às suas necessidades, você poderá criar facilmente sua própria atividade. Neste artigo, comecei com uma atividade simples de fluxo de controle e continuei com a implementação de uma atividade personalizada de fluxo de controle que adiciona nova semântica de execução ao WF 4. Se desejar mais informações, um Community Technology Preview para State Machine está disponível no CodePlex com o código-fonte completo. Você também encontrará uma série de vídeos no Channel 9 sobre as práticas recomendadas para criação de atividades. Com a criação de suas próprias atividades personalizadas, é possível expressar qualquer padrão de fluxo de controle no WF e acomodar o WF para as particularidades de seu problema.

Leon Welicki é gerente de programa da equipe do Windows Workflow Foundation (WF) da Microsoft, onde trabalha com o tempo de execução do WF. Antes de se juntar à Microsoft, ele trabalhava como arquiteto líder e gerente de dispositivos de uma grande empresa de telecomunicação espanhola e como professor associado visitante na faculdade de graduação de ciências da computação na Pontifical University de Salamanca em Madrid.

Agradecemos aos seguintes especialistas técnicos pela revisão deste artigo: Joe Clancy, Dan Glick, Rajesh Sampath, Bob Schmidt e Isaac Yuen