Introduction à la communication hôte / instance
Lors de la modélisation d’un Workflow avec Windows Workflow Foundation, c’est au
développeur de prendre en compte les problématiques d’hébergement.
Une application lambda (service Windows, site Web, appli console…) sera donc
chargée d’héberger le « Runtime » de WF qui lui-même initialisera et pilotera
les différentes instances de Workflow.
Par exemple, un Workflow actif chargé de faire de la validation de documents
possédera, à un moment donné, autant d’instances actives que de documents en
attente d’approbation.
Une instance de Workflow a donc souvent besoin de communiquer avec son
application hôte, qui elle-même peut avoir à transmettre des demandes venant de
d’autres programmes : demande d’initialisation, approbation d’utilisateur,
validation du succès de l’exécution du Workflow… Cette communication doit bien
entendu se faire dans les deux sens, de l’hôte vers une instance ou d’une
instance vers l’hôte.
Pour répondre à ce besoin, Workflow Foundation propose d’une part un système de
transmission de données lorsque l’instance de workflow est créé ou se termine et
d’autre part un système de service de communication permettant de facilement
mettre en place, à tout moment de son exécution, un échange d’informations hôte
/ instances.
Paramètres d’initialisation et de sortie
Nul besoin de système de communication complexe pour initialiser une instance de Workflow avec des valeurs spécifiques, ceci rendant la communication avant / après exécution fort simple.
Au niveau de l’instance, les paramètres d’initialisation doivent être définis en temps que propriétés, par exemple, imaginons que le Workflow suivant nécessite un entier appelé « NombreGauche » en paramètre :

Il suffit pour ceci de définir, dans le code du Workflow, une propriété qui peut
ensuite être affichée dans l’activité de type code « AfficheParametres » :
namespace InstanceWorkflow
{
public sealed partial class SimpleWorkflow:
SequentialWorkflowActivity
{
public SimpleWorkflow()
{
InitializeComponent();
}
private int _nombreGauche;
public int NombreGauche
{
set { _nombreGauche = value; }
}
private void AfficheParametres_ExecuteCode(object
sender, EventArgs e)
{
Console.WriteLine(“Initialisation:
Nombre gauche: {0}”,_nombreGauche);
}
}
}
Pour initialiser cette propriété dans l’application hôte, il suffit de définir un dictionnaire de paramètres, ou chaque clé correspond au nom de la propriété cible et chaque valeur représente la valeur à lui assigner, dans le cas précédant, une seule propriété est à définir soit :
Dictionary<string, object> parametres = new Dictionary<string, object>();
parametres.Add("NombreGauche", 42);
Ce dictionnaire doit ensuite être transmis directement en temps que paramètre lors de la création et donc de l’initialisation de l’instance :
WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(InstanceWorkflow.SimpleWorkflow),
parametres);
instance.Start();
La correspondance dictionnaire / propriétés d’initialisation va ensuite se faire automatiquement lors de l’initialisation de l’instance de workflow fraichement créé. Le dictionnaire n’étant pas typé, il est important de respecter d’une part le nom exact du paramètre du Workflow et d’autre part le type d’objet envoyé. Dans l’exemple précédant, les valeurs suivantes auraient généré une erreur à l’exécution de l’instance :
parametres.Add("NombreDroit", 42); => Par de propriétées “NombreDroit” dans le
Workflow
parametres.Add("NombreGauche", "42"); => La propriétée “NombreGauche” est de
type “INT” et non “STRING”
De la même manière, il est possible de récupérer les valeurs des différentes propriétés d’une instance de workflow lorsque celle-ci a terminé son cycle de vie. A chaque fois qu’une instance prend fin, l’application hôte chargée de celle-ci peut être notifiée au travers de la levée de l’événement « WorkflowCompleted » du Runtime.
Par exemple, enrichissons le code précédant d’une activité de type code « Calcul » chargée de mettre à jour une propriété « Résultat » de type entier:
namespace InstanceWorkflow
{
public sealed partial class SimpleWorkflow:
SequentialWorkflowActivity
{
public SimpleWorkflow()
{
InitializeComponent();
}
private int _nombreGauche;
public int NombreGauche
{
set { _nombreGauche =
value; }
}
private int _resultat;
public int Resultat
{
get { return _resultat; }
}
private void AfficheParametres_ExecuteCode(object
sender, EventArgs e)
{
Console.WriteLine(“Initialisation:
Nombre gauche: {0}”,_nombreGauche);
}
private void Calcul_ExecuteCode(object
sender, EventArgs e)
{
_resultat = _nombreGauche + 1;
}
}
}
La propriété résultat contient donc la valeur du nom de gauche incrémentée, il ne reste plus qu’a extraire cette valeur dans l’application hôte une fois l’instance de workflow terminée :
static void Main(string[] args)
{
using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
AutoResetEvent waitHandle = new
AutoResetEvent(false);
workflowRuntime.WorkflowCompleted +=
delegate(object sender, WorkflowCompletedEventArgs e)
{
Console.WriteLine("Instance:
{0}, resultat: {1}",
e.WorkflowInstance.InstanceId,
e.OutputParameters["Resultat"]);
waitHandle.Set();
};
Dictionary<string, object> parametres = new
Dictionary<string, object>();
parametres.Add("NombreGauche", 42);
WorkflowInstance instance =
workflowRuntime.CreateWorkflow(typeof(InstanceWorkflow.SimpleWorkflow),
parametres);
instance.Start();
waitHandle.WaitOne();
}
}
Le paramètre WorkflowCompletedEventArgs possède donc un dictionnaire « OutputParameters » contenant l’intégralité des valeurs des propriétés publiques de la défunte instance.
Il ne reste plus qu’à tester le tout:

Pour communiquer en phase d’initialisation et de fin de Workflow il faut donc manipuler deux dictionnaires : un d’entrée envoyé à une instance lors de sa création et un de sortie renvoyé par celle-ci une fois son cycle d’exécution terminé.
De l’instance vers l’hôte
Il est pratique de manipuler des valeurs en entrée et sortie de Workflow, mais comment faire pour que l’instance de Workflow puisse envoyer des informations à son application hôte à tout moment de son cycle de vie ?
Il est tout simplement nécessaire de mettre en place un canal de communication hôte / instance et c’est bien sur à ce moment là que la notion de service de communication fait son apparition.
Sa mise en place se fait en trois étapes : définition du service de communication, implémentation de celui-ci dans l’hôte et enfin référencement et utilisation dans le workflow.
Dans un premier temps, il faut donc spécifier, de manière totalement neutre et indépendante de l’hôte et de l’instance, un contrat de communication. Ce contrat de communication sert à définir les différents types de messages pouvant être échangés entre instances et hôte.
Techniquement parlant, cette définition se fait au travers de la définition d’une interface:
namespace ContratCommunication
{
[Serializable]
public class CommunicationSericeEventArg :
ExternalDataEventArgs
{
}
[ExternalDataExchange]
public interface ICommunicationService
{
void
NotifieHoteAttenteNombreDroit(Guid instanceID, int nombreGauche);
}
}
Le but dans cet exemple est de permettre à n’importe quelle instance de notifier l’hôte unique qu’elle est en attente d’informations.
Il est ensuite temps d’implémenter cette interface et donc de créer le service de communication dans l’application hôte. Cette implémentation consiste à définir le code à exécuter, dans l’application hôte, dès que n’importe laquelle des instances en cours souhaite notifier celle-ci :
public class ServiceCommunicationImplementation :
ContratCommunication.ICommunicationService
{
void
ContratCommunication.ICommunicationService.NotifieHoteAttenteNombreDroit(Guid
instanceID, int nombreGauche)
{
Console.WriteLine("L'instance {0} attend le
nombre droit allant avec le nombre gauche {0}!", instanceID, nombreGauche);
}
}
Afin de pouvoir utiliser ce service, il est toutefois requis de l’enregistrer comme type de service de communication actif au niveau du runtime:
using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
ExternalDataExchangeService dataService = new
ExternalDataExchangeService();
workflowRuntime.AddService(dataService);
ServiceCommunicationImplementation ServiceCommunication = new
ServiceCommunicationImplementation();
dataService.AddService(ServiceCommunication);
//...
}
Cet enregistrement se fait en deux temps : d’abord prévenir le runtime que les instances qu’il va exécuter vont potentiellement communiquer (ajout du service de communication au runtime) et ensuite prévenir le service de communication que les instances actives vont pouvoir utiliser le « ServiceCommunicationImplementation » pour dialoguer.
L’hôte étant prêt à recevoir et traiter des instructions, il ne reste plus qu’à modifier la modélisation du workflow afin d’envoyer des messages au travers du service de communication : c’est le rôle de l’activité CallExternalMethod.
Une fois celle-ci ajoutée dans le workflow, il est nécessaire de la paramétrer afin de lui indiquer quel contrat de communication (interface) elle doit utiliser et quel message (méthode) elle représente, ces différentes actions se faisant par l’intermédiaire de la fenêtre de propriétés :

A noter que la fenêtre de propriétés propose de définir directement les valeurs envoyées en paramètre lors de l’appel (instanceID et nombreGauche). Celles-ci sont initialisées juste avant l’appel de la méthode grâce à l’événement MethodInvoking :
private void AppeleHote_MethodInvoking(object sender, EventArgs
e)
{
AppeleHote_instanceID = this.WorkflowInstanceId;
AppeleHote_nombreGauche = this._nombreGauche;
}
public Guid AppeleHote_instanceID = default(System.Guid);
public Int32 AppeleHote_nombreGauche = default(System.Int32);
A noter que le code présent ici n’est pas obligatoire, les valeurs des paramètres auraient pu directement être liées (databinding) à d’autres propriétés ou dépendances du workflow sans écriture de code supplémentaire.
L’interface de communication étant définie, implémentée et utilisées à la fois dans l’hôte et dans le workflow, il ne reste maintenant plus qu’a tester l’exécution:

De l’hôte vers l’instance
Dans la majorité des cas, le besoin de communication hôte / instance ne se situe pas dans le sens instance vers hôte vu précédemment mais dans le sens contraire.
Pour répondre à ce besoin, il suffit de compléter le contrat de communication (interface) vu précédemment en définissant des événements à implémenter. Cet enrichissement du contrat de communication sert à définir les différents messages pouvant être envoyé : d’une part, l’hôte à connaissance des types de messages qu’il peut envoyer (événements qu’il peut lever) et est garanti que l’instance les comprendra et d’autre part, l’instance s’attend à potentiellement recevoir de types de messages particuliers de l’hôte et peut donc les surveiller (abonnement à l’événement).
Voici l’interface enrichie d’un événement permettant d’envoyer un deuxième nombre à l’instance de Workflow :
[ExternalDataExchange]
public interface ICommunicationService
{
void NotifieHoteAttenteNombreDroit(Guid instanceID, int
nombreGauche);
event EventHandler<CommunicationSericeEventArg>
EnvoieNombreDroit;
}
Il est plus difficile de transporter des arguments dans un événement que dans un appel de méthode, pour faciliter cette communication, l’événement définit ici prend en argument une classe « CommunicationServiceEventArg ». Celle-ci sert tout simplement à stocker les données : elle doit ainsi obligatoirement être serializable pour permettre le transport, doit implémenter la classe ExternalDataEventArgs qui possède un constructeur ayant besoin de connaître l’identifiant de l’instance cible : c’est en effet grâce à ce constructeur que l’événement sera redirigée vers la bonne instance (plusieurs instances peuvent être en attente du même événement en parallèle, mais chacune possède un identifiant unique).
[Serializable]
public class CommunicationSericeEventArg : ExternalDataEventArgs
{
private int _nombreDroit;
public int NombreDroit
{
get { return this._nombreDroit; }
}
public CommunicationSericeEventArg(Guid instanceID, int nombreDroit)
: base(instanceID)
{
this._nombreDroit = nombreDroit;
}
}
Il est ensuite bien entendu nécessaire de modifier l’implémentation de l’instance dans l’application hôte afin d’implémenter l’événement et d’écrire une méthode permettant la levée de l’événement :
public class ServiceCommunicationImplementation :
ContratCommunication.ICommunicationService
{
public void NotifieHoteAttenteNombreDroit(Guid instanceID, int
nombreGauche)
{
Console.WriteLine("L'instance {0} attend le nombre
droit allant avec le nombre gauche {0}!", instanceID, nombreGauche);
envoienombredroit(10, instanceID);
}
public event EventHandler<ContratCommunication.CommunicationSericeEventArg>
EnvoieNombreDroit;
public void envoienombredroit(int valeur, Guid instanceIDCible)
{
Console.WriteLine("L'hôte envoie le nombre droit{0}!",
instanceIDCible, valeur);
if (EnvoieNombreDroit != null)
this.EnvoieNombreDroit(null, new
ContratCommunication.CommunicationSericeEventArg(instanceIDCible, valeur));
}
}
Au niveau du Workflow, l’activité HandleExternalEvent est chargée de réceptionner les événements levés par l’application hôte, celle-ci se configure de manière quasiment identique à l’activité CallExternalMethod :

Le paramètre « e » représente les données envoyées de l’hôte à l’instance, afin de les exploiter il est donc nécessaire de le stocker dans une propriété locale à l’instance nommée ici « AttenteHote_arguments ».
Il ne reste enfin plus qu’à modifier le code de « Calcul » afin de prendre en compte le nombre de droite envoyé par l’instance et de tester le tout :
private void Calcul_ExecuteCode(object sender, EventArgs e)
{
this._resultat = AttenteHote_arguments.NombreDroit + this._nombreGauche;
}

Conclusion
Mettre en place un système de communication robuste entre un hôte et ses instances reste une opération fastidieuse et nécessite obligatoirement la mise en place d’un service de communication au niveau du runtime du workflow et l’utilisation des activités spécifiques CallExternalMethod et HandleExternalEvent.
Il est possible de générer un système de communication de manière beaucoup plus simple au travers de l’utilisation d’activités WebServices (nécessitant donc une application ASP.NET comme hôte) ou, dans Orcas, d’activités WCF. Ces deux possibilités feront chacune l’objet de prochains articles.
Le code source illustrant l'article est disponible sur
csharpfr.com
Florent SANTIN