21 octobre 2008

Workflow Foundation : Hello World

Pour ne pas déroger à la tradition du HelloWorld, nous allons créer pour ce premier exemple, un workflow qui écrit HelloWorld et votre prénom dans la console. Cet exemple très simple a le mérite de décrire les étapes fondamentales du développement et de l'exécution d'un workflow séquentiel. On va donc voir comment créer un workflow avec un template de Visual Studio 2008 prêt à l'emploi. Nous verrons ensuite comment il est hébergé et exécuté grâce au moteur de workflow.

Dans un premier temps, créez d'abord un nouveau projet dans une nouvelle solution. Choisissez le template "Sequential Workflow Console Application", nommez le projet "HelloWorld" et la solution "HelloWorldWorkflow". Le projet créé est de type Console comme laisse présager le titre du template.


Ce template crée un workflow s'appelant Workflow1 dans un fichier de type cs (j'ai oublié de dire que le langage utilisé est le c#). En cliquant sur le petit plus, vous voyez apparaître le fichier Workflow1.designer.cs qui est le fichier généré par le designer de Workflow. Comme dans d'autres éditeurs fournis par Visual Studio, la classe Workflow1 est partielle (mot clé partial avant le nom de la classe), ce qui permet de définir dans 2 fichiers séparés, voir plus, la classe en question. Cliquez droit sur Workflow1.cs, et en choisissant View Code, vous verrez l'autre partie de la classe définissant le workflow.

Le template a aussi créé un fichier Program.cs qui contient le code nécessaire pour instancier et exécuter le workflow généré.

Sur ce, revenez sur le designer, ouvrez la Toolbox (boite à outils) et faites glisser l'activité Code qui est de type CodeActivity. Déposez là dans le workflow. Vous devriez avoir le résultat suivant :


Voila, nous venons d'insérer notre 1ere activité dans un workflow. Cette activité a pour but d'exécuter la logique métier que vous devez définir par l'intermédiaire d'une méthode. D'ailleurs, vous avez surement du remarquer le point d'exclamation dans le rond rouge en haut à droite de l'activité. Il signifie que justement, nous n'avons pas encore défini la méthode à exécuter et qu'il est nécessaire de le faire. Pour cela, double cliquez sur l'activité. Cela a pour effet de créer une méthode nommée "codeActivity1_ExecuteCode" et d'afficher le code de la classe Workflow1.cs. Dans l'autre partie de la classe générée par le designer, cela a instancié un délégué qui prend en argument la méthode précédemment créée et ce délégué est ajouté à l'événement ExecuteCode de l'activité.

Définissons le code de la méthode codeActivity1_ExecuteCode. Ce code va écrire dans la console "Hello World " suivi d'un nom qui sera passé en paramètre au workflow. Ajoutez d'abord la propriété Nom de type string comme cela :



public string Nom
{
get;
set;
}

Cette façon d'écrire une propriété, qui est une nouveauté du Framework 3.5, permet de générer à la fois l'accesseur (get), le mutateur (set) et le champ de type string associé. C'est un raccourci d'écriture qui est fort utile lorsque vous n'avez pas besoin de définir un comportement spécial pour le mutateur ou l’accesseur. Rajoutons ensuite la ligne suivante dans la méthode codeActivity1_ExecuteCode :



private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Hello World " + Nom);
}

Le workflow HelloWorld est terminé. Pour information, voici le code du workflow en entier (Workflow1.cs) :



using System;
using System.Workflow.Activities;

namespace HelloWorld
{
public sealed partial class Workflow1: SequentialWorkflowActivity
{
public string Nom
{
get;
set;
}

public Workflow1()
{
InitializeComponent();
}

private void codeActivity1_ExecuteCode(object sender, EventArgs e)
{
Console.WriteLine("Hello World " + Nom);
}
}
}

Tout d'abord le code généré dans la méthode Main qui est le point d'entrée de notre application console, sert à instancier et exécuter le workflow précédemment créé. Pour cela, nous devons d'abord instancier le moteur de workflow représenté par la classe System.Workflow.Runtime.WorkflowRuntime. Cette classe ou plutôt l'objet instancié de cette classe, fournit l'environnement d'exécution pour tous les workflows de l'application. Une seule instance de cette classe est autorisée par domaine d'application, comprenez par application pour simplifier. Il est bien évidemment possible de définir plusieurs domaines d'application dans une même application, mais ce n'est pas le sujet ici. Voici le code d'initialisation du moteur :



using(WorkflowRuntime workflowRuntime = new WorkflowRuntime())
{
}

L'instanciation du moteur est faite dans un bloc using (instanciation) {Suite d'instruction} car le moteur dérive de l'interface IDisposable, qui oblige la classe qui en hérite de définir une méthode Dispose. Celle-ci s'occupe de terminer l'exécution des workflows en cours et de libérer la mémoire utilisée. A la fin du bloc using, cette méthode est donc appelée implicitement. Toute nouvelle instruction prend place entre les accolades du using.

Poursuivons notre description. La ligne suivante déclare un objet de type System.Threading.AutoResetEvent et l'instancie :



AutoResetEvent waitHandle = new AutoResetEvent(false);

Cet objet est utile pour synchroniser 2 threads d'exécution. Il permet de stopper l'exécution d'un thread jusqu'à ce qu'un évènement particulier soit détecté, évènement déclenché dans un autre thread. Le thread déclenchant l'évènement le fait bien évidement au moment opportun, c'est-à-dire lorsqu'il a terminé ce qu'il a à faire.

Mais pourquoi avons-nous besoin d'un tel objet pour exécuter notre workflow HelloWorld ? Et bien, c'est simple. Le comportement par défaut du moteur de workflow est de créer un thread par workflow exécuté. Ce comportement est bien sûr modifiable, mais pour l'heure, nous ferons avec. Si nous exécutons notre workflow sans utiliser un objet AutoResetEvent, alors notre application instancie effectivement le workflow, le lance, ce qui crée un thread dédié où le workflow est exécuté. Cependant juste après la ligne lançant l'exécution du workflow, le flux d'exécution continuera sans attendre la fin du workflow, sortira ensuite du bloc using, ce qui appelera la méthode Dispose implicitement, qui aura pour effet de détruire l'instance du moteur et du workflow par la même occasion. Nous n'aurons même pas la chance de voir afficher notre "HelloWorld" dans la console. Avec un objet AutoResetEvent, cela nous permet d'attendre tranquillement la fin du workflow.

Définissons 2 délégués anonymes que l'on affectera à l'évènement WorkflowCompleted et WorkflowTerminated du moteur de workflow (ici workflowRuntime). Le 1er évènement est déclenché lorsqu'un workflow exécuté par le moteur se termine avec succès. L'autre, WorkflowTerminate, est déclenché lorsqu'un workflow se termine à cause d'une exception. Voici les 2 délégués :



workflowRuntime.WorkflowCompleted += delegate(object sender, WorkflowCompletedEventArgs e)
{
waitHandle.Set();
};

workflowRuntime.WorkflowTerminated += delegate(object sender, WorkflowTerminatedEventArgs e)
{
Console.WriteLine(e.Exception.Message);
waitHandle.Set();
};

Chacun d'eux exécute la méthode Set() de l'objet waitHandle. Cette instruction marche de pair avec la méthode WaitOne() du même objet. Comme expliqué plus haut, un workflow est exécuté dans son propre thread. Ainsi pour attendre la fin du workflow, nous utilisons la méthode WaitOne() qui attend que la méthode Set() soit exécutée dans un autre thread.

Les 2 délégués sont donc exécutés par le thread du workflow. Leur exécution permet de poursuivre l'éxecution de notre programme principal (bloquée jusqu'alors par WaitOne() de l'objet waitHandle).

Notez que le délégué associé à l'évènement WorkflowTerminated récupère l'erreur générée par le workflow et l'écrit dans la console.

A ce stade, il ne reste plus qu'à définir les arguments du workflow, c'est-à-dire le nom qui s'affichera à côté de "HelloWorld", de lancer le workflow et finalement d'attendre la fin de son exécution. Pour définir les arguments d'un workflow, WF permet d'utiliser une Hashtable ou Dictionnaire. Cette structure de données associe une valeur clé (ici de type string) avec un objet et cette association de données définie un élément du dictionnaire. Voici le code pour définir le paramètre de notre workflow :


Dictionary<string,object> arguments = new Dictionary<string,object>();
arguments.Add("Nom", "John Connor");

Il est possible de remplacer le nom ci-dessus par l'un de votre choix, ceci n'a aucune incidence sur le bon fonctionnement du workflow. Lors de la définition de notre workflow, nous avons défini une propriété Nom dans la classe partielle du workflow. La clé de notre argument dans le dictionnaire a le même nom que cette propriété. En fait, lorsque l'on passe le dictionnaire au moteur pour instancier notre workflow, celui-ci va, après avoir instancié le workflow, chercher, via la réflexion, la propriété du workflow s'appelant exactement "Nom" et va lui donner la valeur, ici "John Connor" en la castant, vu que c'est un objet dans le dictionnaire.

Il ne reste plus qu'à instancier le workflow et le lancer avec les 2 instructions suivantes :



WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(HelloWorld.Workflow1),arguments);
instance.Start();

La méthode CreateWorkflow prend en argument le type du workflow et le dictionnaire définissant les arguments. Elle renvoie un objet de type WorkflowInstance qui contient la méthode Start() permettant d'exécuter le workflow.

Finalement, ajoutez les 2 lignes suivantes pour attendre la fin d'exécution du workflow et de laisser le choix à l'utilisateur de fermer le programme.



waitHandle.WaitOne();
Console.ReadKey();

Si tout s'est bien passé, voici l'affichage que vous devriez avoir après avoir compilé et lancé le projet :


Pour une meilleure vision du code nécessaire à l'instanciation et l'exécution d'un workflow, voici l'intégralité du code :



using System;
using System.Threading;
using System.Workflow.Runtime;
using System.Collections.Generic;

namespace HelloWorld
{
class Program
{
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();
};

Dictionary<string,object> arguments = new Dictionary<string,object>();
arguments.Add("Nom", "John Connor");

WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(HelloWorld.Workflow1),arguments);
instance.Start();

waitHandle.WaitOne();

Console.ReadKey();
}
}
}
}

1 commentaire:

Anonyme a dit…

Riad (Algérie):
Merci beaucoup pour cette explication c'est ce que je cherché, simple et efficace !!!