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 string
s 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