J’étais sur le point de transposer mes notes relatives à la dernière vidéo d’EndPointTv sur les bonnes pratiques WF4 quand je me suis rendu compte qu’il fallait parler d’ExecutionProperties alors que je n’y avais jamais fait allusion dans mes précédents articles. Je vais donc m’efforcer de corriger cette lacune ici ;)

ExecutionProperties : quel nom barbare pour une chose si pratique. Vous ne le savez certainement pas, mais vous en avez peut être déjà utilisé. L’objectif de ces “propriétés” et de permettre le partage d’informations entre une activité parente et ses enfants. En général les ExecutionProperties son utilisées dans une activité de type “Scope”.

Le meilleur exemple est très certainement le CorrelationScope avec sa propriété CorrelatesWith. Quand on affecte un CorrelationHandle à cette propriété, toute activité sachant utiliser cette ExecutionProperty, utilisera sa valeur en lieu et place de sa propre propriété CorrelatesWith (si celle-ci n’est pas déjà définie). On réduit alors le travail à faire lorsque l’on design un workflow, et on améliore considérablement l’expérience utilisateur.

Mais ce type de propriétés peut aller beaucoup plus loin en permettant à une activité de mettre à disposition des données qu’elle n’expose pas elle même en tant que propriétés. Par exemple l’activité Rethrow, qui si elle se trouve dans un TryCatch, peut faire un Throw d’une exception qu’elle trouve dans les ExecutionsProperties ajoutées par le TryCatch.

Dans le principe et l’idée que l’on s’en fait, les Executions Properties sont des propriétés “attachés aux activités” lors de leur exécution. Elle ne sont pas présentes hors du contexte d’exécution et peuvent être “considérées comme” des activités. Cela n’est pas vraiment le cas, mais les limitations de ses mécanismes internes sont telles que l’on assimile ces propriétés à des activités. Ce qui nous pousse à considérer ceci vient du fait que ce type de propriétés ne peut pas être utilisé dans une CodeActivity (seule type d’activité ne pouvant pas programmer l’exécution d’une autre activité ou évaluer une Activity<T>).

Plutôt que de partir sur de l’abstrait, j’ai décidé de présenter ici une petite partie d’un projet personnel permettant la manipulation de données via Entity Framework. Pour l’exemple je ne présenterai ici que 4 activités basiques :

  • EntityScope : Permet d’avoir une scope chargé de partager un ObjectContext entre pleusiuers activités (ceci par le biais d’une Execution Property)
  • AddEntity<T> : Ajoute à un ObjectContext une entité de type T héritant bien entendu d’EntityObject .
  • DeleteEntity<T> : Supprimer d’un ObjectContext une entité de type T héritant bien entendu d’EntityObject.
  • SaveChanges : Execute la méthode SaveChages de l’ObjectContext

Ah la différence du sample WF4 qui contient un scénario WF+EF, j’ai voulu coder des activités pouvant fonctionner dans un scope avec ExecutionPropertie aussi bien qu’en dehors.

Voici un petit Workflow mettant en scène tout ce petit monde :

 wf_exemple_executionProperties

Pour commencer, je vais décrire l’EntityScope et sa manière d’ajouter une ExecutionProperty.

Première notion à retenir :  les Execution Properties sont accessibles via la collection Properties d’un NativeActivityContext. Pour ajouter / consulter cette collection il faut donc obligatoirement avoir une activité héritant de NativeActivity. Cette collection est similaire à un ensemble clé+valeur. Pour ajouter une propriété, on utilise donc une méthode Add(), à laquelle on passer un clé sous forme de String et une valeur qui dans le cas présent est mon ObjectContext.

Voici donc le code de mon EntityScope :

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

Afin de faciliter le reste du travail (à savoir la récupération des ExecutionProperties) j’ai codé une interface est une méthode d’extension pour les classes implémentant cette interface (mes autres activités).

Ce code me permet de récupérer l’ObjectContext de mon activité, qu’il soit défini via une ExecutionPropertie ou par l’argument de l’activité implémentant l’interface.

using System.Activities;
using System.Data.Objects;

namespace MyLib.WF4.EntityFramework
{
    /// <summary>
    /// Interface des activity utilisant un Objectcontext
    /// </summary>
    interface IEntityActivity
    {
        InArgument<ObjectContext> ObjectContext { get; set; }
    }

    internal static class EntityActivityExtension
    {
        /// <summary>
        /// Retourner l'ObjectContext de l'activité
        /// </summary>
        /// <param name="activity"></param>
        /// <param name="context"></param>
        /// <returns></returns>
        public static ObjectContext GetObjectContext(this IEntityActivity activity, NativeActivityContext context)
        {
            if(activity.ObjectContext == null
                || activity.ObjectContext.Expression == null)
            {
                ObjectContext objectContext = context.Properties.Find(EntityScope.ObjectContextName) 
                    as ObjectContext;

                if (objectContext == null)
                {
                    throw new ValidationException("'ObjectContext' ne peut être vide!");
                }
                else
                {
                    return objectContext;
                }
            }
            else
            {
                return activity.ObjectContext.Get(context);
            }
        }
    }

}

On retrouve ici une approche similaire aux extensions de WF4.

Utilisé dans une activité simple tel que le SaveChanges, celà donne ce code :

using System;
using System.Activities;
using System.ComponentModel;
using System.Data.Objects;

namespace MyLib.WF4.EntityFramework
{
    /// <summary>
    /// Activity based on NativeActivity<TResult>
    /// </summary>
    public sealed class SaveChanges : NativeActivity<Int32>, IEntityActivity 
    {
        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<ObjectContext> ObjectContext { get; set; }

        /// <summary>
        /// Execute
        /// </summary>
        /// <param name="context">WF context</param>
        /// <returns></returns>
        protected override void Execute(NativeActivityContext context)
        {
            ObjectContext objectContext = this.GetObjectContext(context);
            Int32 result = objectContext.SaveChanges();
            
            // Return value
            this.Result.Set(context, result);
        }

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

            // Register Out arguments
            RuntimeArgument resultArg = new RuntimeArgument("Result", typeof(Int32), ArgumentDirection.Out);
            metadata.AddArgument(resultArg);
            metadata.Bind(this.Result, resultArg);
        }
    }
}

Rien de bien compliqué, on est même dans l’extrêmement simple, et pourtant on tire profit d’une ExecutionProperties.

Les activités AddEntity et DelteEntity sont basées sur le même principe. D’où un code relativement simple :

using System.Activities;
using System.ComponentModel;
using System.Data.Objects;
using System.Data.Objects.DataClasses;

namespace MyLib.WF4.EntityFramework
{
    /// <summary>
    /// Activity based on NativeActivity<TResult>
    /// </summary>
    public sealed class AddEntity<T> : NativeActivity, IEntityActivity where T : EntityObject
    {
        [RequiredArgument]
        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<T> Entity { get; set; }

        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<ObjectContext> ObjectContext { get; set; }

        /// <summary>
        /// Execute
        /// </summary>
        /// <param name="context">WF context</param>
        /// <returns></returns>
        protected override void Execute(NativeActivityContext context)
        {
            // Obtain the runtime value of the Text input argument
            T entity = context.GetValue(this.Entity);

            ObjectContext objectContext = this.GetObjectContext(context);

            ObjectSet<T> objectSet = objectContext.CreateObjectSet<T>();

            objectSet.AddObject(entity);
        }

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

            // Register In arguments
            RuntimeArgument arg = new RuntimeArgument("Entity", typeof(T), ArgumentDirection.In);
            metadata.AddArgument(arg);
            metadata.Bind(this.Entity, arg);

            // [Entity] Argument must be set
            if (this.Entity == null)
            {
                metadata.AddValidationError(
                    new System.Activities.Validation.ValidationError(
                        "'Entity' argument must be set!",
                        false,
                        "Entity"));
            }
        }
    }
}
using System.Activities;
using System.ComponentModel;
using System.Data.Objects;
using System.Data.Objects.DataClasses;

namespace MyLib.WF4.EntityFramework
{
    /// <summary>
    /// Activity based on NativeActivity<TResult>
    /// </summary>
    public sealed class DeleteEntity<T> : NativeActivity, IEntityActivity where T : EntityObject
    {
        [RequiredArgument]
        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<T> Entity { get; set; }

        [Browsable(true)]
        [DefaultValue(null)]
        public InArgument<ObjectContext> ObjectContext { get; set; }

        /// <summary>
        /// Execute
        /// </summary>
        /// <param name="context">WF context</param>
        /// <returns></returns>
        protected override void Execute(NativeActivityContext context)
        {
            // Obtain the runtime value of the Text input argument
            T entity = context.GetValue(this.Entity);

            ObjectContext objectContext = this.GetObjectContext(context);

            objectContext.DeleteObject(entity);
        }

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

            // Register In arguments
            RuntimeArgument arg = new RuntimeArgument("Entity", typeof(T), ArgumentDirection.In);
            metadata.AddArgument(arg);
            metadata.Bind(this.Entity, arg);

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

Là où les chose deviennent intéressantes, cet à partir du moment où l’on veut un designer lié à l’activité qui tire partie du fait d’être dans où hors d’un EntityScope…

La situation “dans un scope” est celle qui est représentée par la toute première capture de cet article.

Hors du scope on préférera avoir un design tel que celui-ci :

 wf_exemple_executionProperties2

Ce qui facilite la saisie d’un ObjectContext.

Alors comment faire?

Premièrement, on code un designer Xaml simple contenant l’interface visuelle couplette :

<sap:ActivityDesigner x:Class="MyLib.WF4.EntityFramework.Design.AddEntityDesigner"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sap="clr-namespace:System.Activities.Presentation;assembly=System.Activities.Presentation"
    xmlns:sapv="clr-namespace:System.Activities.Presentation.View;assembly=System.Activities.Presentation"    xmlns:sapc="clr-namespace:System.Activities.Presentation.Converters;assembly=System.Activities.Presentation"
    xmlns:s="clr-namespace:System;assembly=mscorlib"
    xmlns:ef="clr-namespace:System.Data.Objects;assembly=System.Data.Entity">
    <sap:ActivityDesigner.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="pack://application:,,,/MyLib.WF4.EntityFramework.Design;component/Themes/Generic.xaml" />
            </ResourceDictionary.MergedDictionaries>
            <sapc:ArgumentToExpressionConverter x:Key="ArgumentToExpressionConverter"/>
        </ResourceDictionary>
    </sap:ActivityDesigner.Resources>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <TextBlock Grid.Row="0" x:Name="ObjectContextLabel" Text="ObjectContext (optional in EntityScope)" />
        <sapv:ExpressionTextBox Grid.Row="1" x:Name="ObjectContext"
            OwnerActivity="{Binding Path=ModelItem}"
            Expression="{Binding Path=ModelItem.ObjectContext, Mode=TwoWay,
            Converter={StaticResource ResourceKey=ArgumentToExpressionConverter},
            ConverterParameter=In}" ExpressionType="{x:Type ef:ObjectContext}" />
        <TextBlock Grid.Row="2" Text="Entity" />
        <sapv:ExpressionTextBox Grid.Row="3" x:Name="Entity"
            OwnerActivity="{Binding Path=ModelItem}"
            Expression="{Binding Path=ModelItem.Entity, Mode=TwoWay,
            Converter={StaticResource ResourceKey=ArgumentToExpressionConverter},
            ConverterParameter=In}" />
    </Grid>
</sap:ActivityDesigner>

Et on y ajoute une logique permettant de retrouver l’éventuel EntityScope parent et donc de masquer les contrôles inutiles :

using System;
using System.Activities.Presentation.Model;
using System.Windows;

namespace MyLib.WF4.EntityFramework.Design
{
    // Logique d'interaction pour AddEntityDesigner.xaml
    public partial class AddEntityDesigner
    {
        public AddEntityDesigner()
        {
            InitializeComponent();
            this.Loaded += new RoutedEventHandler(AddEntityDesigner_Loaded);
        }

        void AddEntityDesigner_Loaded(object sender, RoutedEventArgs e)
        {
            Type t = this.ModelItem.Properties["Entity"].PropertyType.GetGenericArguments()[0]; // Type EF manipulé
            this.Entity.ExpressionType = t;

            Visibility visibility = IsInEntityScope(this.ModelItem)
                ? Visibility.Collapsed
                : Visibility.Visible;

            this.ObjectContext.Visibility = visibility;
            this.ObjectContextLabel.Visibility = visibility;
        }

        private static Boolean IsInEntityScope(ModelItem modelItem)
        {
            if (modelItem.Parent == null)
            {
                return false;
            }
            else
            {
                if (modelItem.Parent.ItemType == typeof(EntityScope))
                {
                    return true;
                }
                else
                {
                    return IsInEntityScope(modelItem.Parent);
                }
            }
        }

    }
}

Le secret se trouve dans la simple petite méthode IsInEntityScope(). Celle-ci a pour mission de parcourir l’arbre Xaml représentant le Workflow à la recherche d’un éventuel EntityScope. Evidemment il faut aime le proxy ModelItem. Mais avec un peu de pratique on se rend vite compte que ce n’est pas très compliqué.

 

Donc, si je résume :

Avec ce type de code et les ExecutionProperties, on peut facilement échanger des données entre activités (parents et enfants) sans que la personne chargée de designer le workflow n’ai besoin de passer son temps à faire du copier coller. De plus, nos activités peuvent fonctionner sans ExecutionProperty.

Encore une preuve que Windows Worklfow Foundation sait être souple et améliorer la productivité des designer de workflows ;)