Share via


Fixing the PrePostSequence custom activity in Introduction to Workflow Hands On Lab

Way back before .NET 4 Beta 1 shipped I created a hands on lab called Introduction to Workflow in .NET 4.  After several evolutions, this lab eventually ended up in the Visual Studio 2010 Training Kit.

Last week when I was teaching a workshop in Shanghai, one of the students pointed out a bug in the lab code.  In Exercise 9 of this lab you build a custom activity and then build the activity designer that goes with it.  The activity you build is the PrePostSequnce activity.  As the name implies, it is basically a sequence that has an activity that executes prior to the main body named “Pre” and then a “Post” activity that is executed after the main body as you see below.

PrePostSequence

 

The Bug

In the lab, I have you build a unit test to verify that Pre and Post are called at runtime but the test did not verify the order.  It turned out that the Post activity was invoked before the Pre activity.  In fact, everything happened in the reverse order of what you would expect.  Take a look at the code.

    1:  [Designer(typeof(PrePostSequenceDesigner))]
    2:  public sealed class PrePostSequence : NativeActivity
    3:  {
    4:      public Activity Pre { get; set; }
    5:      public Activity Post { get; set; }
    6:      public List<Activity> Activities { get; set; }
    7:   
    8:      public PrePostSequence()
    9:      {
   10:          Activities = new List<Activity>();
   11:      }
   12:   
   13:      protected override void Execute(NativeActivityContext context)
   14:      {
   15:          // Schedule the activities in order
   16:          context.ScheduleActivity(Pre);
   17:          Activities.ForEach((a) => { context.ScheduleActivity(a); });
   18:          context.ScheduleActivity(Post);
   19:      }
   20:  }

The comment in line 15 show a profound misunderstanding.

The order in which you schedule activities is not the order in which they are invoked.

That is correct, the Workflow Runtime decides on the order in which scheduled activities are invoked and the logic that it uses can change so don’t count on the behavior.  If you want to control the order of execution, then you must use CompletionCallbacks to control it.

With the PrePostSequence there is also the possibility that any of them could be empty but the activity should schedule the non-empty children.  So, for example, if there is only the Post activity, it should be scheduled.

How can I control the order of scheduling?

Simple, you provide a completion callback.  The Workflow Runtime will call back when it determines that the child activity is completed.  The following code demonstrates the correct (though more complex) implementation that will appear in the next release of the training kit.

 using System.Activities;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Markup;

namespace HelloWorkflow.Activities
{
    /// <summary>
    /// An activity that executes child activities in order and supports a pre/post activity
    /// </summary>
    [ContentProperty("Activities")]
    public sealed class PrePostSequence : NativeActivity
    {
        #region Private Members

        Collection<Activity> activities;
        Collection<Variable> variables;

        Variable<bool> preCompleted = new Variable<bool>() { Name = "PreCompleted" };
        Variable<bool> bodyCompleted = new Variable<bool>() { Name = "BodyCompleted" };
        Variable<int> lastIndexHint = new Variable<int>() { Name = "LastIndexHint" };

        CompletionCallback onChildCompleted;
        CompletionCallback onPreCompleted;

        #endregion

        /// <summary>
        /// Contains the variables scoped to this activity
        /// </summary>
        public Collection<Variable> Variables
        {
            get
            {
                if (variables == null)
                {
                    variables = new Collection<Variable>();
                }
                return variables;
            }
        }

        /// <summary>
        /// The Pre-Sequence activity
        /// </summary>
        /// <remarks>
        /// Use the DependsOn to control the order of serialization in XAML
        /// This will come after the variables
        /// </remarks>
        [DependsOn("Variables")]        
        public Activity Pre { get; set; }

        /// <summary>
        /// The Activities in the body of the sequence
        /// </summary>
        /// <remarks>
        /// Use the DependsOn to control the order of serialization in XAML
        /// This will come after Pre activity
        /// </remarks>
        [DependsOn("Pre")]
        public Collection<Activity> Activities
        {
            get
            {
                if (activities == null)
                {
                    activities = new Collection<Activity>();
                }
                return activities;
            }
        }

        /// <summary>
        /// The Post-Sequence Activity
        /// </summary>
        /// <remarks>
        /// Use the DependsOn to control the order of serialization in XAML
        /// This will come after the Activities
        /// </remarks>
        [DependsOn("Activities")]
        public Activity Post { get; set; }

        /// <summary>
        /// The callback for when the Pre activity is completed
        /// </summary>
        CompletionCallback OnPreCompletedCallback
        {
            get
            {
                if (onPreCompleted == null)
                {
                    onPreCompleted = new CompletionCallback(OnPreCompleted);
                }
                return onPreCompleted;
            }
        }

        /// <summary>
        /// The callback for when child activities are completed
        /// </summary>
        /// <remarks>
        /// Creating and caching the callback using this pattern
        /// results in improved performance because the callback
        /// is created only once even though there may be 
        /// many child activities
        /// </remarks>
        CompletionCallback OnChildCompletedCallback
        {
            get
            {
                if (onChildCompleted == null)
                {
                    onChildCompleted = new CompletionCallback(OnChildCompleted);
                }
                return onChildCompleted;
            }
        }

        /// <summary>
        /// Informs the runtime about our activity and the data
        /// </summary>
        /// <param name="metadata">The metadata</param>
        /// <remarks>
        /// The base class implementation
        /// will discover variables and child activities using reflection.  By overriding 
        /// CacheMetadata we avoid this and get improved performance.
        /// </remarks>
        protected override void CacheMetadata(NativeActivityMetadata metadata)
        {
            /// implementation variables are variables that are 
            /// private to our workflow.
            metadata.AddImplementationVariable(lastIndexHint);
            metadata.AddImplementationVariable(preCompleted);
            metadata.AddImplementationVariable(bodyCompleted);

            metadata.SetVariablesCollection(Variables);

            metadata.AddChild(Pre);
            metadata.AddChild(Post);

            // Cannot use metadata.SetActivitiesCollection because of Pre/Post
            foreach (Activity activity in Activities)
            {
                metadata.AddChild(activity);
            }
        }

        /// <summary>
        /// Implementation of the activity
        /// </summary>
        /// <param name="context">The context used to schedule</param>
        protected override void Execute(NativeActivityContext context)
        {
            ScheduleChildActivities(context);
        }

        /// <summary>
        /// Schedules the child activities
        /// </summary>
        /// <param name="context">The context used to schedule</param>
        /// <remarks>
        /// The PrePostSequence can have the following combinations
        /// Pre Only
        /// Post Only
        /// Pre and Post with empty Activities collection
        /// Pre and Post and Activities
        /// </remarks>
        private void ScheduleChildActivities(NativeActivityContext context)
        {
            if (PreActivityExists() 
                && PreHasNotCompleted(context))
            {
                // Schedule the Pre activity
                context.ScheduleActivity(Pre, OnPreCompletedCallback);
            }
            else if (ActivitiesCollectionIsNotEmpty() 
                && ActivitiesHaveNotCompleted(context))
            {
                // Schedule the first child
                // the OnChildCompletedCallback will schedule
                // the other child activities
                context.ScheduleActivity(Activities.First(), 
                    OnChildCompletedCallback);
            }
            else if (PostIsNotEmpty())
            {
                // No CompletionCallback is required for Post because
                // the activity is done when Post is completed
                context.ScheduleActivity(Post);
            }
        }

        /// <summary>
        /// Called when the Pre activity is completed
        /// </summary>
        /// <param name="context"></param>
        /// <param name="completedInstance"></param>
        private void OnPreCompleted(NativeActivityContext context, ActivityInstance completedInstance)
        {
            preCompleted.Set(context, true);
            ScheduleChildActivities(context);
        }

        /// <summary>
        /// Called when one of the child activities is completed
        /// </summary>
        /// <param name="context"></param>
        /// <param name="completedInstance"></param>
        private void OnChildCompleted(NativeActivityContext context, ActivityInstance completedInstance)
        {
            // get the index of the completed activity and increment it
            int completedInstanceIndex = lastIndexHint.Get(context);
            int nextChildIndex = completedInstanceIndex + 1;

            // if the sequence is not done, schedule the next child
            if (nextChildIndex < Activities.Count)
            {
                lastIndexHint.Set(context, nextChildIndex);
                Activity nextChild = Activities[nextChildIndex];
                context.ScheduleActivity(nextChild, OnChildCompletedCallback);
            }
            else // Completed body
            {
                bodyCompleted.Set(context, true);
                ScheduleChildActivities(context);
            }
        }

        private bool PostIsNotEmpty()
        {
            return Post != null;
        }

        private bool ActivitiesHaveNotCompleted(NativeActivityContext context)
        {
            return bodyCompleted.Get(context) == false;
        }

        private bool ActivitiesCollectionIsNotEmpty()
        {
            return Activities.Count > 0;
        }

        private bool PreHasNotCompleted(NativeActivityContext context)
        {
            return preCompleted.Get(context) == false;
        }

        private bool PreActivityExists()
        {
            return Pre != null;
        }
    }
}