Windows Workflow and WPF

0
162

Introduction

Windows Workflow (WF) is a framework included in .NET that, in effect, allows one to develop a domain-specific language (DSL) consisting of user defined activities as the verbs, and a framework which provides the logic for invoking them, including control flow and decision logic. An issue that is underdescribed is how to connect a workflow with a WPF (Windows Presentation Framework) application, particularly in an MVVM (Model, View, View Model) partitioned one. In this article, I demonstrate a simplified method for hosting a workflow and connecting it with the upper layers of an MVVM partitioned application.

Background

MVVM

The essential features of MVVM that we are concerned with here are layering and databinding. We would like for the GUI elements to present the state of the workflow via declarative databinding, with minimal or no coded imperatives, to promote layered separation of concerns.

WF Data

WF allows data to be passed in and out of the WF framework via a Dictionary object. Individual activity classes can access the elements of the Dictionary via WF commands. However, a Dictionary‘s elements do not lend themselves to WPF data binding. There have been some articles in the past demonstrating mechanisms for connecting the WF data context with a WPF DataContext (Methods of WPF – WF Data Exchange, Direct WPF – WF Data Binding), and another using the WorkflowApplication Extensions property to inject the Model into the workflow (How to use Workflow from WPF MVVM). This allowed accessing properties in the Model, and also to raise events which the View Model could subscribe to. However, since the Dynamic features were added to .NET 4.0 and later, a simpler mechanism is available.

ExpandoObject

The ExpandoObject introduced in .NET 4.0 is a dynamic class that allows properties to be added at runtime, and these properties are automatically available for WPF data binding via the INotifyPropertyChanged interface. This makes it an ideal connection between the workflow and the WPF view. A WPF element can be bound to an ExpandoObject‘s property in XAML at compile time, even though the property has not yet been declared (assuming the WPF DataContext is bound to the ExpandoObject). Later, when the property is instantiated at runtime in the ExpandoObject, the WPF binding immediately takes effect.

For example:

using System.Dynamic;
 ...
dynamic Properties = new ExpandoObject();



Properties.demo = "this property is added at runtime";

The “Properties” object now contains a runtime-defined string property, “demo“, which implements change notification automatically. If the demo property were bound via data binding in XAML, for example, to a Label element’s Content property, when the assignment statement is executed, the Label will immediately have its content set to “this property is added at runtime”. This occurs even though when the XAML was compiled, there was no such property as “demo“. Under the covers, an ExpandoObject is a Dictionary, and can hold various types, including delegates. This allows a WF activity with access to the ExpandoObject to raise events, and create, set and get properties bound to the GUI, without any dependency on the GUI.

Connecting WF with the ExpandoObject

The WF WorkflowApplication has a property, Extensions, with which one can inject objects into the WF framework, and make them available to its activities. This is distinct from the mechanism of passing arguments to the workflow. Here, the Properties ExpandoObject is injected into the WorkflowApplication Extensions property before launching the workflow.

Activity activity = new ActivityLibrary1.Activity1();
WorkflowApplication workflow = new WorkflowApplication(activity);



workflow.Extensions.Add(Properties);

workflow.Run();

Inside an Activity, the objects in the Extensions property are retrieved by their type from the activity’s context argument. Note that the local reference object is “dynamic“, but the type searched for is “ExpandoObject“. In this code, the activity is a NativeActivity, but the same technique works with CodeActivity.

protected override void Execute(NativeActivityContext context)
{
 dynamic extensionObj = context.GetExtension<ExpandoObject>();
 

With access to the ExpandoObject, a property can be created and set:

extensionObj.label1text = "This is label 1 text";

Also, a delegate can be invoked. Since the consuming View Model might not have created such a delegate, it is incumbent to test for it. In this example, the delegate is an Action delegate with a string parameter. As with all ExpandoObject members, the template is <string, object>. We must cast the ExpandoObject to the IDictionary type to test whether it has the key (AddList) for the delegate. If we simply tested for the AddList member directly, the ExpandoObject would throw a RuntimeBinderException. Properties on ExpandoObjects can be created automatically by assignment, but not by access. Recall that under the covers, they are implemented by Dictionary<string, object>. ExpandoObjects are a bit tricky!


if (((IDictionary<string, object>)extensionObj).ContainsKey("AddList"))
{
 extensionObj.AddList("this is a list item1");
}

If we want to be even more careful, we could test that the delegate is not null before we invoke it as follows:


if (((IDictionary<string, object>)extensionObj).ContainsKey("AddList"))
{
 extensionObj.AddList?.Invoke("this is a list item1");
}

In my opinion, this is a much simpler mechanism to connect WF activities with View and View Model layers in an MVVM WPF application.

Example Application

The example application demonstrates these concepts. The workflow is created as a library (DLL), with no dependencies on any aspect of the GUI in the application. Bindable elements are the two labels and the list box. An Action delegate pops up a MessageBox dialog, another adds its string argument to the ObservableCollection bound to the list box. The Grid’s DataContext is set to the ExpandoObject “Properties” in the MainWindow constructor.

The Action delegates must use Dispatcher to invoke on the UI thread, since the activities that invoke them run on the Workflow thread.

NativeActivity1 sleeps for 2 seconds to simulate work.

The application, once compiled and run, displays a window with a “Start Workflow” button and a list box visible. Not immediately visible are two labels, since at program start, they have no content. When the button is clicked, the click handler invokes the ViewModel method that creates the workflow, injects the ExpandoObject, and launches the workflow. In this tiny example, the button click is handled in a click handler, instead of a Command. The ViewModel has no dependency on the View.

The two workflow activities populate the labels and add strings to the list box. The labels are bound to the yet to be created properties of the Properties ExpandoObject; their content appears as soon as the properties are created. The ObservableCollection, “list”, is assigned to the Properties.items property, which is bound to the list box as its ItemSource.

Figure 1: The application at launch

Figure 2: The application after NativeActivity1 runs

Figure 3: The application after NativeActivity2 runs. Note that NativeActivity2 has modified both labels content

LEAVE A REPLY