Workflow Foundation (WF4) – Custom Activity to Invoke XAML Based Child Workflows (Part 2)

This article is part of a series:

Inside ExecuteXamlWorkflow

In my last post I introduced the ExecuteXamlWorkflow custom activity, an activity that you can use in your own workflows to execute a XAML file based child workflow. You can download the solution here.

Implementing the ExecuteXamlWorkflow custom activity turned out to be a little challenging in places with a few gotchas and in this post I thought I'd share these with you. I'd also like to discuss some of the rationale behind why things were done the way they were.

First let's take a look at some of the key classes in the solution:

  • ExecuteXamlWorkflow - this class implements the custom activity behavior, extending NativeActivity and overriding the Execute method that runs our XAML file based workflow. It uses the WorkflowInvoker class to execute the workflow synchronously through to completion. Before execution it initialises the child workflow's arguments and after execution assigns any results to the variables or arguments in the parent’s scope. Note that as a result of using WorkflowInvoker, no instance control such as persisting, unloading or resuming of bookmarks is allowed.
  • ExecuteXamlWorkflowDesigner - implements the design surface look and feel of the custom activity. As well as providing some basic WPF styling to make the activity look like standard WF4 activities, it handles click events to show an OpenFileDialog that allows for XAML file selection and also the DynamicArgumentDialog that allows child workflow arguments to be defined.
  • DynamicActivityStore - provides a cache of DynamicActivity instances that the aforementioned classes utilize to improve performance. The ThreadSafeDictionary class provides a thread safe IDictionary implementation for use by the cache. More on this later.

Using the DynamicArgumentDialog Class

I've used the WF4 framework DynamicArgumentDialog class as a means for allowing the user to supply the arguments for the child workflow. It allows arguments to be supplied as expressions which is exactly what we need:

clip_image001_2_6F594AA6

It also allows the user to specify the argument type, direction and allows new arguments to be created. We don't want this since the arguments are derived by introspection of the child workflow and unfortunately we have only limited configurability offered by the DynamicArgumentDesignerOptions class so we can’t disable this functionality. So it's not ideal but has saved a lot of work in creating something requiring reasonable complexity. It just means we need to introduce a bit of validation to ensure the user doesn't change the child's arguments to anything unexpected.

The documentation around setting up the dialog is vague but here's what I finally came up with:

 DynamicArgumentDesignerOptions options = new DynamicArgumentDesignerOptions()
{
    Title = Microsoft.Consulting.Workflow.Activities.Properties.Resources.DynamicArgumentDialogTitle
};

ModelItem modelItem = this.ModelItem.Properties["ChildArguments"].Dictionary;
using (ModelEditingScope change = modelItem.BeginEdit("ChildArgumentEditing"))
{
    if (DynamicArgumentDialog.ShowDialog(this.ModelItem, modelItem, Context, this.ModelItem.View, options))
    {
        change.Complete();
    }
    else
    {
        change.Revert();
    }
}

It took a bit of head scratching to work out what type ChildArguments should be in order for it to be correctly wrapped as a ModelItem instance which the dialog could consume as a dictionary - there's no documentation anywhere. If it can't be consumed as a dictionary you'll get an error dialog with the message "Data input of the dialog is neither ModelItemCollection nor ModelItemDictionary". How do you get create one of those types? You get an instance created by the framework if you use the correct type for your property. I ended up using a Dictionary<string, Argument> which did the trick. Also, the dialog seems to take a copy of the dictionary on first call so subsequent changes to the dictionary are not reflected in the dialog. The solution is to create a new instance of the dictionary, repopulate it and then pass into DynamicArgumentDialog.ShowDialog:

 

 Dictionary<string, Argument> argumentDictionary = new Dictionary<string, Argument>();
this.ModelItem.Properties["ChildArguments"].SetValue(argumentDictionary);

// repopulate ...

// show dialog ...

This is probably a bug, but the workaround seems to work fine with no adverse affect on edit history.

Custom Activity Properties vs. Arguments

If you're new to developing custom activities then you've probably hit the dilemma about when to define a property of an activity as either a CLR property or as an Argument. The article Properties Vs. Arguments may help or you could watch Workflow and Custom Activities - Best Practices which goes into further detail and is highly recommended if you're starting out. A simplification of the distinction is that an Argument will have it's value evaluated at runtime whereas a property is set once at design time and cannot change during execution. We could not have the workflow file path defined as an InArgument simply because we need to know its value at design time so that we are able to load the XAML of the child workflow and initialize the DynamicArgumentDialog with it arguments. Similarly, the DynamicArgumentDialog is initialised with a ModelItem that wraps a dictionary CLR property - the dictionary's contents need to be known at design time to populate its content. It's also worth pointing out here that we're performing validation of the arguments set by DynamicArgumentDialog against the loaded XAML workflow and this we do in the CacheMetadata method of our custom activity which is the place to perform design and runtime validation (see below). CacheMetadata does not get passed an ActivityContext instance and you need this if you want to evaluate an Argument's value.

It would certainly make the solution more flexible if you could setup the child workflow arguments and workflow file path at runtime - it opens up some interesting possibilities. But that's not possible with ExecuteXamlWorkflow because of the design time experience implemented as a result of the motivation to keep it as simple as possible for the user.

Caching Data That Can Be Used At Design And Execution Time

In order to create an instance of our child workflow we call ActivityXamlServices.Load which will load and deserialize a XAML file and return a DynamicActivity instance we can then execute. We have to be careful that this is not called too frequently so as not to impact performance. At design time this is not too much of an issue although if you ever step through execution of the designer code you'll soon notice how frequently CacheMetatdata is called by the framework (and it's here we need to get hold of an instance of our child workflow). What's more significant is at runtime where we're creating workflows that use looping constructs - creating a child workflow instance on each Execute call is going to cause concerns. We're therefore going to need to employ some form of caching so we're as performant as possible.

One option would be to hold the DynamicActivity instance in an instance variable within the designer and custom activity classes. This would work but would not be efficient in cases where you had many ExecuteXamlWorkflow instances in your workflow that were loading the same child workflows that were significantly large. What we need is a shared cache of DynamicActivity instances. Since a DynamicActivity instance defines the composition of a workflow and does not represent execution state, we can share these instances across designer and custom activity instances for multiple workflow instances (potentially across threads) safely.

For this solution I opted to use a simple dictionary of DynamicActivity instances keyed by file path. My initial thought was to implement this singleton as a Workflow Extension, where its creation could be managed by the framework and shared between custom activity instances - this seemed the right thing to do as it fits in with the WF programming model. However this won't work in the designer or in CacheMetadata since to get at an instance of an extension (again) you need the ActivityContext. So the singleton approach works fine providing we use a thread safe dictionary implementation (the custom class ThreadSafeDictionary) to support workflow instances executing on multiple threads. Unfortunately, the caching strategy isn't sophisticated and once an instance is in the cache it remains there until the class is reloaded which means a restart of the application host. With a bit of work this could be improved.

Using CacheMetadata to Perform Design and Execution Time Validation

As mentioned before, we need to validate that the supplied arguments supplied by the parent workflow (those the user setup with DynamicArgumentDialog) match the signature of the child workflow. Also, if an invalid path is specified or we can't load the XAML for whatever reason then we need to report the error. This validation needs to be performed both at design time and at runtime. The correct place to perform this validation is in CacheMetadata, providing you're not dependant on execution context (see above). If our validation logic detects an error, we simply call NativeActivityMetadata.AddValidationError to add an error message and the framework takes care of the rest. In the designer you'll see these messages appear against the activity that raised them such as in the image below:

clip_image002_2_044ACD1A

At runtime, if you're not handling exceptions within the workflow itself then the workflow host will surface these exceptions as a single InvalidWorkflowException.

Conclusion

In my previous post I introduced ExecuteXamlActivity, a custom activity to execute XAML based child workflows that you can freely use out of the box for your own workflow development. In this post I’ve discussed some of the rationale behind the design, including some of the gotchas around using the DynamicArgumentDialog class, criteria around choosing CLR properties over arguments and approaches for caching data and performing validation for both design time and execution scenarios. Hopefully you'll find this useful.

Written by Christopher Owczarek