Voici la retranscription de mes notes sur la 5ème et en théorie dernière vidéo de la série de bonnes pratiques sur la réalisation d’activités custom avec WF4. Cette vidéo arrive sur un sujet très peu traité jusqu’ici sur le web : l’optimisation du Xaml généré par le designer quand il utilise nos activités.

Au total, ce sont presque 5 slide qui sont dédiées à ce sujet… autant dire que l’on a des choses à faire avec Xaml et WF4 ;)

1 – Sérialisation Xaml

CustomActivities_BestPractices8 - 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é.

CustomActivities_BestPractices9 - 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.

Afin de présenter les bienfait de ces pratiques, j’ai repris mon activité EntityScope que j’ai présenté dernièrement. Voici donc son code respectant les bonnes pratiques :

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);
            }
        }
    }
}

Si j’insert cette activité dans un workflow en laissant ses propriétés par défaut j’aurai le Xaml suivante :

<mwe:EntityScope />

Et si je change ces propriétés et que j’insert une séquence dans le body:

<mwe:EntityScope SaveChanges="False" ObjectContext="[New DemoModel()]">
  <Sequence/>
</mwe:EntityScope>
Les attributs sont dans l’ordre voulu et la séquence qui se trouve dans le body devient le contenu de mon activité

Et si je retire mes attributs mais que je n’affecte aucune valeur à mes propriétés, hors mis une séquence dans le body :

<mwe:EntityScope ObjectContext="{x:Null}" SaveChanges="True">
  <mwe:EntityScope.Body>
    <Sequence />
  </mwe:EntityScope.Body>
</mwe:EntityScope>

Je crois que le constat est claire. Sur une aussi petite activité, le Xaml est déjà bien plus important.

3 – Les propriété d’exécution

CustomActivities_BestPractices10 - 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).

4 – La validation

CustomActivities_BestPractices11 - 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.

5 – La validation (règles à respecter error/warning)

CustomActivities_BestPractices12  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.

Lien vers la vidéo : endpoint.tv - Workflow and Custom Activities - Best Practices (Part 5)

@+ et merci pour les mails d’encouragements que j’ai reçu ces derniers mois au sujet de mes articles sur Workflow Foundation 4 ;)