Introduzione a Workflow Foundation (Parte 1)

di Corrado Cavalli - Microsoft MVP

In questa pagina

                Introduzione                  Introduzione
Per iniziare Per iniziare
               Un primo esempio                 Un primo esempio
Interazione tra workflow e applicazione Interazione tra workflow e applicazione
                Base Activity Library                  Base Activity Library
                Conclusione                  Conclusione
Riferimenti Riferimenti

Introduzione

Se state leggendo quest’articolo probabilmente siete degli sviluppatori e quindi sicuramente conoscete già il concetto di workflow considerato che è uno dei primi concetti che vengono spiegati quando si entra nel “magico” mondo della programmazione. Iniziamo analizzando i passi che descrivono un’ipotetica vendita di un titolo azionario:

  1. Immissione ordine a un determinato prezzo

  2. Attesa dell’incrocio domanda offerta, con eventuale possibilità di cancellazione

  3. Esecuzione ordine

  4. Conferma ordine

I passi elencati descrivono in questo caso un workflow sequenziale poiché l’esecuzione scorre linearmente dal punto uno al punto quattro, altri casi potrebbero essere invece descritti da un insieme finito di stati collegati a eventi esterni che regolano lo stato attuale descrivendo quella che viene normalmente conosciuta come macchina a stati.
Ogni qualvolta si ha a che fare con operazioni che, meglio di altre, possono essere rappresentate da un diagramma di flusso, l’utilizzo di Workflow Foundation semplifica il compito dello sviluppatore il quale può concentrarsi sull’implementazione delle funzionalità dimenticandosi gli aspetti di gestione e supporto del flusso stesso.

Per iniziare

Workflow Foundation (WF) è uno dei componenti che assieme a WPF (Windows Presentation Foundation), WCF (Windows Communication Foundation) e Windows Cardspace rappresentano le novità della versione 3.0 di .NET Framework il quale e’ normalmente presente su Windows Vista™ ed e’ disponibile come installazione separata per Windows XP Sp2 e Windows Server 2003.
Volendo utilizzare Visual Studio 2005 come ambiente di sviluppo di applicazioni Workflow Foundation è necessario installare le relative estensioni.

Un primo esempio

Immaginiamo di voler utilizzare Visual Studio 2005 per sviluppare un semplice workflow il quale immetta un ordine di acquisto di un’azione.

Selezioniamo il template Sequential Workflow Console Application il quale produrrà un’applicazione console che consumerà il workflow che stiamo per realizzare.

Il perché della presenza dell’applicazione console deriva dal fatto che Workflow Foundation è un runtime quindi solo dopo che un processo ospite ha caricato e inizializzato il runtime è possibile creare dei workflows.
Workflow Foundation non pone vincoli al tipo di processo dentro il quale può essere ospitato, questo può essere indifferentemente un’applicazione smart-client, una console application (il nostro caso) oppure in ambito web lo stesso processo ASP.NET. Terminata la generazione del progetto, ci troveremo di fronte una superficie all’interno della quale andremo a trascinare l’insieme di operazioni, o per usare la stessa terminologia usata da Workflow Foundation, le varie Attività le quali rappresentano, in maniera più o meno dettagliata l’insieme di azioni da eseguire durante l’esecuzione del workflow (Figura 1)

Figura 1


Dalla tab Windows Workflow presente all’interno della casella strumenti di Visual Studio 2005 selezioniamo e trasciniamo nel designer una Code Activity la quale rappresenta un blocco contenente del codice.

Facendo doppio click sull’attività stessa, oppure indicando il nome di un metodo nella proprietà ExecuteCode e premendo Enter, verrà generato il gestore evento che il motore di Workflow Foundation invocherà durante l’esecuzione della Code Activity.

Figura 2


Inseriamo ora questo semplice codice, il quale non fa altro che chiedere all’utente la conferma d’immissione ordine.

C#

private void codeActivity1_ExecuteCode (object sender, EventArgs e)
{
  Console.WriteLine("Confermi l'immissione dell'ordine?");
  if (Console.ReadKey().KeyChar == 's')
   Console.WriteLine("Ordine immesso.");
  else
   Console.WriteLine("Ordine annullato.");
}

VB

Private Sub codeActivity1_ExecuteCode(ByVal sender As Object, ByVal e As EventArgs)
  Console.WriteLine("Confermi l'immissione dell'ordine?")
  If (Console.ReadKey().KeyChar = "s"c) Then
    Console.WriteLine("Ordine immesso.")
  Else
    Console.WriteLine("Ordine annullato.")
  End If
End Sub

Prima di eseguire l’esempio, diamo un’occhiata al codice che Visual Studio 2005 ha generato nel file program.cs. (o Module1.vb)

C#

static void Main(string[] args)
{
 using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
 {
   AutoResetEvent waitHandle = new AutoResetEvent(false);
   workflowRuntime.WorkflowCompleted += 
     delegate(object sender, WorkflowCompletedEventArgs e) {waitHandle.Set();};
   workflowRuntime.WorkflowTerminated += 
     delegate(object sender, WorkflowTerminatedEventArgs e) {
   Console.WriteLine(e.Exception.Message);
   waitHandle.Set();};
   WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(BuyStock.Workflow1));
   instance.Start();
   waitHandle.WaitOne();
 }
}

VB

Shared WaitHandle As New AutoResetEvent(False)
Shared Sub Main()
  Using workflowRuntime As New WorkflowRuntime()
   AddHandler workflowRuntime.WorkflowCompleted, AddressOf OnWorkflowCompleted
   AddHandler workflowRuntime.WorkflowTerminated, AddressOf OnWorkflowTerminated
   Dim workflowInstance As WorkflowInstance
   workflowInstance = workflowRuntime.CreateWorkflow(GetType(BuyStock .Workflow1))
   workflowInstance.Start()
   WaitHandle.WaitOne()
  End Using
End Sub
Shared Sub OnWorkflowCompleted(ByVal sender As Object, ByVal e As WorkflowCompletedEventArgs)
 WaitHandle.Set()
End Sub
Shared Sub OnWorkflowTerminated(ByVal sender As Object, ByVal e As WorkflowTerminatedEventArgs)
 Console.WriteLine(e.Exception.Message)
 WaitHandle.Set()
End Sub

Come potete notare Visual Studio ha creato un’istanza del runtime che si farà carico di fornire le varie istanze dei workflows che l’applicazione creerà successivamente e ha sottoscritto gli eventi Terminated e Completed che vengono generati rispettivamente quando il Workflow viene terminato (ad esempio a causa di un’eccezione non gestita) e quando il workflow ha completato la propria esecuzione.
In seguito il metodo CreateWorkflow() crea un’istanza del workflow mentre il metodo Start() provoca l’esecuzione delle attività presenti al suo interno, nel nostro semplice caso la Code Activity trascinata in precedenza.

Queste poche righe di codice evidenziano un aspetto molto importante di Workflow Foundation da non sottovalutare ovvero che il workflow viene eseguito in un thread separato ciò spiega la presenza di un oggetto AutoresetEvent il cui compito è quello di evitare la chiusura del processo ospitante fino alla generazione dell’evento Completed o Terminated.
Sebbene molto semplice e di scarsa utilità questo piccolo esempio ci permette di apprezzare alcune peculiarità del designer presente in Workflow Foundation, infatti non solo è possibile creare l’intero workflow in modo grafico ma le varie opzioni di debugging sono accessibili direttamente dal designer, basta infatti selezionare una qualsiasi attività e premere F9 per impostare sui di essa un punto di interruzione.

A ogni attività sono associate delle regole di validazione che impediscono la compilazione del workflow in assenza dei requisiti minimi per un suo corretto funzionamento: ad esempio non è possibile compilare un workflow contenente delle Code Activities senza l’evento ExecuteCode opportunamente associato. Un errore di validazione è indicato da un punto esclamativo a fianco dell’attività e muovendosi col mouse sopra il punto esclamativo è anche possibile capire qual è la causa della segnalazione (figura 3)

Figura 3

Volendo è possibile utilizzare un’altra tipologia di workflow indicata tra i vari templates con With code separation. Questo tipo di workflow è descritto da un file.xoml contente la descrizione del workflow in linguaggio XAML e da un file.cs (o .vb) contente il codice da eseguire. Il workflow creato in precedenza potrebbe perciò essere descritto in questo modo:

<SequentialWorkflowActivity x:Class="BuyStock.Workflow1" x:Name="Workflow1" 
 <CodeActivity x:Name="codeActivity1" ExecuteCode="codeActivity1_ExecuteCode" />
</SequentialWorkflowActivity>

L’impiego di XAML rende la struttura del workflow indipendente dal linguaggio e semplifica notevolmente la creazione automatica di workflows da parte di tools automatici.

Interazione tra workflow e applicazione

Difficilmente un workflow potrà vivere in totale isolamento dall’applicazione ospitante, vediamo perciò come far comunicare i due mondi partendo dal caso più semplice: il passaggio e successivo recupero di parametri al workflow.

Supponiamo di voler passare in ingresso all’esempio precedente il nome dell’azione, la relativa quantità e di voler recuperare al termine l’esito dell’operazione.
Modifichiamo l’esempio aggiungendo tre proprietà publiche Quantity,StockName, OrderProcessed e sostituendo il contenuto del metodo ExecuteCode.

C#

public int Quantity
{
 get { return _quantity; }
 set { _quantity = value; }
}

public string StockName
{
 get { return _stockName; }
 set { _stockName = value; }
}

public bool OrderProcessed
{
 get { return _orderProcessed; }
 internal set { _orderProcessed = value; }
}

private void codeActivity1_ExecuteCode (object sender, EventArgs e)
{
 Console.WriteLine("Confermi la vendita di {0} azioni {1}?",   this.Quantity,this.StockName);
 this.OrderProcessed = (Console.ReadKey().KeyChar == 's');
}

VB

Public Property Quantity() As Integer
Get
  Return _quantity
End Get
Set(ByVal value As Integer)
  _quantity = value
End Set
End Property

Public Property StockName() As String
 Get
  Return _Stockname
 End Get
 Set(ByVal value As String)
   _Stockname = value
 End Set
End Property

Public Property OrderProcessed() As Boolean
 Get
  Return _orderProcessed
 End Get
 Set(ByVal value As Boolean)
  _orderProcessed = value
 End Set
End Property
Private Sub codeActivity1_ExecuteCode(ByVal sender As Object, ByVal e As EventArgs)
 Console.WriteLine("Confermi la vendita di {0} azioni {1}?", Me.Quantity, Me.StockName)
 Me.OrderProcessed = (Console.ReadKey().KeyChar = "s"c)
End Sub

Il passaggio dei parametri avviene creando un Dictionary generico e passandolo al metodo CreateWorkflow()

C#

Dictionary<string, object> parameters = new Dictionary<string, object>();
parameters.Add("StockName", "Acme");
parameters.Add("Quantity", 42);
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(BuyStock.Workflow1),parameters);
instance.Start();

VB

Dim parameters As New Dictionary(Of String, Object)
parameters.Add("StockName", "Acme")
parameters.Add("Quantity", 42)
Dim instance As WorkflowInstance = _ workflowRuntime.CreateWorkflow(GetType(BuyStock.Workflow1), parameters)
instance.Start()

La chiave inserita nel dictionary deve coincidere con il nome di una proprietà pubblica esposta dal workflow.
Recuperare l’esito dell’operazione è possibile attraverso la proprietà OutputParameters disponibile all’interno dell’evento Completed come mostrato di seguito:

C#

workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)
{
 bool success = (bool)e.OutputParameters["OrderProcessed"];
 if (success){
  Console.WriteLine("Order processed");
  Console.ReadLine();}
 waitHandle.Set();
};

VB

Public Sub WorkflowCompleted(ByVal sender As Object, ByVal e As WorkflowCompletedEventArgs)
 Dim success As Boolean = TryCast(e.OutputParameters("OrderProcessed"), Boolean)
 If success Then
  Console.WriteLine("Order processed")
  Console.ReadLine()
 End If
 waitHandle.Set()
End Sub

L’utilizzo dei parametri, seppur utile, è comunque limitativo, quello che serve è un vero e proprio meccanismo di interoperabilità tra workflow e applicazione.
Prima di descrivere nel dettaglio il meccanismo è però necessario accennare brevemente a una area importante di Workflow Foundation: I servizi di runtime.
I servizi sono un meccanismo attraverso il quale è possibile “arricchire” il runtime con funzionalità aggiuntive, alcune rese disponibili da Workflow Foundation stesso, altre create appositamente a uso e consumo di un particolare tipo di workflow (nella seconda parte di questa introduzione tratteremo i servizi in maniera più dettagliata.)

Lo scambio di dati tra workflow e applicazione avviene attraverso un servizio denominato Local Comunication Service(LCS) il quale richiede la descrizione del messaggio scambiato tra workflow e applicazione attraverso un’interfaccia, la comunicazione tra applicazione e workflow avviene poi attraverso eventi mentre il dialogo opposto mediante l’invocazione di metodi.
Immaginiamo che nell’esempio precedente l’operazione di vendita sia gestita dall’applicazione che ospita il workflow e che questo debba attendere una conferma dell’avvenuta operazione prima di eseguire le attività successive.
Iniziamo definendo l’interfaccia che descrive il contratto di comunicazione la quale, per essere identificata come tale, deve essere decorata con l’attributo ExternalDataExchangeService.

C#

[ExternalDataExchange]
interface IOrderProcessingService
{
  event EventHandler<ExternalDataEventArgs> OrderProcessed;
  void ProcessOrder ();
}

VB

<ExternalDataEchange()> _
Public Interface IOrderProcessingService
    Event OrderProcessed As EventHandler(Of ExternalDataEventArgs)
    Sub ProcessOrder()
End Interface

Definiamone ora l’implementazione (notate come la classe debba necessariamente essere serializzabile)

C#

[Serializable]
public class OrderProcessingService:IOrderProcessingService
{
  public event EventHandler<ExternalDataEventArgs> OrderProcessed;
  public void ProcessOrder ()
  {
   //L'invocazione di questo metodo indica una richiesta da parte del Workflow
  }
  public void OrderCompleted (Guid instanceId)
  {
   if (OrderProcessed != null)
    OrderProcessed(this, new ExternalDataEventArgs(instanceId));
  }
}

VB

<Serializable()> _
Public Class OrderProcessingService
 Implements IOrderProcessingService

 Public Event OrderProcessed(ByVal sender As Object, ByVal e As ExternalDataEventArgs)
Implements IOrderProcessingService.OrderProcessed

 Public Sub ProcessOrder() Implements IOrderProcessingService.ProcessOrder
  'L'invocazione di questo metodo indica una richiesta da parte del Workflow
 End Sub

 Public Sub OrderCompleted (Guid instanceId)
  RaiseEvent OrderProcessed(Me, New ExternalDataEventArgs(instanceId))
 End Sub
End Class

Per utilizzare il servizio di comunicazione locale all’interno del workflow è necessario ricorrere a due specifiche attività: CallExternalMethod e HandleExternalEvent.
Rimuoviamo la Code Activity usata in precedenza e trasciniamo nel designer le due attività citate (Figura 4) e impostando, come indicato in Figura 5, le relative proprietà InterfaceType, MethodName e Event. A questo punto il workflow è pronto per comunicare con l’applicazione, non ci resta che aggiungere al runtime il servizio di comunicazione locale (LCS).

Figura 4

Figura 5


Il codice per l’aggiunta del servizio è riportato di seguito:

C# (in program.cs)

...
//Aggiungo il LCS...
ExternalDataExchangeService lcsService = new ExternalDataExchangeService();
workflowRuntime.AddService(lcsService);
OrderProcessingService orderService = new OrderProcessingService();
lcsService.AddService(orderService);
...

VB (in Module1.vb)

...
‘Aggiungo il LCS...
Dim lcsSercice As New ExternalDataExchangeService()
workflowRuntime.AddService(lcsService)
Dim oderService As New OrderProcessingService()
lcsService.AddService(orderService)
...

Eseguendo il workflow, l’attività CallExternalMethod invochera’ il metodo ProcessOrder() all’interno del quale potremmo, ad esempio, generare un evento per informare l’applicazione di iniziare la procedura di emissione ordine terminata la quale comunicheremo al workflow l’avvenuta operazione invocando il metodo OrderCompleted() dell’istanza di OrderProcessingService creata in precedenza, passando come parametro il Guid che identifica in maniera univoca il workflow di destinazione.

Esempio:
C#

orderService.OrderCompleted(instance.InstanceId);

VB

orderService.OrderCompleted(instance.InstanceId)

L’evento intercettato dall’attività HandleExternalEvent deve necessariamente avere un argomento di tipo ExternalDataEventArgs o che eredita da esso.

Affronteremo ora una veloce panoramica delle attività di base presenti in Workflow Foundation.

Base Activity Library

Oltre alle già citate CodeActivity, CallExternalMethod e HandleExternalEvent la casella degli strumenti di Visual Studio 2005 rende disponibili un lungo elenco di attività variabile in funzione del tipo di workflow selezionato (Sequenziale o Macchina a stati). Una descrizione approfondita e dettagliata di tutte le attività va oltre lo scopo di quest’articolo quindi ne analizzeremo solo alcune partendo dall’attività IfElse che è l’equivalente dello statement if-then-else che tutti conosciamo utilizzando il workflow visibile in figura 6.

Figura 6 Figura 7

In presenza di un’attività di tipo IfElse, il workflow ne analizza i vari rami partendo da destra verso sinistra ed esegue le attività contenute nei rami se la condizione associata al ramo è vera. La IfElse ci consente di lasciare un ramo privo di condizione a rappresentare l’eventuale else part e nel caso nessuna condizione venga verificata nessun ramo verrà eseguito. È possibile aggiungere un numero variabile di rami, utilizzando la voce Add Branch disponibile nel menu contestuale dell’attività IfElse.

Il concetto di condizione associata è condiviso da più attività presenti nella libreria di base e ricopre un ruolo importante in Workflow Foundation, vediamo quindi di analizzarlo nel dettaglio.
Alle attività che espongono una proprietà Condition è possibile associare due diversi tipi di condizione, entrambi derivanti dalla classe ActivityCondition: CodeCondition o Declarative RuleCondition.

La prima è una condizione programmatica che si traduce nell’esecuzione di un gestore evento all’interno del quale, via codice, va indicato se la condizione è valida o meno mediante la proprietà Result.

Ecco un esempio di Code Condition:

C#

private void BigStockOrder (object sender, ConditionalEventArgs e)
{
 e.Result = this.Quantity > 100000;
}

VB

Private Sub BigStockOrder(ByVal sender As Object, ByVal e As ConditionalEventArgs)
 e.Result = Me.Quantity > 100000
End Sub

La seconda è invece una condizione dichiarativa che è possibile creare velocemente grazie ad un wizard che guida nella creazione della condizione da valutare come mostrato in figura 8.

Figura 8

Sebbene a prima vista l’utilizzo di Code conditions possa sembrare più immediato, le Rule conditions offrono comunque diversi vantaggi. La loro definizione può essere modificata a runtime ed è contenuta in un file .rules esterno che può essere passato come parametro al metodo CreateWorkflow() separando quindi le regole di business dal workflow stesso.

Nel caso si debbano realizzare condizioni complesse sfruttando comunque i vantaggi offerti dalle rules conditions, è possibile utilizzare l’attività Policy (Figura 9) la quale permette la creazione degli insiemi di regole (rule sets) la cui valutazione può modificare dinamicamente il comportamento del Workflow.

Figura 9

L’attività While è l’equivalente diretto di un ciclo while, ovvero ripete l’esecuzione dell’attività contenuta se la condizione associata è vera. Sebbene possa contenere una singola attività, è possibile ovviare a questa limitazione inserendo una Sequence Activity la quale può contenere più attività che verranno eseguite sequenzialmente.

Nel caso si vogliano eseguire contemporaneamente più gruppi di attività legando l’esecuzione del singolo gruppo e quella dell’insieme di gruppi a delle condizioni, l’attività da utilizzare è la ConditionedActivityGroup.

Suspend, Terminate e Throw Activity interrompono la normale esecuzione del workflow anche se in maniera diversa.
Suspend, come è facile intuire, sospende momentaneamente l’esecuzione del workflow;esecuzione che può essere ripresa invocando tramite metodo Resume esposto da WorkflowInstance. Terminate termina in maniera definitiva un workflow e provoca la generazione dell’evento WorkflowTerminated mentre l’attività Throw permette di generare esplicitamente delle eccezioni.

Le eccezioni generate all’interno di un workflow possono essere gestite nella specifica sezione Fault Handlers disponibile selezionando View fault handlers nel menù contestuale dell’attività/workflow selezionata. (Figura 10)

Figura 10
La gestione delle eccezioni avviene trascinando all’interno della FaultHandlerActivity delle attività di tipo FaultHandler,associando a ognuna di esse il tipo di eccezione da trattare attraverso la proprietà FaultType e indicando l’insieme di attività da eseguire al presentarsi di quella particolare eccezione.

L’attività Parallel permette l’esecuzione di più attività parallele, va detto che questo parallelismo è di tipo cooperativo, nel senso che le varie attività in realtà condividono l’unico thread presente nel workflow ed è la presenza di attività di sospensione (es: HandleExternalEvent oppure Delay) nei vari rami dell’attività a consentire il passaggio da un ramo di esecuzione all’altro.

La Parallel Activity è da ritenersi conclusa solamente quando tutti i rami sono stati eseguiti.

Figura 11

L’esempio di figura 11 mostra una attività Parallel che attende l’approvazione da parte del Boss e relativo Manager prima di proseguire.
L’eventuale necessità di sincronizzazione dei vari rami può essere ottenuta racchiudendo le attività da eseguire all’interno di una attività di tipo SyncronizationScope.

L’attività Listen è molto simile alla Parallel con due principali differenze:

  • L’attività è considerata conclusa quando uno qualsiasi dei rami è stato completato.

  • La prima attività contenuta in un ramo deve implementare IEventActivity, deve quindi essere di tipo HandleExternalEvent oppure Delay.

Figura 12

La figura 12 mostra un attività Listen la quale attende un evento di conferma esterno che deve arrivare entro il tempo stabilito dall’attività Delay (10 sec) scaduto il quale l’attività viene considerata conclusa anche in assenza di conferma.
InvokeWebService permette al workflow di invocare un WebService mentre le attività WebServiceInput, WebServiceOutput e WebServiceFault permettono di esporre un workflow come servizio web.

Nel caso si voglia eseguire un insieme di attività all’interno di un ambiente transazionale (simile all’utilizzo di un blocco TransactionScope) è possibile racchiudere le varie attività all’interno di una TransactionScope Activity la quale assicura che l’intera operazione sia confermata solo in assenza di errori.

Un particolare workflow può richiedere che se l’esecuzione di un determinato insieme di attività non va a buon fine venga eseguita una altra serie di attività con il compito di compensare il mancato completamento delle attività precedenti.

Questa particolare necessità può essere implementata in Workflow Foundation utilizzando le attività CompensatableSequenceo CompensatableTransactionScope, le quali permettono l’inserimento all’interno di una specifica sezione denominata Compensation Handler (disponibile nel menu contestuale dell’attività) delle attività da eseguire in caso di compensazione.
Queste attività saranno invocate sia in presenza di eccezioni che nel caso di invocazione esplicita da parte di una Compensate Activity (figura 13)

Figura 13

È possibile invocare da un workflow, un altro workflow, utilizzando un’InvokeWorkflow Activityponendo attenzione al fatto che l’esecuzione del nuovo workflow è asincrona rispetto al workflow chiamante.

Conclusione

In questa prima parte abbiamo realizzato il nostro primo programma basato su Workflow Foundation, analizzato alcuni dettagli come l’interattività con l’ambiente host e le varie attività presenti nella libreria di base.
Nella prossima parte analizzeremo altri aspetti come la creazione di attività personalizzate, i workflow basati su macchine a stati e l’utilizzo dei servizi presenti in Workflow Foundation.

Riferimenti

Area dedicata a Workflow Foundation su NetFx3.com

Windows Workflow Activities


Page view tracker