Workflow Foundation n’est pas qu’une simple “boite noire”. C’est un puissant outil qui peut permettre entre autre de contrôler la manière dont un utilisateur design son workflow à partir de vos activités. On peut très bien contraindre un utilisateur à ne pas utiliser telle ou telle activité avec la votre… ou imaginer des scénarios totalement farfelus. Tout est possible à partir du moment où l’on a le courage d’aller voir ce qui se cache dans le namespace System.Activities.Validation.

Pour ajouter une contrainte à vos activités, le plus simple consiste à coder une méthode statique retournant un type Constraint. Les plus tordus ou courageux iront peut être coder une classe custom héritant de Constraint. L’objectif est d’avoir une instance de type Constraint et de l’ajouter à la collection Constraints de l’activité. Ce qui se fait facilement via le constructeur de l’activité.

Personnellement ma préférence va dans le sens de coder une classe statique exposant x méthodes statiques génériques. Ceci me permet d’avoir un ajout de contraintes qui se fait comme ceci (MyConstraints étant ma classe statique et GetConstraintNoWriteLine<T> l’une de ses méthodes) :

public sealed class NoWriteLineScope : NativeActivity
{
    public NoWriteLineScope()
    {
        this.Constraints.Add(MyConstraints.GetConstraintNoWriteLine<NoWriteLineScope>());
    }
}

Afin de vous donner un peu envie j’ai sortie de son contexte une paire de contraintes custom. que j’expose via une classe statique MyConstraints.

Pour commencer simple, voici une méthode retournant une contrainte interdisant d’avoir plus de X activités dans une activité.

public static Constraint GetConstraintMaxChildren<T>(Int32 max) where T : Activity
{
    DelegateInArgument<T> myActivity = new DelegateInArgument<T>();
    DelegateInArgument<ValidationContext> context = new DelegateInArgument<ValidationContext>();
    Variable<IEnumerable<Activity>> children = new Variable<IEnumerable<Activity>>();

    return new Constraint<T>
    {
        Body = new ActivityAction<T, ValidationContext>
        {
            Argument1 = myActivity,
            Argument2 = context,
            Handler = new Sequence
            {
                Variables =
                {
                    children 
                },
                Activities =
                {
                    // Récupération de la liste des activité enfantes
                    new GetChildSubtree
                    {
                        ValidationContext = context,
                        Result = children 
                    },
                    // Test final
                    new AssertValidation
                    {
                        Assertion = new InArgument<Boolean>(c => children.Get(c).Count() < max ),
                        Message = new InArgument<String>(String.Format("Cette activité ne peut pas contenir plus de {0} activités!",max-1)),                                
                        PropertyName = new InArgument<String>(c => myActivity.Get(c).DisplayName)
                    }
                }
            }
        }
    };
}

Comme vous pouvez le voir, la création d’une contrainte passe par la composition d’un workflow aillant pour base Constraint<T>. T étant l’activité devant répondre à la contrainte fixée.

Les DelegateInArgument<> ne doivent pas vous perturber plus que cela. Il faut juste retenir qu’ils servent à établir dynamiquement le lien entre l’activité et le contexte de validation au moment du design. Le plus intéressant se trouve dans la propriété Handler de l’ActivityAction<>. C’est cette activité qui contient le workflow de contrainte. On peut y glisser toutes sortes d’activités, mais les plus utiles proviennent de System.Activities.Validation.

Dans mon exemple, j’utilise deux de ces activités :

  • GetChilSubtree : permet de retourner la liste des activités contenue dans notre activité custom (quel que soit le niveau d’imbrication).
  • AssertVamlidation : permet de signifier à l’activité qu’elle respecte ou non la contrainte fixée en vérifiant la propriété Assertion.

Vous l’aurez bien compris, votre workflow serra donc presque toujours en deux partie : la première exécutant éventuellement la logique de validation, et la seconde contrôlant le résultat de la première et remontant d’éventuels messages de violation de contrainte.

Dans mon exemple, je récupère la liste des activités enfants de mon activité et je les compte. Si le nombre d’activité est inférieur à mon maximum toléré, tout vas bien. Si non, mon workflow vas passer au rouge comme ceci :

wf4_constraint0

Si mon activité contenait le code suivant :

public sealed class LimitedScope : NativeActivity
{
    public Activity Body { get; set; }

    public LimitedScope()
    {
        this.Constraints.Add(MyConstraints.GetConstraintMaxChildren<LimitedScope>(3));
    }

// ...
}

On voit ici très bien à quel point cette fonctionnalité est facile à mettre en ouvre et à reproduire quand on a isolé ses contraintes ;)

 

Histoire de remettre une petite couche sur le sujet, voici une autre contrainte interdisant l’ajout d’une activité de type WriteLine :

public static Constraint GetConstraintNoWriteLine<T>() where T : Activity
{
    // L'activitité qui a la contrainte
    DelegateInArgument<T> myActivity = new DelegateInArgument<T>();
    // Le context de validation
    DelegateInArgument<ValidationContext> context = new DelegateInArgument<ValidationContext>();

    Variable<IEnumerable<Activity>> children = new Variable<IEnumerable<Activity>>();
    Variable<Int32> i = new Variable<Int32>("i", 0);
    Variable<Boolean> writeLineExist = new Variable<Boolean>("result", false);

    return new Constraint<T>
    {
        Body = new ActivityAction<T, ValidationContext>
        {
            Argument1 = myActivity,
            Argument2 = context,
            Handler = new Sequence
            {
                Variables =
                {
                    children, i, writeLineExist 
                },
                Activities =
                {
                    // Récupération de la liste des activité enfantes
                    new GetChildSubtree
                    {
                        ValidationContext = context,
                        Result = children 
                    },
                    // Boucle tant que l'on a
                    // pas trouvé un WriteLine
                    // ou que la liste n'a pas été parcourue
                    new While (c=> writeLineExist.Get(c) == false && i.Get(c) < children.Get(c).Count())
                    {
                        Body = new Sequence 
                        {
                            Activities = 
                            {
                                // Test si on a un WriteLine dans le 'childrenIdea'
                                new If(c=> children.Get(c).ElementAt(i.Get(c)).GetType() == typeof(WriteLine))
                                {
                                    // Si oui on affect un Boolean true au result
                                    Then = new Assign<Boolean>
                                    {
                                        To = writeLineExist,
                                        Value = true
                                    }
                                },
                                // Incrémentation de i (i++)
                                new Assign<Int32>
                                {
                                    To = i,
                                    Value = new InArgument<int>(c => i.Get(c) + 1)
                                }
                            }
                        }
                    },
                    // Test final
                    new AssertValidation
                    {
                        Assertion = new InArgument<Boolean>(c => !writeLineExist.Get(c)),
                        Message = new InArgument<String> ("Cette activité ne peut pas contenir de WriteLine"),                                
                        PropertyName = new InArgument<String>(c => myActivity.Get(c).DisplayName)
                    }
                }
            }
        }
    };
}

Ce workflow est un peu plus évolué mais son comportement est très simple :

  • Une activité GetChildSubtree va recherche l’ensemble des activités enfants (children).
  • Une boucle While va parcourir children à la recherche d’un éventuelle activité de type WriteLine.
  • Dans le corps du While, on incrémente i et si on trouve une activité WriteLine, on passe la variable writeLineExist à true
  • L’AssertValidation n’a plus qu’à vérifier l’état de writeLineExist.

Et voila ;)

 

WF4 est riche, très riche en fonctionnalités… je crois que je n’ai pas fini de la dire.