Introduction
This article contains the world’s most trivial and useless Windows Workflow Foundation (WF) application, as well as some background on WF itself. The purpose of this software freak show is to expose a WF newcomer to the basics of how to get started with this exciting new part of .NET 3.0. It does not get into any sophisticated scenarios and certainly does not contain any WF best practices. It just shows you how to put together a minimal WF app. If you have more realistic needs and requirements, you might use this code as a starting place for real development.
I will admit right here (New York City) and now (November 2006) that I am by no means a WF expert. I’m just really excited about WF, and have been reading about it and playing around with it in my free time. This article was written purely out of excitement for the technology, so I hope that excitement shines through and helps get you excited too.
The application shown in this article is a mutation of the demo app used in the beginning of the excellent book “Essential Windows Workflow Foundation” by Dharma Shukla and Bob Schmidt. I highly recommend that book if you want to get serious about learning WF. You might also want to check out this article on MSDN, by Don Box and Dharma Shukla.
WTF is WF?
Before we get into building a WF application, let’s take a moment to grasp the general ideas behind WF. WF stands for Windows Workflow Foundation. It is a subsystem of the .NET Framework 3.0 which provides a runtime for creating and executing workflow-based applications. That’s nice marketing babble, but what exactly does that mean? Good question…
From a very high-level perspective, WF allows you to create programs that can be persisted to a backing store when they are inactive and then resumed when necessary. You can declare the overall program flow in an XML-based language known as eXtensible Application Markup Language (XAML) and load/execute your XAML workflow at runtime. Expressing your application’s general logic flow in a markup language simplifies the development process greatly, especially because it opens new possibilities for the use of graphical design tools in creating software.
Why Would I Use WF?
Many applications are reactive by nature. They sit around for an indefinite period of time waiting for something to happen (perhaps a file to be created in a certain directory). When that external stimulus arrives the application gets busy processing the incoming data. The problem with this common scenario is that performance and scalability are greatly affected by the fact that while an application is waiting for external input, it consumes processing time from whatever thread it runs on.
WF provides a solution to that problem. Since a workflow in WF is represented as a tree of objects, and those objects support being serialized/deserialized, WF workflows have built-in support for being saved to and loaded from a database. I don’t mean that just the data manipulated by the application is saved; I mean that the “program” itself is saved.
The state of the application is effectively “frozen” and put into a cryogenic freezer, so to speak. When it comes time to resume the workflow processing (i.e. the external stimulus arrives) the “frozen” workflow is thawed out and it continues executing as normal. Keep in mind that the workflow could be resumed on a different computer, a thousand miles away from where it was frozen, fifteen months later. This brings tremendous gains in terms of performance and scalability, because the workflow is not bound to a specific thread, process, or even computer.
That’s all of the introductory material I’m going to provide on WF. I didn’t explain nearly all of what WF is all about. If you are interested in a more thorough and circumspect explanation of WF, I recommend you read the book mentioned previously (“Essential Windows Workflow Foundation”). Now let’s play with some WF code!
The World’s Stupidest WF Application
The application I’m going to present to you here asks the user for his/her name, waits for the user to type it in, and then prints “Hello, UserName!” to the console. Obviously this application is only of interest because it uses Windows Workflow Foundation to perform its magic. I tried to keep the use of WF as simple and minimal as possible, just so that it’s easy to see how one goes about setting up an application which uses WF.
There are two assemblies involved in this application:
- FirstWFLibrary.DLL – This assembly contains the custom WF Activities which are used to perform the tasks of printing output to the console, and getting the user’s name.
- FirstWFApp.EXE – This assembly is a console app which loads the WF WorkflowRuntime and puts the Activities in FirstWFLibrary to use.
Each of the two assemblies contains two pieces of the puzzle:
FirstWFLibrary.DLL
- Custom Activities – The fundamental unit of work in a WF workflow is an 'activity.' All custom activities in a WF application derive from the
Activity
base class and override its protected Execute
method to provide their own execution logic.
- Namespace Mapping – In order to be able to use our custom Activities in XAML we need to provide a way to tell the XAML parser the namespace(s) in which those classes exist. This is accomplished by applying an attribute to the assembly, as we will see later.
FirstWFApp.EXE
- Workflow Declaration – In this application the workflow required for reading the user’s name and displaying it back to him/her is expressed in XAML. The XAML file in this assembly uses the custom Activities declared in the FirstWFLibrary assembly. Note, it is not required that workflows are declared in XAML. It is entirely possible to declare them in other formats, such as C# code. I chose to express the workflow in XAML for this application because it’s more fun that way. :)
- Workflow Runtime Host – The last piece of the puzzle is a class which loads the WF runtime, configures it, and then tells it to start running. The
EntryPoint
class in this application takes care of that job.
The rest of this article examines each part of the application listed above.
Custom Activities
As mentioned previously, this demo application has three tasks to perform. First it must display a message in the console window which asks the user for his/her name. Then it must wait for the user to enter their name and press Enter. Finally it displays another message to the user, which includes their name in it.
Each of those tasks is represented as a separate Activity
-derived class. Instances of those classes will perform the actual work necessary to make the program execute. First let’s see how the initial prompt is displayed to the console.
public class PromptForUserName : Activity
{
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext )
{
Console.Write( "Please enter your name and press Enter: " );
return ActivityExecutionStatus.Closed;
}
}
This class is a perfect demonstration of how to create an activity which can be included in a WF workflow. It inherits from the System.Workflow.ComponentModel.Activity
class and overrides the Execute
method to provide custom activity execution logic. Since this activity is logically complete after it writes a message to the console, it returns ActivityExecutionStatus.Closed
to inform the WF runtime that it is done.
You might be wondering why an activity would return from the Execute
method if it was not done executing. Remember earlier on I mentioned that WF workflows can be “passivated” and stored in a database until it needs to continue executing? Well, an activity’s Execute
method will return ActivityExecutionStatus.Executing
if it cannot complete until external input eventually arrives.
In fact, the next step of The World’s Stupidest WF Application requires an indefinite period of time to elapse before it can continue processing. It might take the user three seconds or three days to type in his/her name. During that time the workflow will have nothing to process. If this was a less stupid WF application, we might decide to passivate the workflow until the user name finally arrives, at which point we would resume the workflow and let it continue. We’re not doing that here, but this next activity shows how to set up a “bookmark” so that the WF runtime can inform the activity when input has arrived.
public class ReadConsoleLine : Activity
{
#region InputText Property
private string inputText;
public string InputText
{
get { return this.inputText; }
}
#endregion // InputText Property
#region Execute [override]
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext )
{
WorkflowQueue workflowQueue = this.GetWorkflowQueue( executionContext );
workflowQueue.QueueItemAvailable += ProcessQueueItemAvailable;
workflowQueue.QueueItemAvailable += CloseActivity;
return ActivityExecutionStatus.Executing;
}
#endregion // Execute [override]
#region Event Handlers
void ProcessQueueItemAvailable( object sender, QueueEventArgs e )
{
WorkflowQueue workflowQueue = this.GetWorkflowQueue(
sender as ActivityExecutionContext );
if( workflowQueue.Count > 0 )
this.inputText = workflowQueue.Dequeue() as string;
}
void CloseActivity( object sender, QueueEventArgs e )
{
ActivityExecutionContext executionContext =
sender as ActivityExecutionContext;
WorkflowQueuingService queuingService =
executionContext.GetService<WorkflowQueuingService>();
queuingService.DeleteWorkflowQueue( this.Name );
executionContext.CloseActivity();
}
#endregion // Event Handlers
#region Private Helpers
WorkflowQueue GetWorkflowQueue( ActivityExecutionContext executionContext )
{
WorkflowQueue queue;
WorkflowQueuingService queuingService =
executionContext.GetService<WorkflowQueuingService>();
if( queuingService.Exists( this.Name ) )
queue = queuingService.GetWorkflowQueue( this.Name );
else
queue = queuingService.CreateWorkflowQueue( this.Name, true );
return queue;
}
#endregion // Private Helpers
}
In the ReadConsoleLine
’s Execute
method a “bookmark” is established and the method immediately returns control back to the WF runtime. However, it returns ‘Executing
’ to let the WF runtime know that it should not go on processing any other activities yet. When the user input finally arrives, the ReadConsoleLine
’s ProcessQueueItemAvailable
and CloseActivity
event handling methods will be invoked. The first of those methods stores the user’s name in a private variable. The other method cleans up and closes the activity, so that the WF runtime can continue processing other activities.
The last activity is responsible for printing out a greeting to the user, with his/her name in it. This activity has a dependency property called UserName
. As we will see later, this property is bound to the input value received by the ReadConsoleLine
activity. The data binding is established in the XAML declaration of these objects. We’ll get to that soon, but now let’s see the GreetUser
activity.
public class GreetUser : Activity
{
public static readonly DependencyProperty UserNameProperty;
static GreetUser()
{
UserNameProperty = DependencyProperty.Register(
"UserName",
typeof( string ),
typeof( GreetUser ) );
}
public string UserName
{
get { return (string)GetValue( UserNameProperty ); }
set { SetValue( UserNameProperty, value ); }
}
protected override ActivityExecutionStatus Execute(
ActivityExecutionContext executionContext )
{
string greeting = String.Format( "Hello, {0}!", this.UserName );
Console.WriteLine( greeting );
return ActivityExecutionStatus.Closed;
}
}
Namespace Mapping
In order to use our custom activities in XAML we need to provide a way for the XAML parser to know what CLR namespace those classes reside in. Since XAML is an XML-language, we need to provide a way of associating the CLR namespace with an arbitrary XML namespace (basically, a URI). This can be done in any code file in the project, but I created an AssemblyInfo.cs just for the sake of tradition. Here’s the contents of that file:
using System.Workflow.ComponentModel.Serialization;
[assembly: XmlnsDefinition( "http://FirstWFLibrary", "FirstWFLibrary" )]
Workflow Declaration
Now that we have the custom activities needed to perform our program logic, and their namespace is mapped, we can create instances of those types. In this demo we will create them in XAML. Think of XAML as just an all-purpose object instantiation markup language. It allows you to configure objects and express the hierarchical relationships between them very easily.
Here is the XAML declaration of The World’s Stupidest Workflow:
<wf:SequenceActivity
xmlns="http://FirstWFLibrary"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wf="http://schemas.microsoft.com/winfx/2006/xaml/workflow"
>
<PromptForUserName />
<ReadConsoleLine x:Name="getUserName" />
<GreetUser UserName="{wf:ActivityBind Name=getUserName, Path=InputText}" />
</wf:SequenceActivity>
The root activity in the workflow is a SequenceActivity
object, which is a class provided in the WF framework. It is a CompositeActivity
-derived class which executes its child activities in the order they are declared, only executing one child activity after the previous child activity is complete.
The root activity contains the XML namespace mappings. The default XML namespace is mapped to the URI specified in the XmlnsDefinition
attribute in the previous section of the article. That allows us to refer to our custom activity types without any namespace prefix.
Another point of interest is the relationship between the ReadConsoleLine
and the GreetUser
activities. The UserName
property of the latter is bound to the InputText
property of the former. This binding allows the text typed into the console to be transferred from one activity to another. For a property to be the target of a binding, it must be a 'dependency property.' As we saw in the 'Custom Activities' section, there is a little more code involved with creating a dependency property than just a normal property, but dependency properties can be used in ways that normal properties cannot. You can read about those differences in the SDK, if you care.
Workflow Runtime Host
You can host the WF runtime in a variety of ways, but this demo just uses a plain vanilla console application. There are a few steps you must follow to get the WF runtime up and running in your AppDomain. The following method is where The World’s Stupidest WF Application hosts the WF runtime (this method is, of course, in a class):
public static void Main()
{
using( WorkflowRuntime workflowRuntime = new WorkflowRuntime() )
{
TypeProvider typeProvider = new TypeProvider( workflowRuntime );
typeProvider.AddAssemblyReference( "FirstWFLibrary.dll" );
workflowRuntime.AddService( typeProvider );
workflowRuntime.StartRuntime();
WorkflowInstance workflowInstance;
using( XmlTextReader xmlReader =
new XmlTextReader( @"..\..\HelloUserWorkflow.xaml" ) )
{
workflowInstance = workflowRuntime.CreateWorkflow( xmlReader );
workflowInstance.Start();
}
string userName = Console.ReadLine();
workflowInstance.EnqueueItem( "getUserName", userName, null, null );
Console.ReadLine();
workflowRuntime.StopRuntime();
}
}
I’m not going to explain that method line by line, because it is commented well enough. The one point of interest I will mention is that once the WorkflowInstance
starts, the method then calls Console.ReadLine
to get the user’s name. Once the name is retrieved, it is put onto the “getUserName” workflow queue, which was created by and for the ReadConsoleLine
activity. This is an example of external input being provided to the workflow, causing it to resume processing. Once the user’s name is put onto the workflow queue, the ReadConsoleLine
activity will have its callback methods invoked so that it can finish executing.
Conclusion
This article showed how to create an application which uses Windows Workflow Foundation. Hopefully it left you feeling like The World’s Stupidest WF Application deserves its name, but also that you understand the fundamentals of WF and how to use it in an application.
As I mentioned before, this application does not at all need to use the powers of WF, but it does convey the basic concepts involved. It shows how WF workflows are a tree of activities, how those activities can use the concept of “bookmarks” to indicate where processing should continue after external stimulus occurs, how a workflow can be declared in XAML, and how to host the WF workflow runtime. Along the way I mentioned that WF workflows can be passivated and resumed, which provides a powerful means of improving an application’s scalability and performance.