Réaliser une activité chargée d’incrémenter une valeur numérique n’est pas un exercice très compliqué (quoi que…). Les manières pouvant nous permettre d’arriver à nos fins sont tellement nombreuses que l’on peut vite arriver à un résultat sans pour autant respecter les bonnes pratiques de Workflow Foundation.

Pour résumé l’objectif : Je cherche à coder une activité générique Incremente<T> exposant un argument To de type InOutArgument<T>.

En soit les choses sont simples, mais comment faire une opération +1 sur un type inconnu :(.

1ère approche : La CodeActivity

Pas forcément la meilleur approche, mais celle qui viendra le plus à l’esprit d’un développeur non habitué à WF4. J’utilise Linq pour construire une expression qui fera le calcule et qui me retournera le résultat.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Linq.Expressions;

namespace Demo.Wf.GenericActivity
{
    /// <summary>
    /// Activity based on CodeActivity
    /// </summary>
    public sealed class Incremente<T> : CodeActivity
    {
        // Define an activity input argument of type T
        public InOutArgument<T> To { get; set; }

        /// <summary>
        /// Execute
        /// </summary>
        /// <param name="context">WF context</param>
        protected override void Execute(CodeActivityContext context)
        {
            try
            {
                // Récupération de la valeur à incrémenter
                T value = context.GetValue(this.To);

                Func<T> add = Expression.Lambda<Func<T>>(
                        Expression.Add(
                            Expression.Constant(value),
                            Expression.Constant(1))
                            ).Compile();

                // Execution de value ++
                value = add.Invoke();

                this.To.Set(context, value);
            }
            catch (Exception ex)
            {
                throw ex;
            }
        }

        /// <summary>
        /// Register activity's metadata
        /// </summary>
        /// <param name="metadata"></param>
        protected override void CacheMetadata(CodeActivityMetadata metadata)
        {
            // Register In arguments
            RuntimeArgument arg = new RuntimeArgument("To", typeof(T), ArgumentDirection.InOut);
            metadata.AddArgument(arg);
            metadata.Bind(this.To, arg);

            // [Text] Argument must be set
            if (this.To == null)
            {
                metadata.AddValidationError(
                    new System.Activities.Validation.ValidationError(
                        "[To] argument must be set!",
                        false,
                        "To"));
            }
        }
    }
}

Cette approche est celle qui se conforme le moins à WF4, car dans la mesure du possible, on doit privilégier la composition à la création. Cette activité pourrait être réalisée par composition d’une activité à partir d’autres activités, on est donc sensé l’oublier si on veut les respecter.

Inconvénient :

  • Non respect des bonnes pratiques privilégiant la composition à la création.
  • Pas de contrôle lors du design, il faudrait faire une série de tests sur le type T pour savoir si on peut faire une addition.

 

2de approche : La composition

En théorie, la meilleur approche!

J’ai réalisé mon activité à partir d’une séquence contenant une activité Assign et j’utilise une expression Vb pour réaliser le fameux calcul sur T. Tout le code important se trouve dans la méthode GetImplementation() qui contient l’activité composée.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Activities.Statements;
using Microsoft.VisualBasic.Activities;

namespace Demo.Wf.GenericActivity
{
    public class Incremente<T> : Activity
    {
        // Define an activity input argument of type T
        public InOutArgument<T> To { get; set; }

        // Implementation cache
        private readonly Func<Activity> m_Implementation;

        /// <summary>
        /// New
        /// </summary>
        public Increment2()
        {
            this.m_Implementation = new Func<Activity>(this.GetImplementation);
        }

        /// <summary>
        /// Implementation
        /// </summary>
        protected override Func<Activity> Implementation
        {
            get { return this.m_Implementation; }
            set { }
        }

        /// <summary>
        /// Get Implementation
        /// </summary>
        /// <returns></returns>
        private Activity GetImplementation()
        {
            Variable<T> v = new Variable<T>("v", c => this.To.Get(c));
            return new Sequence
            {
                Variables = { v },
                Activities =
                    {
                        new Assign
                        {
                            To = new OutArgument<T>(c => this.To.Get(c)),
                            Value = new InArgument<T>(new VisualBasicValue<T>("v + 1"))
                        }
                    }
            };
        }

        /// <summary>
        /// Register activity's metadata
        /// </summary>
        /// <param name="metadata"></param>
        protected override void CacheMetadata(ActivityMetadata metadata)
        {
            // Register In arguments
            RuntimeArgument arg = new RuntimeArgument("To", typeof(T), ArgumentDirection.InOut);
            metadata.AddArgument(arg);
            metadata.Bind(this.To, arg);

            // [Text] Argument must be set
            if (this.To == null)
            {
                metadata.AddValidationError(
                    new System.Activities.Validation.ValidationError(
                        "[To] argument must be set!",
                        false,
                        "To"));
            }
        }
    }
}

Facile à mettre en œuvre cette activité tire profit de WF4 et rien n’est réinventer. On est sensé respecter ici les bonnes pratiques.

Mais on se retrouve avec une Variable intermédiaire… On peut faire mieux.

Inconvénient :

  • On instancie une variable supplémentaire.
  • On encapsule 2 activités pour une seule opération... on peut faire mieux.
  • Le calcul est dans une String, ce qui pourra poser problème en cas de calculs plus complexes ou de refactoring :(
  • Pas de contrôle lors du design, il faudrait faire une série de tests sur le type T pour savoir si on peut faire une addition.

 

3ème approche : La composition optimisée

L’idée est la même que la précédente : on tire partie de la composition mais on cherche à supprimer les éléments de trop et ceux qui peuvent être perturbateurs.

Je cible donc la suppression de :

  • La séquence
  • La variable
  • L’expression Visual Basic sous forme de String.

Heureusement WF4 dispose d’une série d’activités dédiées au traitement d’expressions. Ne les cherchez pas dans la ToolBox, elles n’y sont pas. Mais vous pouvez les trouver dans le namespace : System.Activities.Expressions. Pour mon exemple je vais utiliser le Add<Left,Right,Result>.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Activities.Statements;
using Microsoft.VisualBasic.Activities;
using System.Activities.Expressions;

namespace Demo.Wf.GenericActivity
{
    public class Incremente<T> : Activity
    {
        // Define an activity input argument of type T
        public InOutArgument<T> To { get; set; }

        // Implementation cache
        private readonly Func<Activity> m_Implementation;

        /// <summary>
        /// New
        /// </summary>
        public Increment3()
        {
            this.m_Implementation = new Func<Activity>(this.GetImplementation);
        }

        /// <summary>
        /// Implementation
        /// </summary>
        protected override Func<Activity> Implementation
        {
            get { return this.m_Implementation; }
            set { }
        }

        /// <summary>
        /// Get Implementation
        /// </summary>
        /// <returns></returns>
        private Activity GetImplementation()
        {

            return new Assign<T>
                {
                    To = new OutArgument<T>(c => this.To.Get(c)),
                    Value = new InArgument<T>
                    {
                        Expression = new Add<T, Int32, T>
                        {
                            Left = new InArgument<T>(c => this.To.Get(c)),
                            Right = 1
                        }
                    }
                };
        }

        /// <summary>
        /// Register activity's metadata
        /// </summary>
        /// <param name="metadata"></param>
        protected override void CacheMetadata(ActivityMetadata metadata)
        {
            // Register In arguments
            RuntimeArgument arg = new RuntimeArgument("To", typeof(T), ArgumentDirection.InOut);
            metadata.AddArgument(arg);
            metadata.Bind(this.To, arg);

            // [Text] Argument must be set
            if (this.To == null)
            {
                metadata.AddValidationError(
                    new System.Activities.Validation.ValidationError(
                        "[To] argument must be set!",
                        false,
                        "To"));
            }
        }
    }
}

Plus agréable à lire. Et comme convenu je me suis séparé des parasites en trop ;)

Mais l’activité Assign est peut être de trop…

On doit pouvoir utiliser Add pour gérer notre expression sans Assign, vu que Add est une Activité…

Inconvénient :

  • Si on test cette activité on a un souci : en utilisant le Add<T,Int32,T> on a cassé la généricité et on ne peut plus utiliser autre chose que des entier dans le calcul. On pourrait corriger le tire en faisant une conversion.
  • Assign ne sert plus à rien :(

 

4ème approche : La composition basée uniquement sur Add<T,T,T>

En corrigeant Add<T,Int32,T> et en le remplaçant par Add<T,T,T> mon activité redevient générique et je supprime Assign :

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Activities;
using System.Activities.Statements;
using System.Activities.Expressions;
using System.ComponentModel;

namespace Demo.Wf.GenericActivity
{
    public class Incremente<T> : Activity
    {
        // Define an activity input argument of type T
        public InOutArgument<T> To { get; set; }

        // Implementation cache
        private readonly Func<Activity> m_Implementation;

        /// <summary>
        /// New
        /// </summary>
        public Increment()
        {
            this.m_Implementation = new Func<Activity>(this.GetImplementation);
        }

        /// <summary>
        /// Implementation
        /// </summary>
        protected override Func<Activity> Implementation
        {
            get { return this.m_Implementation; }
            set { }
        }

        /// <summary>
        /// Get Implementation
        /// </summary>
        /// <returns></returns>
        private Activity GetImplementation()
        {
            return new Add<T, T, T>
                {
                    Left = new InArgument<T>(c => this.To.Get(c)),
                    Right = (T)Convert.ChangeType(1, typeof(T)),
                    Result = new OutArgument<T>(c => this.To.Get(c))
                };
        }

        /// <summary>
        /// Register activity's metadata
        /// </summary>
        /// <param name="metadata"></param>
        protected override void CacheMetadata(ActivityMetadata metadata)
        {
            // Register In arguments
            RuntimeArgument arg = new RuntimeArgument("To", typeof(T), ArgumentDirection.InOut);
            metadata.AddArgument(arg);
            metadata.Bind(this.To, arg);

            // [Text] Argument must be set
            if (this.To == null)
            {
                metadata.AddValidationError(
                    new System.Activities.Validation.ValidationError(
                        "[To] argument must be set!",
                        false,
                        "To"));
            }
        }
    }
}

Plus simple, on ne peut pas ;). Toute la composition est basée sur une seule activité inaccessible par défaut via la ToolBox et on reste sur un code générique et sécurisée. En cas d’utilisation d’un type sur laquelle l’addition ou la conversion sont impossibles, on est directement prévenu. Donc, pas de surprises.

Inconvénient : N’a pas d’inconvénients ;)

 

Moralité : Tout cela pour vous démontrer que même si avec WF4 vous arrivez très vite à des résultats, il faut rester prudent et critique avec son code. Regardez donc toujours d’un peu plus près vos activités. Vous trouverez certainement quelque chose qui peut être allégé. Ce sont les hôtes de vos Workflows qui vous remercierons ;)