Création d'activités personnalisées avec Workflow Foundation

| Louis-Guillaume MORAND | Vincent Lainé |

Dans cet article, nous découvrirons comment étendre Workflow Foundation en créant des activités personnalisées réutilisables et intégrant les mécanismes de Workflow Foundation, tels le designer ou encore les validations.

N'hésitez pas à commenter cet article ! Commentez ?

- Titre : Création d'activités personnalisées avec Workflow Foundation

- Auteurs : Louis-Guillaume MORAND, Vincent Lainé

- Parution : 30 avril 2009

- Licence : Copyright ® 2008 Louis-Guillaume Morand. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- Introduction

- I. Création de l'activité

- I-A. Création du projet et de la classe principale

- I-B. Ajout du code métier

- II. Modifier l'affichage de l'activité

- II-A. Ajouter l'activité à la boite à outils

- II-B. Changer l'icône affichée dans la boite à outils

- II-C. Changer l'affichage de l'activité

- II-D. Modifier le comportement du designer en fonction de notre activité

- III. Mettre en place un système de validation WF

- IV. Utilisation de l'activité personnalisée

- V. Création d'une activité « Root »

- V-A. Création de notre activité « root »

- V-B. Création du designer associé

- VI. Conclusion

- VII. Liens et téléchargements

- VIII. Remerciements

Introduction

Workflow Foundation est un Framework très puissant qui se distingue de ses concurrents par sa relative simplicité, au point même de parfois paraître être un framework incomplet et pas suffisamment professionnel. C'est là, une grossière erreur de jugement, car précisément, l'équipe de développement a conçu un moteur de workflow volontairement simple, mais facilement extensible afin de permettre à chacun de n'avoir que l'utile et d'y ajouter le nécessaire. L'exemple du tracking et de la persistance en sont deux parfaits exemples, il s'agit de laisser un moteur léger, mais sur lequel il est très simple d'ajouter différents types de services, y compris des services personnalisés. Il en va de même pour les activités. Workflow Foundation en propose un certain nombre, toutes utiles, mais aussi riche soit-il, il laisse la possibilité de créer et d'ajouter soi-même ces activités.

Au sein de cet article, nous allons donc voir comment créer ces activités, comment les personnaliser graphiquement et voir leurs avantages et inconvénients.

I - Création de l'activité

La plupart du temps, les activités personnalisées servent à exécuter quelques lignes de code, ce que nous pourrions faire avec l'activité Code. Pourquoi alors s'embêter à faire une activité personnalisée puisqu'on peut s'en passer ? Et bien pour plusieurs raisons.

1- Pour permettre la réutilisation et gagner du temps. En effet, plutôt que placer plusieurs activités Code et de personnaliser chacune d'entre elles, il nous suffira de placer notre activité perso qui contiendra déjà le code à exécuter.

2- Pour améliorer la visibilité. Si l'activité répète régulièrement un même code, alors pour la distinguer des autres activités code, il suffit de la faire se démarquer pour être facilement repérable.

3- Pour faciliter l'édition au cours du temps et éviter les erreurs. En construisant une activité tirant parti de la fenêtre de propriétés, vous la rendez facilement modifiable et vous pouvez éviter les erreurs en proposant une liste d'options limitée.

4- Pour permettre une évolution aisée. Toutes les applications évoluent au cours du temps et l'activité personnalisée permet d'avoir un point central à modifier, se répercutant sur l'ensemble du Workflow. Là encore, un gain de temps limitant par la même occasion le risque de régression.

5- Pour permettre la réutilisation au sein de plusieurs applications. L'activité, créée dans une assembly séparée, pourra être utilisée par d'autres applications.

Pour ces cinq raisons, qui ne sont pas les seules, il peut être plus qu'utile de créer des activités personnalisées. Au cours de cet article, nous prendrons un exemple simple : celui d'un Workflow dans lequel, à chaque étape, nous souhaitons recevoir un mail indiquant le statut actuel du Workflow. La première solution qui ne nous convient pas, consiste à placer des activités code, pour lesquelles, à chaque fois, nous réécrirons le code d'envoi de mail en personnalisant les informations à envoyer.
L'autre solution est de créer une activité personnalisable qui se chargera d'envoyer le mail pour nous et pour laquelle, nous n'aurons qu'à définir une ou deux propriétés comme le statut actuel du Workflow.

I-A - Création du projet et de la classe principale

Ouvrez Visual Studio et assurez-vous d'avoir installé les extensions pour Workflow Foundation, surtout si vous utilisez Visual Studio 2005. Créez un nouveau projet d'activité: Fichier > Nouveau > Projet > Workflow Activity Library. Renommez l'activité de base par le nom de l'activité personnalisée que vous utiliserez au long de cet article. Je vous propose pour commencer d'utiliser un simple « MonActivite ». Vous obtenez alors le code suivant :

public partial class MonActivite: SequenceActivity
{
public MonActivite()
{
InitializeComponent();
}
}

Vous remarquerez que votre activité hérite de SequenceActivity. Cela signifie que vous créez une activité séquentielle qui exécutera des activités enfants les unes après les autres. En effet, SequenceActivity hérite de CompositeActivity qui hérite lui-même d'Activity. Une activité composite est justement une activité qui peut contenir des enfants comme une BranchActivity* par exemple. (* activité déjà existante au sein de WF)

Bien sûr, libre à vous de n'hériter que de la classe Activity si votre activité n'en a pas besoin. Dans notre cas, notre activité ne fera qu'envoyer un mail et nous pouvons donc n'hériter que de Activity.

Nous allons maintenant rajouter deux propriétés Comment et StatusID, le second se basant sur une énumération correspondant aux statuts en base. L'énumération sert à la validation des ID, mais surtout à une sélection facilitée grâce à l'attribut Description de chaque valeur.

Énumération des statuts possibles

public enum WorkflowStatus
{
[Description("Workflow is not started")]
Unstarted = 0,
[Description("Workflow is started")]
Started = 1,
[Description("Workflow is waiting for an operation")]
Waiting = 2,
[Description("Workflow finished normally")]
Finished = 3,
[Description("Workflow has been aborted")]
Aborted = 4
}

Ainsi, nous allons enregistrer deux propriétés de dépendances CommentProperty et StatusProperty qui seront utilisées par nos propriétés.

Les propriétés de dépendance

public static DependencyProperty CommentProperty = DependencyProperty.Register("Comment", typeof(string), typeof(MonActivite));
public static DependencyProperty StatusIDProperty = DependencyProperty.Register("StatusID", typeof(WorkflowStatus), typeof(MonActivite));

Les best practices veulent qu'une propriété de dépendance se nomme du nom de la propriété associée concaténé au terme « Property ». Ici Comment+Property = CommentProperty

Propriétés Comment et StatusID

[Browsable(true)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public string Comment
{
get
{
return ((string)(base.GetValue(MonActivite.CommentProperty)));
}
set
{
base.SetValue(MonActivite.CommentProperty, value);
}
}

[Browsable(true)]
[Category("Developpez")]
[Description("The status to log")]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
public WorkflowStatus StatusID
{
get
{
return ((WorkflowStatus)(base.GetValue(MonActivite.StatusIDProperty)));
}
set
{
base.SetValue(MonActivite.StatusIDProperty, value);
}
}

Si vous regardez les attributs des propriétés, ceux-ci servent principalement à la personnalisation de la boite de propriétés de Visual Studio lorsque l'activité est configurée en mode Design.

I-B - Ajout du code métier

Nous allons maintenant définir notre code métier, à savoir le code d'envoi de mail. Ce code peut être une méthode interne à l'activité.

Voici notre méthode toute simple : on envoie un mail à l'administrateur du workflow pour lui signifier l'état courant du workflow ainsi qu'un commentaire.

private void SendMail()
{
MailMessage mail = new MailMessage("workflow@dvp.com", "admin@dvp.com")
{
Subject =
String.Format("WORKFLOW - {0}",
Enum.GetName(typeof (WorkflowStatus), StatusID)),
Body = this.Comment

};
SmtpClient smtp = new SmtpClient("smtp.free.fr");
smtp.Send(mail);
}

Il nous reste maintenant à appeler cette méthode. Bien entendu, cela ne se fait pas n'importe comment ni surtout, à n'importe quel moment.

Une activité a un cycle de vie particulier, elle est instanciée à un certain moment, ses méthodes sont appelées soit à l'initialisation de l'instance du workflow, soit lors de son exécution. Voici les différentes étapes du cycle de vie :
- OnActivityExecutionContextLoad ;
- Initialize ;
- Execute ;
- Uninitialize ;
- OnActivityExecutionContextUnload ;
- Dispose.

Généralement, deux étapes conviennent à l'injection du code métier. L'étape Initilize qui se déclenche dès que l'on crée une instance du workflow

WorkflowInstance instance = runtime.CreateWorkflow(typeof(Workflow1));

Et l'étape Execute, qui se déclenche lorsque le workflow « passe » par cette activité et l'exécute.

Dans notre cas, c'est l'étape Execute qui nous intéresse, car le but de notre activité est d'envoyer un mail à un moment spécifique de notre workflow séquentiel, là où nous aurons placé notre activité personnalisée.
Pour utiliser cette méthode, nous allons simplement la surcharger et laisser le workflow l'appeler. Nous allons ensuite pouvoir librement insérer l'appel à notre méthode SendMail et préciser que l'activité a fini son travail. Le fait de retourner le statut Closed précise au moteur de workflow qu'il peut passer à l'activité suivante.
Voici le code produit :

protected override ActivityExecutionStatus Execute(ActivityExecutionContext executionContext)
{
// on envoie le mail
SendMail();

return ActivityExecutionStatus.Closed; // puisque notre activité a fini tout son traitement

}

II - Modifier l'affichage de l'activité

II-A - Ajouter l'activité à la boite à outils

Maintenant que notre activité est prête, nous allons l'utiliser. Pour cela, créez un nouveau projet de type Workflow séquentiel dans une autre solution. Ouvrez le Workflow en mode designer et ouvrez le panneau latéral de boite à outils contenant les activités disponibles. Cliquez à l'aide du bouton droit sur le panneau des activités et choisissez le menu Choisir les éléments.

Votre activité n'est probablement pas présente dans la liste. Cliquez alors sur Parcourir et allez chercher la dll qui a été générée par votre projet. Une fois sélectionnée, l'(ou les) activité(s) devrai(en)t apparaitre et être cochée(s). Cliquez encore une fois sur OK pour voir se fermer la fenêtre et apparaitre le nom de votre activité.

Normalement, les contrôles personnalisés et les activités, à partir du moment où ils se trouvent dans une assembly séparée, s'ajoutent directement dans la boite à outils. Néanmoins, lorsque cela ne marche pas OU que les modifications d'affichage (comme nous verrons juste après) ne sont pas répercutées, alors il convient d'utiliser cette méthode pour recharger correctement l'assembly.

II-B - Changer l'icône affichée dans la boite à outils

Nous venons de voir comment notre activité peut apparaitre dans la boite à outils. Pour rendre cela un peu plus professionnel, et surtout pouvoir distinguer les activités par leur icône, comme vous le faites habituellement pour les contrôles, il nous suffit d'ajouter un attribut ToolboxBitmap

[ToolboxBitmap(typeof(MonActivite), "MyActivity.ico")]
public partial class MonActivite : Activity
{
...

MyActivity.ico est une image qui devra être placée dans la solution et mise en ressource intégrée. Pour ce faire, cliquez sur l'image dans l'arborescence puis, dans la fenêtre des propriétés, choisissez Ressource Intégrée pour la propriété Action de compilation.

Et c'est tout pour l'icône.

II-C - Changer l'affichage de l'activité

Lorsque l'on développe, que ce soit un composant réutilisable comme une activité, un custom contrôle ou simplement une ligne de code, il ne faut pas penser à comment nous pourrons l'utiliser, mais également comment un autre développeur pourra l'utiliser. Oui, il y aura toujours quelqu'un pour reprendre votre code et cette personne se demandera à chaque fois « à quoi sert cela ? ». C'est donc le rôle du premier développeur de faire en sorte que les développeurs suivants puissent comprendre et utiliser son code ou ses composants. Dans le cas d'une activité personnelle, il convient alors de documenter son code, mais également d'ajouter des petites aides visuelles comme les tooltips.

Pour ajouter un tooltip, dit aussi une infobulle, dans le fichier de votre activité perso, créez une nouvelle classe héritant d'ActivityDesigner

public class MyActivityDesigner : ActivityDesigner
{
...
}

Créez ensuite une méthode du nom de votre choix, dans laquelle vous appellerez la méthode héritée ShowInfoTip. Dans mon exemple, le texte est défini dynamiquement et je me sers de la description éventuellement saisie par le développeur dans les propriétés de l'activité pour ajouter un peu plus d'informations.

private void ToolTip()
{
const string title = "Developpez - Status Activity";
string text = "Change the status of the demand when activity is fired";
if (!string.IsNullOrEmpty(Activity.Description))
text += "\r\n\r\n" + Activity.Description;
ShowInfoTip(title, text);
}

Pour finir, dans la classe créez, surchargez les trois méthodes OnMouseEnter, OnMouseHover et OnMouseMove, et appelez votre méthode :

protected override void OnMouseEnter(System.Windows.Forms.MouseEventArgs e)
{
ToolTip();
}
protected override void OnMouseMove(System.Windows.Forms.MouseEventArgs e)
{
ToolTip();
}
protected override void OnMouseHover(System.Windows.Forms.MouseEventArgs e)
{
ToolTip();
}

Ajoutez également un attribut sur la classe de votre activité pour qu'il charge votre designer nouvellement créé :

[Designer(typeof(MyActivityDesigner),typeof(IDesigner))]
public partial class MyActivity: SequenceActivity

Dorénavant, lorsque votre souris passera au-dessus de l'activité, une bulle d'information s'affichera

La dernière chose à améliorer est la reconnaissance visuelle de notre activité sur le designer de Workflow. En effet, reconnaitre notre activité par son icône n'est pas le moyen le plus rapide pour notre cerveau. Mémoriellement, notre cerveau est plus rapide à associer des formes et surtout des couleurs à une information, que ne le fait une description textuelle. C'est pourquoi nous allons changer l'apparence visuelle de notre activité.
Nous allons changer les couleurs de notre activité, à la fois le fond, le conteur et la couleur de la police utilisée.

Pour cela, créez une classe interne et scellée héritant de ActivityDesignerTheme

internal sealed class MyActivityDesignerTheme : ActivityDesignerTheme
{
...
}

Puis définissez le constructeur :

public MyActivityDesignerTheme(WorkflowTheme theme): base(theme)
{
...
}

Dans lequel, vous n'avez qu'à redéfinir les propriétés d'affichage qui seront utilisées pour dessiner votre activité :

public MyActivityDesignerTheme(WorkflowTheme theme): base(theme)
{
BackColorEnd = Color.Pink;
BackColorStart = Color.GreenYellow;
BackgroundStyle = LinearGradientMode.ForwardDiagonal;
ForeColor = Color.Black;
}

Enfin, puisque nous venons créer un thème pour designer, nous devrons dire à notre Designer créé juste avant de charger ce thème. Pour cela, ajoutez l'attribut ActivityDesignerThemeAttribute sur votre classe MyActivityDesigner :

[ActivityDesignerThemeAttribute(typeof(MyActivityDesignerTheme))]
public class MyActivityDesigner : ActivityDesigner
{
...
}

Il nous reste plus qu'à admirer le résultat, certes peu joli dans mon cas, mais totalement personnalisable comme le montre l'exemple donné :

II-D - Modifier le comportement du designer en fonction de notre activité

Dans le cas où votre activité est une activité qui peut en contenir d'autres (vous héritez donc non plus de la classe Activity, mais de la classe SequentialActivity), vous souhaitez peut-être pouvoir contrôler le type d'activité qui sera autorisé à être inséré dans votre activité.

Afin de faire cela, il faut procéder en deux temps.

En premier lieu, il faut modifier notre activité afin qu'elle hérite de SequentialActivity.
Ensuite nous allons redéfinir la méthode void OnActivityChangeAdd(ActivityExecutionContext executionContext, Activity addedActivity) afin de vérifier le type de l'activité ajoutée.

protected override void OnActivityChangeAdd(ActivityExecutionContext executionContext, Activity addedActivity)
{
if( !(addedActivity is MychildActivity) )
throw new ArgumentException("addedActivity");
base.OnActivityChangeAdd(executionContext, addedActivity);
}

Mais laisser cela dans cet état pose problème. En effet rien ne vous empêche de drag/droper une activité du mauvais type, le résultat sera une exception, ce qui, disons-le, n'est pas super pratique.
Afin de combler cette lacune nous allons modifier la classe designer associée à notre activité afin qu'elle hérite de la classe SequentialActivityDesigner.

L'étape suivante est de trouver un moyen pour que le drag/drop ne soit autorisé que si l'activité en cours de drap/drop est du bon type. Heureusement le framework .NET est bien conçu :)

public class MyActivityDesigner : SequentialActivityDesigner
{
public override bool CanInsertActivities(HitTestInfo insertLocation,
System.Collections.ObjectModel.ReadOnlyCollection<System.Workflow.ComponentModel.Activity> activitiesToInsert)
{
foreach (System.Workflow.ComponentModel.Activity activity in activitiesToInsert)
{
if (!(activity is MyActivity))
return false;
}

return base.CanInsertActivities(insertLocation, activitiesToInsert);
}
}

La méthode CanInsertActivties du designer est appelée à chaque fois qu'une ou plusieurs activités sont insérées grâce au designer.

III - Mettre en place un système de validation WF

Notre activité est fonctionnelle, son interface a été revue et améliorée, mais il nous manque une chose importante : la validation des données. Parmi les erreurs les plus courantes des développeurs se trouve l'oubli de la validation des saisies utilisateurs. Même ici, où c'est le développeur qui saisira les informations (ici notre propriété Comment), il est nécessaire de valider la saisie. En effet, comment réagirait notre application si la propriété Commentaire n'avait pas été saisie ? Dans le meilleur des cas, un mail vide partirait, ce que nous voulons à tout prix éviter.

Comment s'assurer que notre développeur utilise correctement notre activité ? Simplement en utilisant des validateurs de saisie, un peu l'équivalent d'un RequireValidator, utilisé pour forcer la saisie d'un contrôle TextBox par exemple.

Cela n'a, heureusement pour nous, rien de complexe. Nous allons simplement créer une classe personnalisée et à l'aide d'attributs spécifiques, le designer de Visual Studio sera en mesure de l'utiliser et d'imposer une utilisation précise de notre activité.
Créez une classe MonActiviteValidator qui hérite de ActivityValidator.

Maintenant que nous avons notre classe prête à être utilisée, nous devons préciser ce que nous voulons qu'elle valide. Elle ne peut pas en effet, deviner quelles propriétés sont obligatoires. Nous allons alors surcharger la méthode de validation :

public override ValidationErrorCollection Validate(ValidationManager manager, object obj)
{
// votre code de validation
}

Remarquons tout de suite les paramètres qui seront automatiquement passés, à savoir le gestionnaire de validation : manager et obj, un objet qui représente en fait notre activité.

Nous devons pour commencer, caster notre objet afin de récupérer une instance de notre activité :

MonActivite crw = obj as MonActivite;
if (crw == null)
{
throw new InvalidOperationException();
}

Enfin, pour nous attaquer à notre validation personnalisée, nous appelons la méthode Validate de la classe héritée pour vérifier que notre activité est valide en récupérant une liste d'erreurs éventuelles, puis, nous allons, à l'aide d'un test, ajouter une erreur supplémentaire si notre propriété Comment, que nous voulons obligatoire, n'est pas renseignée.

ValidationErrorCollection errors = base.Validate(manager, obj);

if (string.IsNullOrEmpty(crw.Comment) &&
crw.GetBinding(MonActivite.CommentProperty) == null)
{
errors.Add(new ValidationError("Un commentaire est nécessaire", 100, false, "Comment"));
}

C'est tout. Pas besoin de lancer une exception pour afficher notre validateur. La seule chose à faire est de rajouter à la collection d'erreurs, notre erreur personnalisée. Le designer se chargera alors d'afficher le message et de bloquer la compilation.

Pour finir, il nous faut rajouter l'attribut ActivityValidator à notre activité, tout en précisant le nom de la classe que l'on vient de créer :

[ActivityValidator(typeof(MonActiviteValidator))]
public partial class MonActivite : Activity
{
...

Le message obtenu en cas de non-définition du commentaire, ressemblait donc à ceci :

IV - Utilisation de l'activité personnalisée

Pour valider notre activité personnalisée, nous allons créer un workflow séquentiel le plus simple possible dans lequel nous déposerons simplement notre activité sans rien d'autre à côté.

Pour finir et pour vérifier que tout se passe comme nous l'avons prévu, nous allons simplement créer un projet WinForms avec un bouton grâce auquel nous lancerons notre workflow :

private void btnStart_Click(object sender, EventArgs e)
{
WorkflowRuntime runtime = new WorkflowRuntime();
runtime.StartRuntime();
WorkflowInstance instance = runtime.CreateWorkflow(typeof(Workflow1));
instance.Start();
}

Il ne nous reste plus qu'à lancer l'application, cliquer sur le bouton et vérifier que le mail est bien parti.

V - Création d'une activité « Root »

Il existe un type d'activité un peu particulier que l'on peut également modifier. Il s'agit de l'activité « root ».

La plus connue des activités « root » est SequentialWorkflowActivity.
Elle permet de représenter un workflow sous forme d'un enchainement d'activités.
De plus c'est son affichage, ou plus exactement c'est l'affichage de son designer, qui conditionne la façon dont la surface du designer de workflow est affichée.

Par défaut une SequentialWorkflowActivity affiche trois vues dans le designer : Exécution, Gestionnaire d'erreur et Gestionnaire d'annulation.
Nous allons créer une activité root qui ne propose qu'une seule vue : l'exécution.

V-A - Création de notre activité « root »

La première, et à vrai dire la seule chose à faire dans notre exemple, c'est de créer une classe qui hérite de SequentialWorkflowActivity

[Designer(typeof(MySequentialWorkflowActivityDesigner), typeof(IRootDesigner)), ToolboxItem(false)]
public class MySequentialWorkflowActivity : SequentialWorkflowActivity
{
}

Nous avons notre activité root de perte. Dans cet exemple simple, nous nous contentons de redéfinir le designer associé à l'activité. Il va sans dire que vous pouvez enrichir cette activité, notamment en personnalisant son comportement lors de l'exécution.

V-B - Création du designer associé

Afin de personnaliser la façon dont notre activité root va être affichée nous allons redéfinir le designer associé en créant une classe qui hérite de SequentialWorkflowRootDesigner
Cet héritage va nous permettre d'accéder à certaines propriétés marquées « protected » et surtout de modifier le comportement d'autres.

public class RefreshSequentialWorkflowActivityDesigner : SequentialWorkflowRootDesigner
{

protected override void Initialize(System.Workflow.ComponentModel.Activity activity)
{
base.Initialize(activity);

this.Header.Text = "Mon super designer de workflow !";
}

public override System.Collections.ObjectModel.ReadOnlyCollection<DesignerView> Views
{
get
{
return new System.Collections.ObjectModel.ReadOnlyCollection<DesignerView>(new DesignerView[] { base.Views[0] });
}
}
}

La partie intéressante de ce code se situe dans la redéfinition de la propriété Views.
En effet dans notre cas nous empêchons l'affichage des deux autres vues créées par défaut, mais rien ne vous empêche de définir votre propre vue et de l'ajouter ici.

L'autre petite « customisation » vient du changement du titre affiché dans le designer.

VI - Conclusion

Comme nous l'avons vu, il est très facile de créer sa propre activité personnalisée. Il ne s'agit pas uniquement de créer une activité dont on aura modifié l'affichage pour la rendre esthétique, mais bien d'une possibilité de représenter du fonctionnel métier au travers d'activités, qui seront utilisables au sein des différents workflows de l'entreprise. Cette possibilité permet sur le moyen et long terme, d'accélérer le développement de workflows et surtout d'assurer la réutilisabilité de composants afin mieux contrôler certains processus métier, parallèles aux workflows fonctionnels.

VII - Liens et téléchargements

Cliquez ici pour télécharger les sources de l'article.

VIII - Remerciements

Nous tenons à remercier ram-0000 pour ses corrections et conseils apportés à l'article