- Le patterne Creat/Set/use ne parle certainement pas à grand monde. En fait l'idée est toute simple. Etant donné que votre activité va devoir passer par une sérialisation puis un désserialisation, il est impératif que vos activités aient une structure propre, avec : Un constructeur par défaut et des propriétés sérialisées avec des accesseurs Get et Set.
En thérorie on doit déjà avoir tout cela... même si bien souvent on néglige le constructeur par défaut (dommage pour certaine optimisations)
- Utiliser l'attribut [DefautValue(...)] afin de minimiser le Xaml à écrire. Si le designer ne touche pas à votre propriété, celle-ci n'ajoutera pas de Xaml car il est sous-entendu qu'elle utilise sa valeur par défaut.
- Utiliser l'attribut [DependsOn("..")]. Il s'agit là d'une idée purement esthétique dont l'objectif est de contraindre le Xaml a être écrit dans un ordre précis. Pour cela on donne à l'attribut le nom de la propriété à laquelle on veut que la propriété succède... c'est une histoire de goûts. Ceci fontionne sur tout type de propriété.
- Utiliser l'attribut [ContentProperty] afin d'indiquer la propriété censée faire office de contenu de votre activité. Quand on n'a qu'un Body, ou une collection Activities, on va la désigner comme ContentProperty. Ceci réduit grandement le Xaml.using System; using System.Activities; using System.ComponentModel; using System.Data.Objects; using System.Windows.Markup; namespace MyLib.WF4.EntityFramework { /// <summary> /// Activity based on NativeActivity<TResult> /// </summary> [ContentProperty("Body")] public sealed class EntityScope : NativeActivity { public const String ObjectContextName = "ObjectContext"; [DefaultValue(null)] [RequiredArgument] [Browsable(true)] [DependsOn("SaveChanges")] public InArgument<ObjectContext> ObjectContext { get; set; } [DefaultValue(true)] [Browsable(true)] public Boolean SaveChanges { get; set; } [DefaultValue(null)] [Browsable(false)] public Activity Body { get; set; } public EntityScope() { this.SaveChanges = true; } /// <summary> /// Execute /// </summary> /// <param name="context">WF context</param> /// <returns></returns> protected override void Execute(NativeActivityContext context) { if (this.Body != null) { ObjectContext obj = this.ObjectContext.Get(context); context.Properties.Add(ObjectContextName,obj ); context.ScheduleActivity(this.Body,new CompletionCallback( this.BodyCompletionCallback)); } } /// <summary> /// Body Completion Callback /// </summary> /// <param name="context"></param> /// <param name="completedInstance"></param> private void BodyCompletionCallback(NativeActivityContext context, ActivityInstance completedInstance) { ObjectContext c = this.ObjectContext.Get(context); if (c != null) { if (this.SaveChanges) { c.SaveChanges(); } c.Dispose(); c = null; } } /// <summary> /// Register activity's metadata /// </summary> /// <param name="metadata"></param> protected override void CacheMetadata(NativeActivityMetadata metadata) { // [ObjectContext] Argument must be set if (this.ObjectContext == null) { metadata.AddValidationError( new System.Activities.Validation.ValidationError( "[ObjectContext] argument must be set!", false, "ObjectContext")); } else { RuntimeArgument arg = new RuntimeArgument(ObjectContextName, typeof(ObjectContext), ArgumentDirection.In); metadata.AddArgument(arg); metadata.Bind(this.ObjectContext, arg); } // [Body] Argument must be set if (this.Body == null) { metadata.AddValidationError( new System.Activities.Validation.ValidationError( "[Body] argument must be set!", false, "Body")); } else { metadata.AddChild(this.Body); } } } }
<mwe:EntityScope />
<mwe:EntityScope SaveChanges="False" ObjectContext="[New DemoModel()]"> <Sequence/> </mwe:EntityScope>
<mwe:EntityScope ObjectContext="{x:Null}" SaveChanges="True"> <mwe:EntityScope.Body> <Sequence /> </mwe:EntityScope.Body> </mwe:EntityScope>- Utiliser les ExecutionProperties pour partager des données entre une activités parente et ses enfants. Ceci facilite la lecture du workflow et réduit grandement le Xaml car l'affectation de la valeur sur l'ExecutionPropertie n'a lieu que sur l'activité parente (le scope).
Pour plus d'informations à ce sujet, j'ai réalisé un article dédié aux ExecutionProperties et à leurs avantages : [WF4] Un petit peu d'ExecutionProperties avec Entity Framework ;)
Bien évidement on nous conseil de prendre un nom proche du type affecté à nos ExecutionProperties (comme dans mon EntityScope).
- Utiliser l'attribut [RequiredArguments] pour indiquer les arguments devant avoir une valeur. Simple et efficace (mais ne fonctionne que sur les arguments, pas sur des propriétés CLR ou des activités... donc attentions!!!).- Utiliser l'attribut [OverloadGroups] pour regrouper des attributs requis. Si on a plusieurs groupes sur une activité, il suffit qu'un groupe ait ses propriété affectées pour valider l'activité. Très pratique, ceci évite de monter une logique de psychopathe dans les méthodes CacheMetaData de vos activités.
La MSDN a un très bon exemple sur ces deux sujets : Arguments obligatoires et groupes surchargés.
Si vous jouez la metadata pour ajouter une logique de validation à vos activités, ce slide vous est dédié.Il indique clairement que le Boolean warning/error du constructeur de la classe ValidationError a été prévu pour vous permettre la remonté d'erreurs pour les cas bloquants et la remonté de warnings pour les cas non bloquants mais sur lesquelles vous voulez mettre l'accent. A ce sujet, Leon Welicki a dit que le warning était présent mais qu'aucune activité de base de WF4 ne l'utilisait.
Gardons à l'esprit que même si le warning n'est pas encore très utilisé, il a son utilité. Vos utilisateurs risquerons d'être surpris à son apparition dans vos activités.