Di Corrado Cavalli, Microsoft MVP
Nel precedente articolo abbiamo introdotto i concetti fondamentali di Workflow Foundation: L’architettura, l’integrazione con l’ambiente ospite e le attività di base.Questa seconda parte si apre analizzando la creazione di attività personalizzate.
.gif)
In questa pagina
Attività personalizzate
State Machine workflows
Servizi e loro integrazione
Conclusion
Riferimenti (in lingua inglese)
Attività personalizzate
Le attività di base, pur interessanti, non possono sicuramente competere con la flessibilità offerta dal codice C# o VB e pensare di realizzare un workflow complesso interamente basato su attività base è sicuramente controproducente.
Ciò che rende Workflow Foundation veramente appetibile è invece la possibilità di creare e utilizzare librerie di attività personalizzate le quali possono costituire delle vere e proprie macrofunzionalità che possono essere in seguito assemblate da chi, pur non essendo sviluppatore, conosce benissimo il flusso di lavoro che vuole ottenere.
Le attività personalizzate possono essere realizzate per composizione di altre attività oppure per derivazione dalla classe base Activity che rappresenta la classe base dalla quale ereditano tutte le attività di Workflow Foundation.
La figura 1 mostra l’esempio descritto nel precedente articolo racchiuso all’interno di una custom activity alla quale sono state aggiunte, da codice, le proprietà StockName e Quantity.
C#
public static readonly DependencyProperty StockNameProperty =
DependencyProperty.Register("StockName", typeof(string), typeof(SendOrder), new PropertyMetadata(null));
public static readonly DependencyProperty QuantityProperty =
DependencyProperty.Register("Quantity", typeof(int), typeof(SendOrder), new PropertyMetadata(0));
public int Quantity
{
get { return (int)GetValue(QuantityProperty); }
set { SetValue(QuantityProperty, value); }
}
public string StockName
{
get { return (string)GetValue(StockNameProperty); }
set { SetValue(StockNameProperty, value); }
}
VB
public shared readonly StockNameProperty as DependencyProperty = _
DependencyProperty.Register("StockName", GetType(string), GetType(SendOrder), _
new PropertyMetadata(Nothing))
public shared readonly QuantityProperty as DependencyProperty = _
DependencyProperty.Register("Quantity", GetType(int), GetType(SendOrder), _
new PropertyMetadata(0))
Public Property Quantity() As Integer
Get
Return Convert.ToInt32(GetValue(QuantityProperty))
End Get
Set(ByVal value As Integer)
SetValue(QuantityProperty, value)
End Set
End Property
Public Property StockName() As String
Get
Return Convert.ToString(GetValue(StockNameProperty))
End Get
Set(ByVal value As Integer)
SetValue(StockNameProperty, value)
End Set
End Property
Le proprietà StockNameeQuantity per come sono state definite rappresentano delle Dependency Properties ovvero delle proprietà il cui valore non è memorizzato come consuetudine in un membro privato, ma all’interno della classe base DependencyObject dalla quale Activity deriva. Dichiarare delle proprietà in questo particolare modo offre, tra i vari vantaggi, quello di poter utilizzare il designer per connettere tra loro le proprietà esposte dalle varie attività presenti in un workflow realizzando quello che viene conosciuto come Activity Binding. Compilato il progetto l’attività apparirà nella casella degli strumenti di Visual Studio 2005 e potrà quindi essere trascinata all’interno di un workflow e successivamente configurata.
Figura 2
Figura 3
Grazie al fatto che le proprietà StockName e Quantity sono delle dependency property utilizzando il designer è stato possibile collegarle alle corrispettive proprietà esposte dal workflow (Figura 3)
Le proprietà pubbliche delle attività interne possono essere rese visibili a livello di singola attività direttamente via designer realizzando quello che viene conosciuto come Property Promotion.
Nel caso si abbia la necessità di un maggiore livello di dettaglio nel descrivere il comportamento di un’attività si può decidere di operare per derivazione, l’esempio che segue mostra un’attività SendMail per l’invio di un messaggio di posta elettronica.
C#
public class SendMailActivity:System.Workflow.ComponentModel.Activity
{
public static readonly DependencyProperty MessageProperty =
DependencyProperty.Register("Message", typeof(string), typeof(SendMailActivity),
new PropertyMetadata(null));
public string Message
{
get { return (string)GetValue(MessageProperty); }
set { SetValue(MessageProperty, value); }
}
protected override ActivityExecutionStatus Execute
(System.Workflow.ComponentModel.ActivityExecutionContext executionContext)
{
MailMessage message=new MailMessage("john@doe.com","Carl@site.com","Email Message", Message);
SmtpClient client=new SmtpClient("smtp.doe.com");
client.Send(message);
return ActivityExecutionStatus.Closed;
}
}
VB#
Public Class SendMailActivity: Inherits System.Workflow.ComponentModel.Activity
Public Shared ReadOnly MessageProperty As DependencyProperty= _
DependencyProperty.Register("Message", GetType(String), GetType(SendMailActivity), _
New PropertyMetadata(Nothing))
Public Property StockName() As String
Get
Return Convert.ToString(GetValue(MessageProperty))
End Get
Set(ByVal value As String)
SetValue(MessageProperty, value)
End Set
End Property
Protected Overrides Function Execute( _
ByVal executionContext As System.Workflow.ComponentModel.ActivityExecutionContext) As _
ActivityExecutionStatus
Dim message As New MailMessage("john@doe.com", "Carl@site.com", "Email Message", message)
Dim client As New SmtpClient("smtp.doe.com")
client.Send(message)
Return ActivityExecutionStatus.Closed
End Function
End Class
La classe SendMailActivity eredita dalla classe Activity, espone una proprietà Message e ridefinisce il metodo Execute() il quale verrà invocato al momento dell’esecuzione dell’attività.
Una volta completato, il metodo Execute() deve informare il workflow riguardo lo stato in cui si trova l’attività ritornando uno dei possibili valori esposti dall’enumerato ActivityExecution. Nel nostro esempio, essendo l’attività conclusa, ritorniamo il valore ActivityExecutionStatus.Closed.
Anche in questo caso, dopo la compilazione, l’attività può essere trascinata dalla casella degli strumenti e utilizzata all’interno di un workflow come una qualsiasi attività di base.
Le attività sono normalmente associate a dei validatori i quali ne assicurano la congruenza al momento della compilazione segnalando visivamente ogni eventuale problema.
Associamo quindi alla classe SendMailActivity un validatore il cui scopo è consentire l’utilizzo dell’attività solo se la proprietà Message è correttamente valorizzata.
C#
class SendMailActivityValidator : ActivityValidator
{
public override ValidationErrorCollection Validate (ValidationManager manager, object obj)
{
ValidationErrorCollection errors = base.Validate(manager, obj);
SendMailActivity activity = (obj as SendMailActivity);
if (activity.Parent != null)
{
if (!activity.IsBindingSet(SendMailActivity.MessageProperty) && String.IsNullOrEmpty(activity.Message))
{ errors.Add(ValidationError.GetNotSetValidationError("Message"));
}
}
return errors;
}
}
VB
Class SendMailActivityValidator:Inherits ActivityValidator
Public Overrides Function Validate _
(ByVal manager As ValidationManager, ByVal obj As Object) As ValidationErrorCollection
Dim errors As ValidationErrorCollection = base.Validate(manager, obj)
Dim activity As SendMailActivity = TryCast(obj, SendMailActivity)
If (activity.Parent IsNot Nothing) Then
If Not activity.IsBindingSet(SendMailActivity.MessageProperty) _
AndAlso String.IsNullOrEmpty(activity.Message) Then
errors.Add(ValidationError.GetNotSetValidationError("Message"))
End If
End If
Return errors
End Function
End Class
L’associazione del validatore alla classe SendMailActivity avviene decorando la classe con l’attributo ActivityValidator:
C#
[ActivityValidator(typeof(SendMailActivityValidator))]
public class SendMailActivity:System.Workflow.ComponentModel.Activity{…}
VB
<ActivityValidator(GetType(SendMailActivityValidator))> _
Public Class SendMailActivity : Inherits System.Workflow.ComponentModel.Activity
...
End Class
Cercando di compilare un workflow contenente una SendMailActivity priva della proprietà Message valorizzata otterremo un errore di compilazione e un indicazione simile a quella di figura 4
Figura 4
Associando la classe ad un proprio ActivityDesigner è anche possibile cambiare l’aspetto dell’attività personalizzata come mostrato in Figura 5.
C#
[ActivityValidator(typeof(SendMailActivityValidator))]
[Designer(typeof(SendMailActivityDesigner))]
public class SendMailActivity:System.Workflow.ComponentModel.Activity {...}
VB
<ActivityValidator(GetType(SendMailActivityValidator))> _
<Designer(GetType(SendMailActivityDesigner))> _
Public Class SendMailActivity : Inherits System.Workflow.ComponentModel.Activity
...
End Class
Figura 5
Realizzare delle librerie di attività personalizzate con l’obiettivo che sia poi l’utente finale ad utilizzarle nei propri workflows usando Visual Studio è ovviamente improponibile, fortunatamente questo non è necessario in quanto è possibile ospitare il designer di Workflow Foundation all’interno di proprie applicazioni. (Figura 6)
Figura 6
State Machine workflows
Gli esempi finora mostrati presupponevano l’esecuzione delle attività secondo uno schema sequenziale, questo non e’ pero’sempre applicabile, anzi, spesso i workflows devono essere rappresentati come un insieme di stati la cui evoluzione e’ controllata da eventi esterni ottenendo quelli che vengono identificati come State Machine Worflows.
Un esempio di state machine workflow relativo alla gestione di un ordine è riportato in Figura 7.
Figura 7
La scelta di un template di tipo State Machine Workflow fa si che la toolbox si arricchisca di nuove attività, tra queste l’attivita State rappresenta uno dei possibili stati in cui si può trovare il workflow.
Tra i vari stati e’ possibile selezionare sia lo stato iniziale, ovvero quello in cui si trovera’ il workflow allo startup (Waiting nel nostro esempio) sia quello finale (se applicabile) raggiunto il quale il workflow si puo’considerare completato.
All’interno delle varie State Activities si possono trascinare altri tipi di attività:
-
StateInitialization
Consente l’inserimento di attività che verranno eseguite quando il workflow entra in un determinato stato.
-
StateFinalization
Consente l’inserimento di attività che verranno eseguite quando il workflow esce da un determinato stato.
-
EventDrivenActivity
Contiene le attività che attendono gli stimoli esterni che determinano il passaggio dallo stato attuale verso un altro, il passaggio verso il nuovo stato avviene impiegando un attività di tipo SetState. (Figura 8)
-
Altre attività di tipo State che ereditano la gestione degli stati dello stato parent permettendo la realizzazione di macchine a stati gerarchiche.
Figura 8
In alcuni casi il debug di un workflow basato su macchine a stati puo’ risultare problematico, ecco il motivo percui Workflow Foundation espone la classe StateMachineWorkflowInstance mediante la quale e’ possibile conoscere tutti i dettagli di una particolare istanza come: lo stato attuale, gli stati visitati in precedenza e i possibili stati verso i quali il workflow puo’ eventualmente migrare.
Servizi e loro integrazione
Abbiamo già accennato in precedenza che Workflow Foundation è un runtime estendibile grazie ad un’architettura basata su servizi che possono essere aggiunti a run-time.
La presenza di questi servizi determina la disponibilità o meno di specifiche funzionalità come il Local Comunication Service di cui abbiamo parlato nel precedente articolo.
Tra i servizi già disponibili in Workflow Foundation il servizio di persistenza è senza dubbio quello più interessante.
Il workflow, per propria natura, è un classico esempio di “programma reattivo” ovvero di programma che passa la maggior parte del proprio tempo in attesa di stimoli esterni che ne facciano proseguire l’esecuzione.
Immaginiamo un processo di approvazione ordine: com’è facile intuire il processo passerà gran parte del tempo in attesa che i vari attori interessati (Capo Ufficio, Direttore Tecnico e Direttore Amministrativo) approvino la richiesta e, per come spesso vanno le cose, è molto probabile che l’approvazione arrivi parecchi giorni, per non dire mesi, dopo che la richiesta è stata inoltrata.
Mantenere in memoria un workflow in attesa che un evento esterno gli comunichi di proseguire è ovviamente inutile, com’è altresì ovvia la necessità di garantire il corretto riavvio del workflow in presenza di un imprevisto shutdown della macchina che lo ospita.
Il servizio di persistenza si occupa di salvare un intero workflow scaricandolo dalla memoria e gestisce automaticamente il suo successivo caricamento in presenza di azioni esterne dirette verso qualsiasi workflow attualmente serializzato.
Il servizio di persistenza disponibile in Workflow Foundation è in grado di serializzare i vari workflows all’interno di SQL Server ed è esposto dalla classe SqlWorkflowPersistenceService.
Prima di poter utilizzare questo servizio è necessario creare il database di appoggio utilizzando gli scripts presenti nella cartella <Drive>:\Windows\Microsoft.NET\Framework\v3.0\Windows Workflow Foundation\SQL\EN eseguendo nell’ordine: SqlPersistenceService_Schema.sql e SqlPersistenceService_Logic.sql.
Prendiamo come esempio il semplice workflow di figura 9 il quale non fa altro che scrivere nella console “WF Started”, attendere 10 secondi e proseguire scrivendo “WF Resumed”.
Il codice per l’aggiunta del servizio di persistenza e la sottoscrizione degli eventi WorflowLoaded e WorkflowPersisted generati quando il workflow viene rispettivamente caricato e scaricato dalla memoria e riportato di seguito:
C#
using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
workflowRuntime.WorkflowLoaded += delegate{Console.WriteLine("Workflow Reloaded...");};
workflowRuntime.WorkflowPersisted += delegate { Console.WriteLine("Workflow Persisted...");};
//Creo e aggiungo il servizio di persistenza
SqlWorkflowPersistenceService persisService =
new SqlWorkflowPersistenceService(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestWF;Integrated
Security=True", true,TimeSpan.MaxValue,new TimeSpan(0, 0, 2));
workflowRuntime.AddService(persisService);
...
...
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(Persistence.Workflow1));
instance.Start();
waitHandle.WaitOne();
}
VB
Using runtime As WorkflowRuntime = New WorkflowRuntime()
AddHandler workflowRuntime.WorkflowLoaded, AddressOf Loaded
AddHandler workflowRuntime.WorkflowPersisted, AddressOf Persisted
'Creo e aggiungo il servizio di persistenza
Dim persisService As New SqlWorkflowPersistenceService( _
"Data Source=.\SQLEXPRESS;Initial Catalog=TestWF;Integrated Security=True", _
True, TimeSpan.MaxValue, New TimeSpan(0, 0, 2))
workflowRuntime.AddService(persisService)
...
...
Dim instance As WorkflowInstance = _
workflowRuntime.CreateWorkflow(GetType(Persistence.Workflow1))
instance.Start()
waitHandle.WaitOne()
End Using
Sub LogLoaded(ByVal sender As Object, ByVal e As WorkflowEventArgs)
Console.WriteLine("Workflow Reloaded...")
End Sub
Sub LogPersisted(ByVal sender As Object, ByVal e As WorkflowEventArgs)
Console.WriteLine("Workflow Persisted...")
End Sub
Figura 10
Eseguendo il workflow si otterrà il risultato riportato in Figura 10 dal quale è possibile notare come, all’avvio del timer, il workflow venga memorizzato e successivamente ricaricato allo scadere dello stesso.
Il costruttore della classe SqlPersistenceService, riportato di seguito, consente la personalizzazione del servizio di persistenza.
C#
public SqlWorkflowPersistenceService (string connectionString, bool unloadOnIdle, TimeSpan
instanceOwnershipDuration, TimeSpan loadingInterval )
VB
Public Sub New (connectionString As String, unloadOnIdle As Boolean, _
instanceOwnershipDuration As TimeSpan, loadingInterval As TimeSpan)
Il parametro unloadOnIdle, se impostato a true (il nostro caso) indica che il workflow deve essere serializzato non appena raggiuge uno stato di attesa, instanceOwnershipDuration rappresenta il tempo entro il quale i records che descrivono una particolare istanza non devono essere disponibili ad altri workflows che condividono lo stesso database, cio’ per assicurare che un istanza venga deserializzata e gestita da un unico workflow mentre loadingInterval indica l’intervallo di tempo utilizzato dal servizio per verificare la presenza di eventuali timers scaduti che potrebbero provocare il ricaricamento del workflow (come nel nostro caso).
La persistenza di ogni istanza puo’ essere controllata manualmente utilizzando i metodi Load(), Unload(), TryUnload()esposti dalla classe WorkflowInstance.
Ovviamente non siamo vincolati all’utilizzo di SQL Server come repository per i dati di persistenza, in sostituzione al SqlPersistenceService è infatti possibile creare un proprio servizio di persistenza creando una classe che eredita da WorkflowPersistenceService e implementando i metodi SaveWorkflowInstanceState() e LoadWorkflowInstanceState().
Oltre al servizio di persistenza Workflow Foundation rende disponibile un servizio di tracking grazie al quale è possibile tracciare tutte le operazioni svolte dal workflow e relative attività durante la sua esecuzione. La modalità di utilizzo è simile a quella indicata in precedenza e anch’essa utilizza SQL Server come base dati, di conseguenza prima di utilizzare la classe SqlTrackinService e’ necessario creare le relative tabelle lanciando gli scripts Tracking_Schema.sql e Tracking_Logic.sql.
Un esempio di utilizzo e’ riportato di seguito:
C#
...
//Creo e aggiungo il servizio di tracking
string trackingString = @"Data Source=.\SQLEXPRESS;Initial Catalog=TestWF;Integrated Security=True";
SqlTrackingService trackingService = new SqlTrackingService(trackingString);
trackingService.UseDefaultProfile = true;
workflowRuntime.AddService(trackingService);
...
VB
...
'Creo e aggiungo il servizio di tracking
Dim trackingString As String = _
"Data Source=.\SQLEXPRESS;Initial Catalog=TestWF;Integrated Security=True"
Dim trackingService As New SqlTrackingService(trackingString)
trackingService.UseDefaultProfile = True
workflowRuntime.AddService(trackingService)
...
Lo snippet che segue invece utilizza la classe SqlTrackingQuery per analizzare i dati memorizzati:
C#
...
SqlTrackingQuery query = new SqlTrackingQuery(trackingString);
SqlTrackingWorkflowInstance wi;
query.TryGetWorkflow(instance.InstanceId, out wi);
IList<ActivityTrackingRecord> events = wi.ActivityEvents;
for (int i = 0; i < events.Count; i++)
{
ActivityTrackingRecord atr = events[i];
Console.WriteLine(string.Format("{0}-{1}-{2}", atr.QualifiedName, atr.EventDateTime, atr.ExecutionStatus));
}
...
VB
...
Dim query As New SqlTrackingQuery(trackingString)
Dim wi As SqlTrackingWorkflowInstance
query.TryGetWorkflow(instance.InstanceId, ByRef wi)
Dim events as IList<ActivityTrackingRecord> = wi.ActivityEvents
For i As Integer = 0 To events.Count
Dim atr As ActivityTrackingRecord = events(i)
Console.WriteLine(String.Format("{0}-{1}-{2}", atr.QualifiedName, _
atr.EventDateTime, atr.ExecutionStatus))
Next
...
Anche il servizio di tracking è sostituibile con uno proprio ereditando dalla classe TrackingService, tenedo presente che vista la notevole mole di informazioni che vengono tracciate va usato con parsimonia in quanto può impattare sulle prestazioni dell’intero workflow.
Come accennato nell’articolo precedente il workflow viene eseguito in un thread separato preso dal thread-pool e reso disponibile dal servizio di scheduling predefinito DefaultSchedulerService.
Se questa modalità e’ utile in un applicazione smart-client non è sicuramente ottimale nel caso il workflow sia utilizzato all’interno di un applicazione web dove ogni richiesta è già a sua volta processata usando threads presi a loro volta da thread-pool.
In questo caso, l’utilizzo di un ManualSchedulerService il quale anziché creare ulteriori threads “dona” il thread corrente al workflow (che perciò viene eseguito in maniera sincrona) può contribuire ad ottimizzare le risorse impiegate e a minimizzare l’utilizzo di threads.
Come per i precedenti servizi, anche il servizio di scheduling può essere personalizzato ereditando da WorkflowSchedulerService.
Conclusion
ConclusioniA prima vista Workflow Foundation può sembrare una ambiente interamente basato sul drag-and-drop dove la programmazione avviene quasi totalmente in maniera visuale. In realtà le cose sono ben diverse, realizzare workflows per il mondo reale significa scrivere parecchio codice soprattutto nella definizione delle varie macroattività.Questo però non ci deve spaventare in quanto tutto il lavoro dello sviluppatore sarà diretto sul workflow stesso e non su inutili dettagli di contorno (persistenza, tracking…) aumentando, non poco, la produttività.
Riferimenti (in lingua inglese)
Area dedicata a Workflow Foundation su NetFx3.com