Simplify Development With The Declarative Model Of Windows Workflow Foundation
and Dharma Shukla
|
|
This article discusses:
|
This article uses the following technologies:
WinFX, XAML, C#, Visual Studio 2005 |
Contents |
WinFX™ provides a set of general-purpose activities that cover most control flow constructs, but users are free to ignore them and write an entirely new set of activities that are precisely tailored to the problem domain they are working in. More commonly, a workflow program will use WinFX-provided activities for basic control flow and program structure, and will use custom user-defined activities for domain-specific functionality.
In addition to supporting a XAML-based compositional approach to creating programs, workflow-based programs also benefit from a richer set of runtime services than traditional CLR-based programs. The WinFX workflow runtime can be hosted in any CLR AppDomain. The runtime allows workflows to be removed from memory (a technique called passivation) and later reloaded and resumed without making developers write explicit state management logic. The workflow runtime also provides common facilities for handling errors and compensating transactions to allow either automatic or customized undo logic to be specified for long-running units of work. In addition, you can take advantage of management services that allow the state of a given workflow program to be inspected either through eventing, tracking, or querying the workflow state.
Workflow 101
In WinFX, a workflow can be expressed in either declarative XAML or in imperative code using any .NET-targeted language. The use of XAML allows for workflows to be developed and modified in visual designers without requiring traditional C# or Visual Basic code. A workflow can be defined either purely in XAML or with XAML plus a codebehind file that contains C# or Visual Basic code. Pure XAML workflows have the advantage of being directly loadable at run time without a separate compilation step. Just to prove the point, in this article we'll use XAML wherever possible. In all cases, the XAML we show could be expressed in C# or Visual Basic, albeit more verbosely.
Creating a workflow program is a matter of producing the right XML description of the program. For a certain class of users, creating and editing XML in a text editor is an adequate (and sometimes ideal) solution. For the large number of users who prefer a visual designer, WinFX includes a visual workflow designer that can be embedded into any Windows-based application (shown in Figure 1). Again, for exposition purposes, this article shows workflow programs in their underlying XAML form, but in all cases, the XAML can be created and edited using the workflow designer.
To allow the developer using C# or Visual Basic to create and edit workflows, the workflow designer can be used within Visual Studio® 2005, including integration with the Visual Studio project system and debugger. Integration with the Visual Studio debugger allows you to use the familiar F5/F10/F11 key bindings to debug through both the workflow visualization and the underlying C# or Visual Basic code in a single debugging session (shown in Figure 2).
Inside a Workflow
Fundamentally, a workflow is a tree of domain-specific program statements called activities. The concept of an activity is central to the workflow architecture—you should think of activities as domain specific opcodes, and workflows as programs that are written in terms of those opcodes.
WinFX provides a family of broadly applicable activities, several of which are discussed in this article. WinFX also allows you to write your own domain-specific activities either in XAML or in a CLR-compatible language. A WinFX activity is simply a CLR type that derives (directly or indirectly) from System.Workflow.ComponentModel.Activity. For example, the following C# code defines a working (yet useless) WinFX activity:
using System; using System.Workflow.ComponentModel; // bind an XML namespace to our CLR namespace for XAML [assembly: XmlnsDefinition( "http://schemas.example.org/MyStuff", "MyStuff.Activities")] namespace MyStuff.Activities { public class NoOp : Activity {} }To use this activity, we can write a simple workflow in XAML like the following line of code:
<my:NoOp xmlns:my="http://schemas.example.org/MyStuff" />This workflow consists of a simple tree that contains exactly one activity. Because our activity does nothing, when we load and run this workflow, nothing happens. More accurately, the workflow runtime loads the activity tree and calls the Execute method on the root activity. Because our activity didn't override the Execute method, nothing happens.
To get a better handle on how workflow-based programs work, let's define a somewhat more interesting activity, shown in Figure 3. This activity has two noteworthy characteristics. For one, it defines a public property named Text that can be initialized in the XAML-based workflow. More importantly, it overrides the Execute virtual method and responds by printing the Text property to the console. With this activity in place, we can now write a workflow that does something:
<my:WriteLine Text="Hello, world from WinFX." xmlns:my="http://schemas.example.org/MyStuff" />When this is run, the workflow program prints out the string "Hello, world from WinFX."
Like all workflow programs, the previous workflow did not specify what environment to run in. Rather, it is up to the hosting environment (for example, SharePoint®, ASP.NET, a custom application server, or the Windows® shell) to load and run the workflow. The workflow runtime is embeddable in any environment. The runtime is exposed to hosting environments through the System.Workflow.Runtime.WorkflowRuntime class. Causing the workflow to activate is a two phase process: First, you load the program in memory by instantiating WorkflowRuntime and calling the CreateWorkflow method on it, passing either the XAML definition as XML or a compiled activity type. WorkflowRuntime.CreateWorkflow returns an object of type WorkflowInstance, which is a handle to the workflow program instance in memory. Next, in order to begin the actual workflow execution, you simply call WorkflowInstance.Start. The following code shows how to host the workflow runtime and run a workflow from its XAML definition:
// create and start an instance of the workflow runtime WorkflowRuntime runtime = new WorkflowRuntime(); runtime.StartRuntime(); // get an XML reader to the XAML-based workflow definition XmlReader xaml = XmlTextReader.Create("myworkflow.xaml"); // create a running instance of our workflow WorkflowInstance instance = runtime.CreateWorkflow(xaml); instance.Start();
The WorkflowInstance.Start call creates the initial data structures needed to run the workflow and then returns control to the hosting environment. The actual work that is represented by the workflow will be scheduled asynchronously by the runtime using either host-provided threads or the CLR thread pool.
Composite Activities
The WriteLine activity shown previously is an example of an atomic activity—that is, the activity contains all of its execution logic as opaque code. The WinFX workflow programming model also supports the notion of a composite activity, which is an activity that implements its execution logic in terms of one or more child activities. Composite activities are derived from the CompositeActivity class and have a well-known property (Activities) that contains the child activities. In XAML, these child activities are expressed as child elements of the composite.
WinFX includes a set of commonly used composite activities that simplify building workflows. The first one we'll look at is SequenceActivity, as shown in the following:
<SequenceActivity xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow" xmlns:my="http://schemas.example.org/MyStuff" > <my:WriteLine Text="One"/> <my:WriteLine Text="Two"/> <my:WriteLine Text="Three"/> <my:WriteLine Text="Four"/> </SequenceActivity>When run, this workflow writes the following console output:
One Two Three FourThe SequenceActivity executes every child activity in sequence. To support conditional execution, WinFX provides an activity for conditional execution called IfElseActivity.
The IfElseActivity contains one or more child activities, each of which binds a Boolean expression to a sequence of activities that execute if (and only if) the Boolean expression evaluates to true. Figure 4 shows a simple example using IfElseActivity.
This code corresponds to the visual designer view shown in Figure 5. Note that this XAML file uses the x:Class attribute to indicate that the file is defining a new type (MyNamespace.MyWorkflow) rather than an instance of an existing type. The use of x:Class allows us to have a codebehind file that contains C# or Visual Basic code, also shown in Figure 4.
Figure 5 Visual Representation of MyWorkflow.xaml
In this example, we've defined two conditional expressions (Is05 and Is06) that are referenced by the workflow definition using the standard XAML notation for wiring up methods based on symbolic name.
The semantics of the IfElseActivity are simple. Each subordinate IfElseBranchActivity has a condition expression that is evaluated to determine which branch to execute. The first IfElseBranchActivity whose condition expression evaluates to true is selected to run. If no condition expressions evaluate to true, then the last branch will be selected provided it had no condition (which was the case in our workflow). The workflow just shown is equivalent to the following C#:
if (DateTime.Now.Year == 2005) Console.WriteLine("Circa-Whidbey"); else if (DateTime.Now.Year == 2006) Console.WriteLine("Circa-Vista"); else Console.WriteLine("Unknown era");
Though the workflow-based version of the program is more verbose, it is also more transparent, easier to change (especially by non-C# programmers), and can take advantage of the services of the workflow runtime such as program suspension, dehydration, and compensation.
The workflow example using IfElseActivity used CodeConditions for its Boolean expressions. Expressions can also be written in pure XAML without relying on a separate codebehind file (the syntactic details are beyond the scope of this article). Using XAML-based expressions allows the conditions used in the workflow to be expressed in a declarative format that is amenable to visual designers rather than a separate textual code editor that is specific to a given programming language.
In addition to sequencing and conditionals, WinFX includes an activity that models iteration: WhileActivity. Like IfElseBranchActivity, WhileActivity has a Condition property and a set of activity children. Figure 6 shows a simple workflow that uses WhileActivity as well as the codebehind file for this activity.
Using this code, our workflow would print out the message "Hello, WinFX" ten times. Note that in this example, we've used the WinFX-provided CodeActivity to cause the underlying IncrementCounter method to execute at the end of our while loop's body.
Beyond the basic three composite activities that mimic traditional imperative programming languages, WinFX provides a range of more exotic activities that support forward-chaining rule evaluation, event-condition-action (ECA) workflows, and parallelism. The latter is expressed most directly using the WinFX ParallelActivity. Consider this XAML fragment that creates two parallel branches of execution:
<ParallelActivity xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow" xmlns:my="http://schemas.example.org/MyStuff" > <SequenceActivity> <my:WriteLine Text="One"/> <my:WriteLine Text="Two"/> </SequenceActivity> <SequenceActivity> <my:WriteLine Text="Three"/> <my:WriteLine Text="Four"/> </SequenceActivity> </ParallelActivity>
When run, the two sequential branches are scheduled to run in parallel, with the execution logic of ParallelActivity determining the order. Given this XAML, the following would be legal output:
One Three Two FourThis would also be legal output:
Three One Four Two
ParallelActivity makes no promise about the exact order of execution across branches. However, ParallelActivity will not complete until all branches are finished executing. This means that, given the XAML fragment shown in Figure 7, the initial line that is output will always be "Zero" and the sixth and final line output will always be "Five." The intervening output is again nondeterministic.
Inside Activity Execution
One of the key architectural principles of the Windows Workflow Foundation is that specific program semantics are always the responsibility of the individual activities and are never the responsibility of the workflow runtime. The architecture anticipates that developers as well as domain experts will define custom activities that are tailored to given application domains. To keep domain specific semantics out of the runtime, activities and the workflow runtime communicate via a well-defined contract that is expressed using virtual methods on the Activity base class. The workflow runtime does not treat any of the activities shipped as a part of WinFX any differently than the ones you create. Rather, the workflow runtime uses the same contract to interact with both SequenceActivity and a custom activity like the WriteLine activity we've used throughout this article.
To understand how activities and the runtime relate, let's look at the state machine that each activity must implement. That state machine is reflected by the ActivityExecutionStatus enumeration and is shown in Figure 8. Additionally, the Activity base class exposes a CLR event for each state that is raised whenever a transition occurs.
Figure 8 Activity State Machine
Each activity is in exactly one of the six states at any given point in its lifetime—initialized, executing, canceling, faulting, compensating, and closed. The dotted transitions represent the final transitions to the closed state beyond which the activity will not transition any further. In general, activity state transition can be influenced directly by the workflow runtime or by the parent activity requesting the workflow runtime to schedule the transition for its child. In either case, the workflow runtime coordinates (and enforces) the transition of activities from one state to another.
The contract between the workflow runtime and an activity is expressed in terms of the following protected virtual methods on the Activity base class:
void Initialize(IServiceProvider provider) ActivityExecutionStatus Execute(ActivityExecutionContext aec) ActivityExecutionStatus HandleFault(ActivityExecutionContext aec) ActivityExecutionStatus Cancel(ActivityExecutionContext aec)
Additionally, as a part of the workflow runtime-activity contract, compensatable activities are required to implement ICompensatableActivity which has one method called Compensate:
namespace System.Workflow.ComponentModel { public interface ICompensatableActivity { ActivityExecutionStatus.Compensate(ActivityExecutionContextaec) } }Except for the Initialize method (which is called synchronously when the workflow is first started by the host), all of the methods return the state that the activity is in when it returns control to the runtime. For operations that can execute quickly and synchronously, the activity will return the enumeration value for the next state (typically closed):
protected override ActivityExecutionStatus Execute( ActivityExecutionContext aec) { DoWorkSynchronously(); // indicate that we're now in the Closed state return ActivityExecutionStatus.Closed; }In contrast, activities that enqueue asynchronous work must indicate that they are still executing:
protected override ActivityExecutionStatus Execute( ActivityExecutionContext aec) { EnqueueAsynchronousWork(); // indicate that we're still executing return ActivityExecutionStatus.Executing; }It is then the responsibility of the activity to inform the runtime when it transitions to the closed state. This is typically done in response to some event raised by the runtime itself:
// Activity's event handler that is registered with the runtime void LastStageOfAsyncWork(object sender, EventArgs e) { // grab a reference to the runtime ActivityExecutionContext aec = (ActivityExecutionContext)sender; // inform the runtime that we're now // closed aec.CloseActivity(); }
This event handler demonstrates an important aspect of activity development. Each time the runtime calls into your activity, it provides a reference to itself of type ActivityExecutionContext. This reference is the only API into the runtime for activities to use. More importantly, the reference that is passed to you is only valid for as long as your CLR method is executing. Once your method returns control to the runtime, the context reference is no longer valid. The runtime will enforce this with an exception if you make the mistake of caching the reference in a field for later use.
Despite the inherently asynchronous nature of the runtime/activity contract, the runtime will never make concurrent calls to a given activity. Rather, any given activity is guaranteed to have at most one method invocation executing at a given instant. This vastly simplifies the task of writing activities in languages like C# or Visual Basic.
Normal Activity Execution
Under normal circumstances, an activity experiences two transitions: from initialized to executing and from executing to closed. The first transition (from initialized to executing) occurs when the workflow runtime is able to assemble the required resources for the activity's execution and schedules the Execute method on the activity. The second transition (from executing to closed) happens either synchronously when the activity's Execute method returns ActivityExecutionStatus.Closed or, if the Execute method returns ActivityExecutionStatus.Executing, asynchronously when a subsequent event handler on the activity runs and calls ActivityExecutionContext.CloseActivity to communicate the state transition to the runtime.
The Execute method of a composite activity typically asks the workflow runtime to schedule the execution of its child activities, and it subscribes to their status changes asynchronously. It then remains in the executing state at least while one of its children is in the executing state.
To better understand the normal activity execution protocol, we can look at the implementation of the SequenceActivity. For simplicity, we will just focus on the normal execution path of activity (initialized to executing to closed), and thus only look at the Execute method.
In the implementation of the Execute method, SequenceActivity subscribes to the closed event of its first child and requests the workflow runtime to schedule the execution of its first child by calling the ExecuteActivity method on the runtime context, as you can see in Figure 9.
The call to ExecuteActivity causes the scheduler to run the child activity using the standard initialized-to-executing-to-closed sequence. When the child gets scheduled, the workflow runtime will place the child in the executing state and call the Execute method of the child. Eventually, when the child goes to the closed state, the event handler for the Closed event (SequenceActivity.OnChildClosed) will get called by the runtime, passing a new ActivityExecutionContext as the first parameter. Figure 10 shows the definition of that event handler for SequenceActivity.
In this event handler, we check to see if there are any remaining children. If there are, we schedule the next one, using the same event handler method we used before as the completion routine. If there are no more children to run, then we inform the runtime that we are ready to transition to the Closed state by calling the CloseActivity method.
Context and State Management
Astute readers will notice that activities interact with the runtime through the ActivityExecutionContext (AEC). Conceptually, the AEC acts as a serializable execution environment in which activities get executed and the object state of the activities is automatically managed in the environment. The workflow runtime allows activities to create new persistent execution environments during normal activity execution. Activities that require their children to execute more than once need to create new AECs for each execution. The canonical example of this is WhileActivity, which mimics the standard C while loop.
Figure 11 shows the multiple AECs spawned for each of the iterations of a WhileActivity. Assuming that the WhileActivity in the program iterates three times, three instances of SequenceActivity within the WhileActivity are executed dynamically in distinct AECs, each with its own state. By giving each iteration of the while loop its own AEC, the runtime has the ability to undo the execution of each iteration when running compensation logic.
Figure 11 Activity Execution Contexts Spawned for Iteration
Workflows can run for long periods of time. At any given instant, a workflow instance may be considered idle, which means that the runtime has no runnable work for any of the workflow's activities. When a workflow instance is idle, its state may be serialized to a host-provided store and removed from memory. This process is called passivation. The serialized state of the workflow instance can be used to reactivate the instance in memory, at which point the instance is restored to its state and is available to do work. Reactiviation typically happens when the host detects that new work is available, usually by noticing that an external event has occurred.
Consider the following simple workflow program that uses the built-in DelayActivity to cause the workflow to go idle in the middle of the sequence:
<SequenceActivity xmlns="http://schemas.microsoft.com/winfx/2006/xaml/workflow" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="http://schemas.example.org/MyStuff" > <my:WriteLine Text="One"/> <my:WriteLine Text="Two"/> <DelayActivity TimeoutDuration="00:10:00" /> <my:WriteLine Text="Three"/> <my:WriteLine Text="Four"/> </SequenceActivity>In these snippets, when DelayActivity executes, it sets up a subscription for a timer and returns ActivityExecutionStatus.Executing. At this point, the workflow instance has no runnable work and is therefore considered idle. At idle-time, the workflow runtime will attempt to persist the state of the workflow instance using a host-provided persistence service. The host environment may provide an implementation of the abstract WorkflowPersistenceService type when initializing the runtime. If the host provides a concrete implementation of this type (through the WorkflowRuntime.AddService method), then the runtime will call the host's SaveWorkflowInstanceState method, giving the host a chance to save the state to a persistent store. Each workflow instance is identified by a unique runtime-generated GUID, which the store is expected to use to identify the persisted state of the instance for later retrieval.
The runtime persists the workflow instance at idle-time for reliability purposes. By default, the instance is retained in memory. The host can cause the workflow instance to be evicted from memory either by setting the runtime-wide UnloadOnIdle property to true or by calling the WorkflowInstance.Unload method on a specific instance. The host can detect when an instance goes idle by registering for the WorkflowIdled event on the runtime, like so:
WorkflowRuntime runtime = new WorkflowRuntime(); runtime.StartRuntime(); runtime.WorkflowIdled += delegate(object s, WorkflowEventArgs e) { ... };
To force a saved workflow to be reloaded into memory from the host's persistence service, you can call the WorkflowRuntime.GetWorkflow method, passing in the instance's unique GUID.
Where Are We?
We barely scratched the surface of the rich workflow programming model and capabilities provided by Windows Workflow Foundation. Windows Workflow Foundation provides a way to declaratively and more naturally express the semantics of your application in terms of activities, including the program control flow, transactions, concurrency, synchronization, exception handling, and interactions with other applications. It also provides a rich set of services beyond those available in the base CLR, including automatic program persistence, compensating transactions, and runtime inspection of the program state. The workflow runtime can be hosted in any CLR app domain and thus can be embedded in any application or application container. You can download the WinFX beta from Windows Vista and WinFX Beta and start writing activities today.
Don Box is an architect in the Microsoft Connected Systems Division where he works on programming models and plumbing to support building programs that talk to other programs. Prior to joining Microsoft, Don led a small but multi-national cult of IUnknown worshippers. Reach Don at www.pluralsight.com/blogs/dbox.
Dharma Shukla is a development lead on the Windows Workflow Foundation team at Microsoft responsible for the workflow programming model and developer tools. He is also working on a Windows Workflow book. Reach Dharma at www.dharmashukla.com.