Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#4.0

Saving and loading Workflow Foundation 4 activities

4.71/5 (3 votes)
27 Jul 2012CPOL3 min read 34.1K   640  
All necessary bits and pieces to successfully save and load WF4 activities.

Introduction

In one of my projects, I had the need for user editable workflows. The focus of this article is not how to enable the users to edit the workflows, i.e. how to host the designer. The focus of this article is the seemingly simple task of loading a XAML file that contains a WF4 activity definition in the format created by the Visual Studio designer and create just that format when saving the activity back to XAML.   

This proved harder than expected so I thought about sharing my results. Please note that this is my first article, so constructive criticism is always welcome.

Implementation

The easiest way to load and save activities from and to XAML is to use the class ActivityXamlServices. A look at the documentation reveals no Save method but several Load methods, so lets start with the latter.  

Loading an activity from XAML

Loading the activity is actually very simple:

C#
var activity = ActivityXamlServices.Load(reader);

reader is a TextReader, e.g. an instance of a StreamReader or StringReader.

As far as I am aware, this always returns an instance of type DynamicActivity when loading XAML that has been created by the VS designer or the Save method we are going to implement later.

For a round-trip of loading and saving it is important to have a DynamicActivity, the more general Activity doesn't help us. Because of this, I am trying to cast the result of Load to a DynamicActivity and throw an exception if that doesn't work:

C#
var activity = ActivityXamlServices.Load(reader) as DynamicActivity;
if (activity == null)
    throw new InvalidDataException("The XAML doesn't represent a DynamicActivity.");

Saving a DynamicActivity to XAML

In order to get the same XAML that the VS designer creates, it is necessary to have an instance of ActivityBuilder. Depending on how you do it, saving a normal Activity either throws an exception or leads to XAML in a different format than the one we need. 

Creating an ActivityBuilder from a DynamicActivity

So, in order to save our activity to XAML we need to have an ActivityBuilder but all we do have is a DynamicActivity. The way from a DynamicActivity to an ActivityBuilder is actually very simple, as both classes have a very similar layout. We just new up an ActivityBuilder and assign its properties with the values of our DynamicActivity:

C#
var activityBuilder = new ActivityBuilder();
 
activityBuilder.Implementation = dynamicActivity.Implementation != null ?
                                     dynamicActivity.Implementation() : null;
activityBuilder.Name = dynamicActivity.Name;
 
foreach (var item in dynamicActivity.Attributes)
    activityBuilder.Attributes.Add(item);
 
foreach (var item in dynamicActivity.Constraints)
    activityBuilder.Constraints.Add(item);
 
foreach (var item in dynamicActivity.Properties)
{
    var property = new DynamicActivityProperty
                   {
                       Name = item.Name,
                       Type = item.Type,
                       Value = null
                   };

    foreach (var attribute in item.Attributes)
        property.Attributes.Add(attribute);
 
    activityBuilder.Properties.Add(property);
}
 
VisualBasic.SetSettings(activityBuilder, VisualBasic.GetSettings(dynamicActivity));
 
return activityBuilder;  

This code is an extended version of code presented in Winfried Lötzsch's excellent article about rehosting the WF designer. For the most part, this is straight forward.

But my version of this code has two important differences. The first difference is this line:

C#
VisualBasic.SetSettings(activityBuilder, VisualBasic.GetSettings(dynamicActivity));  

This line has two purposes:

  1. It creates a very important magic string in the XAML file:
    XML
    <mva:VisualBasic.Settings>
        Assembly references and imported namespaces for internal implementation
    </mva:VisualBasic.Settings>  

    Without this, the activities our activity is composed of have no access to the input and output parameters of our workflow.

  2. It copies all the namespace imports from our DynamicActivity to the new ActivityBuilder.
    If this is omitted, the routine that we will later use to save the activity to XAML tries to infer the needed namespaces from the types used in the activity. It normally does a pretty good job at that task but it fails when it comes to extension methods that are used in expressions in the workflow.
    This leads to compiler errors when the XAML is later loaded and invoked.

The second difference is how I copy the properties:

C#
foreach (var item in dynamicActivity.Properties)
{
    var property = new DynamicActivityProperty
                   {
                       Name = item.Name,
                       Type = item.Type,
                       Value = null
                   };

    foreach (var attribute in item.Attributes)
        property.Attributes.Add(attribute);
 
    activityBuilder.Properties.Add(property);
} 

I am not simply adding the instances from the DynamicActivity but I am creating new instances of DynamicActivityProperty. For the most part, I assign the values of the old instance to new instance - with one important difference: I assign null to Value.

When an activity is loaded from XAML Value is null. When the activity is then executed, Value will be assigned by the WF engine. The problem now is that an assigned Value will lead to attributes on the root tag of the generated XAML. These attributes then lead to an exception when trying to load the XAML using the method described above.

That's it, now we have an ActivityBuilder instance we can save.

Saving the ActivityBuilder

Saving is not as straight forward as loading, but still not too hard:

C#
var stringBuilder = new StringBuilder();
var xamlXmlWriter = new XamlXmlWriter(new StringWriter(stringBuilder), new XamlSchemaContext());
var builderWriter = ActivityXamlServices.CreateBuilderWriter(xamlXmlWriter);

XamlServices.Save(builderWriter, activityBuilder);
var xaml = stringBuilder.ToString(); 

Conclusion

Once you know what to look out for, it actually is pretty simple. The attached .cs file encapsulates all this functionality in a nice little helper class.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)