Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / XAML

A dynamic Rehosted Workflow Designer for WF 4

4.85/5 (26 votes)
29 Jul 2012CPOL10 min read 114.2K   11.8K  
This article presents a framework allowing you to integrate the workflow designer more easily in your own applications

(Download updated on 29th July 2012)

Introduction

When you write an application using the Windows Workflow Foundation (WF), you often want to have your own workflow designer integrated into your application. Microsoft provides a class called WorkflowDesigner to solve this problem, but there are still some tasks you have to do by yourself:

  • Initialize the WorkflowDesigner
  • Load the toolbox (especially loading the icons is tricky)
  • Load/Save the workflow-XAML (database, filesystem ...)
  • Handle errors (invalid xaml, ...)

I realized, that these tasks are mainly implemented the same way every time, so I started to think about a generic infrastructure. First of all there are some guidelines I want to mention the infrastructure should satisfy:

  • Generic layout - I did this by defining each part of the designer as prism module, so you are able to put these modules in your own application, change their position ...
  • Generic logic - Almost everything regarding the logic is described by an interface and resolved using the UnityContainer (DependencyInjection)

To show you some examples, here are two implementations, that use the framework.

1. My standard workflow designer application:

My standard workflow designer application

2. Another application using a ribbon bar and an outlook bar (from Odyssey library):

Another test using a ribbon bar and an outlook bar (from Odyssey library)

The core components

As already said, we´re using Prism-modules. For those who don´t know Prism: We have a root-view with some content controls (Regions) in it. At runtime views (usually UserControls) are attached to them as content. Because I provide these views, you can easily change the layout later by just using a different root-view. We basically need:

  • A View for designer model
  • A View for the property grid
  • A View for the toolbox
  • A ViewModel to host the WorkflowDesigner class, we will also have to think about initializing the designer and error handling here

Views

As the WorkflowDesigner already provides UIElements as properties, we can easily bind our Views to them, while using the ViewModel as DataContext. I decided to implement a UserControl for each view and not just to hand the UIElements from the WorkflowDesigner over to Prism, because there might be cases when we don´t have a WorkflowDesigner, but still want to display something, for instance when a load error occurs.

XML
<UserControl ...
     <ContentControl Content="{Binding CurrentSurface.Designer.View}"/>
</UserControl>  

ViewModel

Now let´s say something about the ViewModel. You may have noticed the CurrentSurface.Designer in the path of the binding, so what is the internal structure of the ViewModel?

The DesignerViewModel is defined by the following interface:

C#
public interface IDesignerViewModel
{
    void ReloadDesigner(object root);
    
    object CurrentSurface { get; }
    event Action SurfaceChanged;
} 

As you can see, the actual state of the ViewModel is represented by a "surface". This could be just the normal WorkflowDesigner view, but also an error display. Because WPF bindings don´t throw exceptions in case they don´t work, our content control above is just not visible when an error occurs. Knowing that, we can place an error TextBox behind the content control that is normally covered by the designer view.

The ReloadDesigner method tells the ViewModel to reload the designer given a designer root (either an Activity or an ActivityBuilder) by creating a StandardDesignerSurface, which hosts the WorkflowDesigner.

For the case of an invalid XAML definition, I created another interface my DesignerViewModel implements:

C#
public interface ILoadErrorDesignerViewModel
{
    void ReloadError(string xaml);
} 

Calling the ReloadError method tells the ViewModel to use an LoadErrorDesignerSurface as CurrentSurface, from which you can get the invalid XAML and display it or do whatever you want.

The Storage Module

After having discussed the basics of the framework we can continue with some more advanced stuff. In connection with storage, we will have to talk about additional data stored for each workflow: Maybe you want a description or you want to enter the key for a database or something else. Including this additional data in our design leads us to the definition of a workflow type. That means there are different kinds of workflows that differ in the way they´re stored, the additional data attached to them and even the toolbox items having to be loaded when editing such a workflow.

C#
public interface IWorkflowType
{
    IToolboxCreatorService ToolboxCreatorService { get; }
    IStorageAdapter StorageAdapter { get; }
    string DisplayName { get; }
    string Description { get; }
} 

The ToolboxCreatorService is responsible for loading the correct toolbox items and the StorageAdapter handles all the other things we spoke about.

C#
public interface IStorageAdapter
{
    IEnumerable<IWorkflowDescription> GetAllWorkflowDescriptions();

    bool CanCreateNewDesignerModel { get; }

    IDesignerModel CreateNewDesignerModel();
    IDesignerModel GetDesignerModel(object key);

    bool SaveDesignerModel(IDesignerModel designerModel);
    bool DeleteDesignerModel(object key);
}

For performance reasons we don´t want to load the whole database (or all data files, depending on which storage mechanism we are using) into memory in case the user want´s to load only one of the stored workflows. Everything the user needs to identify the workflow he wants to load is a name and maybe a description, so we separate these things from the rest (XAML definition and so on) by defining an interface (IWorkflowDescription). The other interface (IDesignerModel) contains everything, it´s also responsible for holding the additional data in a property called PropertyObject. The two interfaces are correlated by a key object that should be the key used for storing the workflow. If you ask yourself how you could edit the additional data, by a property grid of course: The storage module exchanges the standard property view for another one that consists of the normal property grid from the WorkflowDesigner and a custom PropertyGrid bound to the PropertyObject of the IDesignerModel (I got the custom property grid from here).

It makes sense to show a storage dialog that could for instance look like that:

Image 3

You can select the workflow type on the left and the activity in the middle. Take notice of the possibility to have multiple workflow types in one application.

The XAML view

In some cases it is much more efficient to edit the XAML directly than to edit the workflow in the designer. Especially when you have got an invalid XAML definition, you have no choice but to edit the XAML, because the workflow can´t be displayed.

The XAML view is again implemented as a Prism module. I created a ViewModel that reacts on the SurfaceChanged event of the IDesignerViewModel implementation. In the event handler, we first check, whether the new surface is an IDesignerSurface or an ILoadErrorDesignerSurface.

  • If it is a ILoadErrorDesignerSurface, we take the invalid XAML and display it
  • If it is a IDesignerSurface, we attach an event handler to the ModelChanged event of the workflow designer and so update our text every time the model changes

Every time the user edits the XAML, the ViewModel tries to update the designer view and displays eventually occuring errors. For a better performance I have buit in a timer that delays the update of the model. As this timer is restarted every time a key is pressed, the model only changes when the user stops to write and not every time he presses a key.

I am using the AvalonEdit text editor to get some nice colors:

Image 4

How to put it all together

The basic idea was to integrate the modules in another application, but if you just want a workflow designer, you can use the sample implementation I´ve taken the screenshots from. Basically all the application does is running a very straightforward PrismBootstrapper that shows the MainWindow (where the Prism regions are defined). You can extend the application by registering your own IWorkflowType in the UnityContainer (using the App.config):

XML
<types>
        <!--...-->
        <!--this is the example workflow type-->
    <type name="Selen.ActivityWorkflowType"
        type="Selen.WorkflowDesigner.Contracts.IWorkflowType, Selen.WorkflowDesigner.Contracts"
        mapTo="Selen.ActivityWorkflowType.VoidActivityType, Selen.ActivityWorkflowType">
        <lifetime type="singleton"/>
    </type>
        <!--Put your own workflow type here-->
</types>

ActivityBuilder and DynamicActivity

Now lets make the last step and see how you can implement such a workflow type. Due to several reasons, I take the ActivityBuilder as example:

  • An activity is the most dynamic workflow type at all, because it can be everything and even have arguments to pass in or out, so you might be able to use that type in your own application
  • It has always been topic of discussion whether it is possible to design activities in the Rehosted Designer (like the VisualStudio designer does) and if yes in which way, because this is actually not very easy. So I want to present my approach, which is kind of a hack, but it works well.

Activities as root and child

For those who may not know the difference between ActivityBuilder and DynamicActivity: The ActivityBuilder class can be used as root in the workflow designer. When we generate the XAML from it we get it in the form "<Activity …" (just like with Visual Studio). But when we want to execute the activity or use it in a workflow (or another activity), we have to convert it to a DynamicActivity (See MSDN for more details). This all works fine until the moment you try to save the workflow, as this is not possible with a DynamicActivity in it. There must be a solution, but I wasn´t able to compile the "<Activity ..." XAML to a real type, so I had to deal with the DynamicActivity. To solve the saving problem I created another class that behaves exactly like a DynamicActivity, despite the fact that it can be saved.

The PlaceholderActivity

My so called PlaceholderActivity exposes exactly the same properties like the DynamicActivity, I just added that attribute for each of them:

C#
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]

By doing that I tell the WF Runtime not to serialize these properties to XAML, because this would result in an exception. The only way to save a DynamicActivity to XAML is converting it to an ActivityBuilder and that is exactly what I do. I implemented a string property that is serialized to XAML and in the getter of that property I create a new ActivityBuilder and then copy all properties of the PlaceholderActivity to the ActivityBuilder (the ActivityBuilder has the same properties like the DynamicActivity and thus the same like the PlaceholderActivity). After that I can serialize the ActivityBuilder and return the outcoming XAML, which contains all information about my PlaceholderActivity. While deserializing the workflow, the WF Runtime sets the XAML property and I can load a DynamicActivity from it and again copy the properties.

C#
public PlaceholderActivity(DynamicActivity dynamicActivity)
    : this()
{
    this.ApplyDynamicActivity(dynamicActivity);
}

public PlaceholderActivity()
    : base()
{
    this.typeDescriptor = new PlaceholderActivityTypeDescriptor(this);
}

private void ApplyDynamicActivity(DynamicActivity dynamicActivity)
{
    this.DisplayName = dynamicActivity.Name;

    foreach (var item in dynamicActivity.Attributes)
    {
        this.Attributes.Add(item);
    }

    foreach (var item in dynamicActivity.Constraints)
    {
        this.Constraints.Add(item);
    }

    this.Implementation = dynamicActivity.Implementation;
    this.Name = dynamicActivity.Name;

    foreach (var item in dynamicActivity.Properties)
    {
        this.Properties.Add(item);
    }
}

[Browsable(false)]
public string XAML
{
    get
    {
        var activityBuilder = new ActivityBuilder();

        foreach (var item in this.Attributes)
        {
            activityBuilder.Attributes.Add(item);
        }

        foreach (var item in this.Constraints)
        {
            activityBuilder.Constraints.Add(item);
        }

        activityBuilder.Implementation = this.Implementation != null ? this.Implementation() : null;
        activityBuilder.Name = this.Name;

        foreach (var item in this.Properties)
        {
            activityBuilder.Properties.Add(item);
        }

        var sb = new StringBuilder();
        var xamlWriter = ActivityXamlServices.CreateBuilderWriter(
                         new XamlXmlWriter(new StringWriter(sb), new XamlSchemaContext()));
        XamlServices.Save(xamlWriter, activityBuilder);

        return sb.ToString();
    }
    set
    {
        this.ApplyDynamicActivity(ActivityXamlServices.Load(new StringReader(value)) as DynamicActivity);
    }
}

Implementation of the data contracts

The implementation of the two data contracts (IDesignerModel and IWorkflowDescription) is very straightforward:

C#
public class WorkflowDescription : IWorkflowDescription
{
    public WorkflowDescription(object key)
    {
        if (key == null) throw new ArgumentNullException("key");
        this.Key = key;
    }

    public string WorkflowName { get; set; }

    public string Description { get; set; }

    public object Key { get; private set; }
} 

public class ActivityEntity
{
    [Category("General")]
    [Description("Activity description")]
    public string Description { get; set; }
}

public class DesignerModel : IDesignerModel
{
    private string originalKey;

    public DesignerModel(object rootActivity, ActivityEntity activityEntity)
    {
        if (activityEntity == null) throw new ArgumentNullException("activityEntity");

        this.RootActivity = rootActivity;
        this.ActivityEntity = activityEntity;
        this.ApplyKey();
    }

    public object Key { get { return this.RootActivity != null ? (this.RootActivity as ActivityBuilder).Name : null; } }

    public bool HasKeyChanged
    {
        get
        {
            return (string)this.Key != originalKey;
        }
    }

    public object RootActivity { get; set; }

    public object PropertyObject { get { return this.ActivityEntity; } }

    public ActivityEntity ActivityEntity { get; private set; }

    public void ApplyKey()
    {
        this.originalKey = (string)this.Key;
    }
} 

Note that we can use the name of the ActivityBuilder as key for our workflow, but we could also define the key in the additional data (the ActivityEntity class in this example). I have used the additional data to provide a way to edit the description of the workflow. We also have to implement a property, whether the key changed since the last saving of the workflow (we know this, because ApplyKey is called every time the workflow is saved). My infrastructure needs this information to keep the user from accidentally overriding workflows with the same key.

The IActivityTemplateFactory template

Because we want to show our custom activities in the toolbox later, we need a type we can pass to the ToolboxItemWrapper. As it is in no way possible to define parameters, we must create a type for each of the activities. I decided to do this by an IActivityTemplateFactory, because we don´t have real compiled activity types (just the DynamicActivity). So I compile an assembly for each activity with only one type in it, that derives from IActivityTemplateFactory and defines the following private fields:

C#
private readonly string xaml = @"{0}";
private readonly string description = @"{1}";
private readonly string activityName = @"{2}";

The placeholders stand for the hard coded values of the workflow data. The private fields are exposed by public properties to make them readable for the StorageAdapter. As we can implement the Create method of the IActivityTemplateFactory, we can let a PlaceholderActivity being inserted in the workflow when the users drags the toolbox item on the designer surface.

C#
public Activity Create(DependencyObject target)
{
    DynamicActivity dynamicActivity = ActivityXamlServices.Load(new StringReader(this.xaml)) as DynamicActivity;
    return new PlaceholderActivity(dynamicActivity);
}

The StorageAdapter

All what´s left for the StorageAdapter to do is compiling the assemblies including the IActivityTemplateFactories and loading them. If the StorageAdapter should encounter a load error caused by invalid XAML, a WorkflowLoadException has to be thrown including the invalid XAML and the additional data:

C#
if (workflow == null)
{
    throw new LoadWorkflowException() { XAMLDefinition = instance.XAML, DesignerModel = new DesignerModel(null, new ActivityEntity() { Description = instance.Description }) };
}

After having catched this exception, my infrastructure calls the ReloadError method of the ILoadErrorDesignerViewModel and thus displays the invalid XAML in the XAML view and shows an error text.

The Test Host

If you want to test your custom activities, that you created with my Reshosted Workflow Designer, you can use my Test Host console application (only included in the source download), which automatically loads the activity dlls from the designer exe. To test an activity, follow these steps:

  1. Run the TestHost.exe one time to create the subfolders
  2. Create your activity in the designer and save it
  3. Go into the activities folder among the WorkflowDesigner.exe and copy the dll named like the activity you want to test
  4. Drop it in the activities folder among the TestHost.exe
  5. Run the TestHost.exe, it´s also possible to execute multiple activities consecutively

Here is a screenshot of how the TestHost could look after execution:

Image 5

History

  • 29th July 2012 - Updated source and demo, Added Test Host description to article
    UI enhancements, some bug fixes, refractored source code, workflow types can now be grouped and are displayed in a tree view

License

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