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:
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:
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
:
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:
VisualBasic.SetSettings(activityBuilder, VisualBasic.GetSettings(dynamicActivity));
This line has two purposes:
- It creates a very important magic string in the XAML file:
<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.
- 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:
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:
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.