Workflow services are often built to act as the control mechanism for routing complex data sets. For example, they may define the flow of an invoice within an organization for review, or they may guide a shipment through a warehouse. In most cases, the data set that the workflow service operates on is described as semi-structured, which means that parts of the data have a fixed schema and other parts are more dynamic. These more dynamic parts are often just a collection of key-value pairs. Windows Workflow Foundation handles semi-structured data just fine—if you are only developing a single workflow. In this article, we will examine how you can automate the process of defining a data flow at design time by schematizing your workflow definitions to drive the automatic generation of workflow variables, using category editors, and building custom activities that both dynamically define their own arguments and automatically wire themselves up to variables in the parent workflow's schema.
Understanding the Challenge
For the single-workflow definition, the implementation dealing with semi-structured data is quite simple: Define your workflow variables to store both structured and unstructured data elements, build activities to handle these variables as arguments, and pass around any unstructured data in key-value collections.
Eventually, however, one wants to build cleaner workflows on top of this semi-structured data. For example, instead of always using an expression to reach into an indexed collection to pull out a transaction number, the workflow designer simply wants to use the transaction number as if it were part of the structured data, perhaps by binding directly to the variable that represents it. The steps to accomplish this are straightforward:
- Add a variable to store the value of that unstructured data element.
- Add an argument for this element to all the custom activity types that need to get or set its value.
- Set the argument's value in the properties of all the custom activity instances in the workflow design so that they reference the workflow variable by name.
However, what happens when you have more than one unstructured data element to add? What it if the set of unstructured data elements that you used previously changes completely? Obviously, these three simple steps can add up to a lot of work. So what can you do to automate these forms of design changes and at the same time make your workflow definitions easier to work with? The approach is to schematize your workflow definition and build activities that automatically wire themselves up to that schema.
Schematizing Your Workflow Definition
When schematizing your workflow definition, you have two things to consider: how to define the schema and how to store the schema with your workflow service. To define the schema, you add a CategoryEditor, accessed from the Property Grid, to the root activity (e.g., Sequence, Flow Chart, or State Machine) that lets you define the schema. Figure 1 shows the Schema property in the Property Grid of a selected Sequence.
Figure 1: Displaying a custom user interface to configure schema
Clicking the ellipses displays a simple schema editor that can load a schema from a file, which lets you choose a subset of that schema for use in the workflow.
With the desired schema elements selected in the editor, clicking OK creates a new workflow variable for each selected attribute and adds it to the root Sequence in the workflow (see Figure 2).
Figure 2: Auto-generated schema variables
It also provides a default value for each workflow variable. Now all activities within the workflow can bind to the schema simply by using the variable name as the expression value of their arguments. With this approach, the schema is stored within the workflow service's XAML as the variables that are declared on the root activity.
To support the aforementioned automation, we need to define a type that can describe schema variables, and we need to implement the schema editor.
FormVariable. The astute reader may have noticed that the variable type for the automatically created variables is FormVariable. This is a custom type (see Figure 3) that was created to act as a way to label variables that are defined on the root activity as schema variables, and to enable the custom activities to select them from a variables collection that might include other variable types that are not schema-created.
Building a Schema Editor
To provide the schema editing experience that was shown previously requires that you build a dialog, define the custom CategoryEditor, add the CategoryEditor as a DataTemplate to a ResourceDictionary, and associate the CategoryEditor with the desired root activity types.
Schema editor dialog. The schema editor dialog that is displayed can be anything you want. In our case, we simply created a new Windows Presentation Foundation (WPF) window and added controls for loading and managing the schema.
Custom CategoryEditor. The implementation of the CategoryEditor represents the bulk of the work. There are many types of editors that you can plug in to control the Property Grid editing experience. The CategoryEditor is useful because it helps you edit multiple properties at one time by using a design surface that is completely customizable.
To create an editor, you build a UserControl, register that UserControl as a DataTemplate in a ResourceDictionary, and reference that DataTemplate from a class that derives from CategoryEditor. Then, you associate the CategoryEditor derived type with any activity types that you want it used on via an implementation of the Register method within a class that implements the IRegisterMetadata interface. If you are familiar with associating custom activity designers with custom activities, this approach is identical.
The implementation of the CategoryEditor type that is called SequenceCategoryEditor is shown in Figure 4. There are three key overrides. First, we override the TargetCategory property. The design time infrastructure uses this value to determine which group of an activity's properties to apply this editor to. Recall that activity properties can be decorated with the CategoryAttribute that names the group they should display in (that is, which category editor should be used to display it). The ConsumesProperty method examines the passed-in propertyEntry and can use it to indicate whether this CategoryEditor will define the UI for the particular property. In our case, we always return to true so that our editor handles the display for all properties in the Misc category.
Finally, the EditorTemplate property returns to the designer infrastructure the DataTemplate stored in a Resource Dictionary that references the UserControl that appears on the Property Grid itself. The content of the Resource Dictionary is shown in Figure 5. Our custom CategoryEditor is simple—it consists of a label, a text box, and a special button that shows the schema editor dialog (see Figure 6).
The real work is performed within the Click event handler of the EditModeSwitch button, which passes the Variables collection to the schema editor dialog, displays it, and is responsible for adding the selected schema elements as workflow variables when the user clicks OK (see Figure 7). In the ShowDialog method, notice the use of the ModelEditingScope, which enables the user to undo the addition of all the schema variables by clicking the Undo button.
The last step is to associate the SequenceCategoryEditor with the Sequence activity. We do this within an implementation of the IRegisterMetadata Register method, as shown in Figure 8, by adding an EditorAttribute to the Sequence type that refers to our SequenceCategoryEditor.
Build Self-Wiring Activities
Next, we build activities that can automatically, and without code modification, discover and use the variables that are generated as a result of schematizing the workflow. To do this, our activity needs to dynamically create and wire up its arguments. Recall that arguments are like holes poked in the surface of an activity, through which data may pass. Without these holes, a custom activity has trouble processing any data that is made available by in-scope workflow variables. Because the arguments an activity surfaces are usually tied to the type defining the activity, it is not possible to change that surface without changing the activity's implementation and re-compiling.
So how do we get our custom activities to automatically create their arguments at design time in response to the activity being dropped on the design surface or in response to variables being defined on the root activity? Subsequently, how do we get these arguments to reference the in-scope variables? Conceptually, the solution is to have each custom activity watch for modification events on the root activity. When such an event occurs, the activity needs to process each variable defined on the activity, define an argument for it, and bind that argument to that variable by defining an expression containing the variable's name. Figure 9 shows the resulting XAML that is created in response to either event happening on our custom activity called DynamicArgumentsActivity. Notice how InOutArguments for TransactionDate, PurchaserName, and PurchaserAmount were added to the activities arguments collection and were bound to the in-scope variables.
Now let's examine how to implement the DynamicArgumentsActivity to handle this dynamic argument declaration and binding. There are two main parts to the implementation: a custom activity designer and the activity itself.
The Activity Designer
The key function of the custom activity designer in this scenario is to react to changes in the root variables (see the complete implementation in Figure 10). For our purposes, we listen to the PropertyChanged event of the root model item, which wraps the root activity. In response to this event, we add a new InOutArgument to the Arguments collection of the activity for each FormVariable that is discovered. We initialize each argument with a VisualBasicReference
The activity must be created to support a list of arguments, as the Activity base class does not offer this out of the box. This requires implementing three items: an Arguments property to store the list of arguments, the CacheMetadata method, and the Execute method. The complete implementation is shown in Figure 11.
Arguments property. This is a simple custom CLR (that is, regular, non-argument type) property that maintains the collection of dynamic arguments. When this collection is modified, the new arguments are saved in the workflow's XAML or XAMLX. It needs to initialize its collection if null when it is first accessed.
Override CacheMetadata. This method exposes the dynamic arguments to the workflow runtime when the workflow instance is executing. First, we make the required call to base.CacheMetadata. Then the code loops over all the arguments that are collected in the Arguments property, creates an equivalent RuntimeArgument, binds the Argument to the RuntimeArgument, and adds each to the metadata's arguments collection via the AddArgument method.
Execute. Within the Execute method, access the runtime value of any dynamic argument by querying against the Arguments collection for a particular argument and using Get(context) on it to get its value or Set(context, value) to set it to a new value. In the DynamicArgumentsActivty implementation of Execute, we shred an input List of FormVariables, as might be passed as input to a Receive activity in the workflow, into the corresponding workflow variables. We also track the values of the variables to verify that shredding is working as expected. Then, after the Activity Designer and the activity are defined, you need to associate them in the Register method, as we showed previously in Figure 8.
The solution structure for all this is to include the activity definition in its own Activity Library project. The designer, category editor, schema editor window, resource dictionary, and IRegisterMetadata implementation go in an Activity Designer Library project. The FormVariable helper class should go in a third Class Library where you define any shared types. When using these designers, it is important that the Activity Designer Library output go to the output directory of the Activity library, and that any references to the Activity library point to the Activity Library itself or to the assembly in its output directory. By following this approach (where the activity and designer libraries are side by side in the same folder), the designers will be automatically loaded and applied by Visual Studio.
With that, all the major pieces are now in place for defining a schematized workflow where custom activities can wire themselves up automatically, and changes to the schema propagate through the activities automatically.
The process that is described here is a fair amount of up-front work However, when this process is completed, the benefit should be clear for repeated workflow designs and modifications: Adding new custom activities that bind to the schema is easier, wiring them up is automatic, and changes to the schema are significantly easier to perform.
Zoiner Tejada ([email protected]) is the president of TejadaNET, is recognized as a Microsoft Industry Influencer, and is an advisor to Microsoft's Connected Systems Division. Zoiner has a degree in computer science from Stanford University and blogs at www.theworkflowelement.com.