This chapter focuses on hosting and executing workflows. Regardless of the type of host application, the WorkflowInvoker
and WorkflowApplication
classes are used to execute your workflows. The chapter describes how to use each class and then demonstrates features of each class with a number of short examples.
The WorkflowInvoker
and WorkflowApplication
classes require that you create an instance of the workflow type that you want to execute. Windows Workflow Foundation (WF) also provides the ability to load and execute a workflow directly from a Xaml document. The ActivityXamlServices
class provides this functionality and is described and demonstrated in this chapter.
Invoking a workflow from an ASP.NET web application is a common scenario that is demonstrated later in the chapter. Finally, the chapter concludes with an example that demonstrates how to execute and manage multiple instances of a workflow from a Windows Presentation Foundation (WPF) application.
Note Workflows that use Windows Communication Foundation (WCF) messaging can be self-hosted using the WorkflowServiceHost
class. Chapter 9 discusses the use of this class.
The WorkflowInvoker
class is the simplest way to execute a complete workflow or a single activity. It allows you to execute an activity on the current thread with the simplicity of calling a method. This simplicity is primarily due to the use of the current thread for execution. This eliminates the need for thread synchronization code between the current thread and a separate workflow thread. It also eliminates the need for the class to support a wide range of state notification members and events.
But even with this simplicity, the class supports a number of variations and options for workflow and activity execution, which are described in the following sections.
One way to use the WorkflowInvoker
class is via a set of static Invoke
methods. To use one of these methods, you create an instance of the workflow that you want to execute and pass it as an argument to the appropriate method. If the workflow requires input arguments, you pass them as a dictionary using one of the overloads of the Invoke
method. Any output arguments are returned from the method as another dictionary. Other Invoke
method overloads allow you to pass combinations of the workflow to execute, input arguments, and a timeout value.
Here are the variations of the static Invoke
method that target the execution of workflows:
public static IDictionary<string, object> Invoke(Activity workflow);
public static IDictionary<string, object> Invoke(Activity workflow,
IDictionary<string, object> inputs);
public static IDictionary<string, object> Invoke(Activity workflow,
TimeSpan timeout);
public static IDictionary<string, object> Invoke(Activity workflow,
IDictionary<string, object> inputs, TimeSpan timeout);
The timeout value determines the maximum amount of time that you want to allow the workflow to execute. If the elapsed time for execution of the workflow exceeds the specified timeout value, the workflow instance is terminated, and a System.TimeoutException
is thrown.
The class also includes a set of static Invoke
methods that target the execution of a single activity. The activity must derive from one of the generic activity base classes (Activity<TResult>
, CodeActivity<TResult>
, AsyncCodeActivity<TResult>
, NativeActivity<TResult>
) that return an output argument named Result
. These Invoke
methods accept a single generic argument that identifies the return type of the activity. Here are the method overloads:
public static TResult Invoke<TResult>(Activity<TResult> workflow);
public static TResult Invoke<TResult>(Activity<TResult> workflow,
IDictionary<string, object> inputs);
public static TResult Invoke<TResult>(Activity<TResult> workflow,
IDictionary<string, object> inputs, TimeSpan timeout);
public static TResult Invoke<TResult>(Activity<TResult> workflow,
IDictionary<string, object> inputs,
out IDictionary<string, object> additionalOutputs, TimeSpan timeout);
These methods simplify execution of a single activity since they eliminate the need to retrieve the output from a dictionary. The final method overload in the list does provide a way to retrieve additional output arguments.
In addition to the static Invoke
method, you can create an instance of the WorkflowInvoker
class and execute the workflow using instance methods. Using the instance methods provides you with a few additional options such as the ability to add workflow extensions to the instance before execution begins (using the Extensions
property).
Note Workflow extensions are covered in Chapter 8.
After creating an instance of the WorkflowInvoker
class, you call one of these Invoke
methods to begin execution:
public IDictionary<string, object> Invoke();
public IDictionary<string, object> Invoke(IDictionary<string, object> inputs);
public IDictionary<string, object> Invoke(TimeSpan timeout);
public IDictionary<string, object> Invoke(IDictionary<string, object> inputs,
TimeSpan timeout);
The WorkflowInvoker
class is designed to execute a workflow or activity on the current thread; however, it does support a set of asynchronous methods:
public void InvokeAsync();
public void InvokeAsync(IDictionary<string, object> inputs);
public void InvokeAsync(object userState);
public void InvokeAsync(TimeSpan timeout);
public void InvokeAsync(IDictionary<string, object> inputs, object userState);
public void InvokeAsync(IDictionary<string, object> inputs, TimeSpan timeout);
public void InvokeAsync(TimeSpan timeout, object userState);
public void InvokeAsync(IDictionary<string, object> inputs, TimeSpan timeout,
object userState);
When you use one of the InvokeAsync
methods, you must add a handler to the InvokeCompleted
event in order to retrieve any output arguments.
But contrary to their name, these methods don’t actually begin execution of the workflow or activity on a separate thread. The InvokeAsync
methods differ from the Invoke
methods only in how they handle resumption of workflows that become idle.
To illustrate the difference in behavior between the Invoke
and InvokeAsync
methods, imagine a workflow that does some processing but also includes a Delay
activity. If you start the workflow using either method, the workflow begins execution on the current thread. If you started it using the Invoke
method, the workflow will resume execution on the same host thread after the delay expires. In contrast with this behavior, if you started the workflow using one of the InvokeAsync
methods, execution after the delay will resume on a background thread from the thread pool.
If the workflow you are executing never becomes idle, there is no noticeable difference between Invoke
and InvokeAsync
. For full asynchronous operation, you need to use the WorkflowApplication
class, which is discussed later in this chapter.
The WorkflowInvoker
class also provides a set of BeginInvoke
and EndInvoke
methods. These methods operate in a similar way as the InvokeAsync
method, but they support the familiar .NET asynchronous pattern.
On the surface, the differences between InvokeAsync
and BeginInvoke
are subtle. However, under the covers they do operate differently. The InvokeAsync
method uses the current synchronization context, whereas BeginInvoke
does not. This means that you can manually set the Current
property of the SynchronizationContext
class to a different synchronization context object, and this will change the behavior of InvokeAsync
. For example, you might want to use a synchronization context designed for Windows Presentation Foundation, Windows Forms, or ASP.NET applications if the workflows that you are executing needed to directly interact with those frameworks.
In the series of short examples that follow, I will demonstrate some of the most common ways to use the WorkflowInvoker
class. The same example workflow (along with one custom activity) will be used for all of the examples in this chapter. Since the goal is to demonstrate the various ways to host workflows, the workflow itself is very simple and totally contrived. It accepts a single integer as an input argument and returns a string containing the integer.
The workflow includes a Delay
activity to introduce a short delay in processing. This is necessary in order to demonstrate the behavior when the workflow becomes idle in subsequent examples. The short delay also provides an opportunity to manually manage the workflow instance by canceling it and so on.
The workflow also writes messages that identify the current managed thread. Using these messages, you can determine when the workflow is using the host thread and when it is executing asynchronously on its own thread.
You will complete these tasks to implement this example:
- Declare the
HostingDemoWorkflow
.- Implement the code to host the workflow.
This example workflow will be used throughout the remainder of this chapter for several examples. For this reason, it should be declared in an activity library that can be referenced by other projects.
Create a new project using the Activity Library project template. Name the project ActivityLibrary
, and add it to a new solution that you create for this chapter. You can delete the Activity1.xaml
file since it won’t be used.
Add a new workflow to the project using the Add Item Activity template, and name it HostingDemoWorkflow
. Using the workflow designer, add these arguments to the workflow:
The ArgNumberToEcho
argument is the integer that is echoed back in the Result
string argument. The ArgTextWriter
argument is used to override the default WriteLine
activity behavior that writes messages to the console. If an optional TextWriter
property is provided, the WriteLine
activity writes the messages to the TextWriter
instead of the console. This feature is used later in the chapter.
Please follow these steps to complete the declaration of the workflow:
- Add a
Sequence
activity to the empty workflow as the root activity.- Add a
WriteLine
activity to theSequence
activity. Set theText
property toString.Format("Workflow: Started - Thread:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)
. Set theTextWriter
property toArgTextWriter
.- Add another
WriteLine
activity below the previous one. Set theText
property to"Workflow: About to delay"
and theTextWriter
property toArgTextWriter
.- Add a
Delay
activity below the previousWriteLine
. Set theDuration
property toTimeSpan.FromSeconds(3)
to provide a three-second delay.- Add another
WriteLine
activity below theDelay
. Set theText
property to"Workflow: Continue after delay"
and theTextWriter
property toArgTextWriter
.- Add an
Assign
activity below theWriteLine
. Set theAssign.To
property toResult
and theAssign.Value
property toString.Format("Result is {0}", ArgNumberToEcho)
.- Add a final
WriteLine
activity below theAssign
activity. Set theText
property toString.Format("Workflow: Completed - Thread:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId)
. Set theTextWriter
property toArgTextWriter
.
The completed workflow should look like Figure 4-1.
To host the workflow, create a new project using the Workflow Console Application template. Name the project InvokerHost
, and add it to the solution for this chapter. You can delete the Workflow1.xaml
file that was added for you since it won’t be used. Add a project reference to the ActivityLibrary
project, which is in the same solution.
Here is the code for the Program.cs
file:
namespace InvokerHost
{
using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;
using ActivityLibrary;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Host: About to run workflow - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
try
{
IDictionary<String, Object> output = WorkflowInvoker.Invoke(
new HostingDemoWorkflow(),
new Dictionary<String, Object>
{
{"ArgNumberToEcho", 1001},
});
Console.WriteLine("Host: Workflow completed - Thread:{0} - {1}",
System.Threading.Thread.CurrentThread.ManagedThreadId,
output["Result"]);
}
catch (Exception exception)
{
Console.WriteLine("Host: Workflow exception:{0}:{1}",
exception.GetType(), exception.Message);
}
}
}
}
This hosting code displays the current managed thread before and after the workflow is executed. This allows you to compare the thread ID with the value that is displayed by the workflow itself. An instance of the workflow to execute and a single argument with the number to echo are passed to the static Invoke
method of the WorkflowInvoker
class. The Result
output argument is returned in a dictionary by the Invoke
method.
After building the solution and running the InvokerHost
project, you should see results similar to these:
Host: About to run workflow - Thread:1
Workflow: Started - Thread:1
Workflow: About to delay
Workflow: Continue after delay
Workflow: Completed - Thread:1
Host: Workflow completed - Thread:1 - Result is 1001
As expected, the results indicate that the workflow executed on the current host thread.
Note The actual managed thread ID that you see in your results may be different from what is shown here. The important point is that the same thread ID (regardless of the actual value) is displayed by the host and the workflow instance.
In the previous example, a Dictionary<String,Object>
was used to pass an argument to the workflow. A simpler and more type-safe way to pass arguments to a workflow is to set the public properties that are generated for each input argument. For example, the same workflow could be executed like this with the same results:
namespace InvokerHost
{
…
class Program
{
static void Main(string[] args)
{
…
try
{
HostingDemoWorkflow wf = new HostingDemoWorkflow();
wf.ArgNumberToEcho = 1001;
IDictionary<String, Object> output = WorkflowInvoker.Invoke(wf);
…
}
…
}
}
}
The choice as to which mechanism to use for passing input arguments really depends on your needs. If you need to create and execute a single instance of a workflow, then I personally favor using the argument properties. On the other hand, if you need to execute multiple instances of a particular workflow with different sets of parameters, you’re better off passing the arguments as a dictionary. During the construction of a workflow instance, metadata about the workflow is detected and cached. This is a relatively expensive operation that you should try to avoid. Using a dictionary for arguments allows you to reuse a single instance of the workflow definition, passing a different set of arguments each time the workflow is executed.
Tip You can pass input arguments to the workflow using the argument properties that are provided. However, even though properties are also generated for the output arguments, you can’t use them to obtain values when the workflow has completed. The reason for this difference is that the workflow instance passed to WorkflowInvoker
(or WorkflowApplication
) really represents the definition of the workflow. It is merely a template from which a runnable workflow instance is constructed and executed by the workflow runtime. Setting input argument properties makes sense since they become part of the workflow definition. Retrieving output arguments would require you to reference the actual workflow instance that was executed. Output arguments are not propagated back to the workflow definition.
One of the overloads of the static Invoke
method allows you to specify a timeout value. This is used to limit the amount of time that the workflow is allowed to execute. If the workflow exceeds the TimeSpan
value that you specify, the workflow is terminated, and an exception is thrown.
To see this in action, you only have to make a slight modification to the previous hosting code. Here is a partial listing of the Program.cs
file showing the lines of code that require a change:
namespace InvokerHost
{
…
class Program
{
static void Main(string[] args)
{
…
try
{
HostingDemoWorkflow wf = new HostingDemoWorkflow();
wf.ArgNumberToEcho = 1001;
IDictionary<String, Object> output = WorkflowInvoker.Invoke(
wf, TimeSpan.FromSeconds(1));
…
}
…
}
}
}
In this example, the TimeSpan
is set to one second. Since the workflow includes a Delay
activity that is set to three seconds, the workflow should exceed the maximum timeout value and throw an exception. When you run the project again, the results should look like this:
Host: About to run workflow - Thread:1
Workflow: Started - Thread:1
Workflow: About to delay
Host: Workflow exception:System.TimeoutException:The operation did not complete
within the allotted timeout of 00:00:01. The time allotted to this operation may
have been a portion of a longer timeout.
The WorkflowInvoker
class also makes it easy to execute an activity that derives from one of the generic base activity classes. The advantage to using one of these specialized Invoke
methods is that the result is returned directly from the method rather than as a dictionary. This enables you to more easily incorporate execution of activities within your procedural C# code.
To complete this example, you need a simple activity to execute. The activity must derive from one of the generic activity base classes: Activity<TResult>
, CodeActivity<TResult>
, AsyncCodeActivity<T>
, or NativeActivity<TResult>
. Add a new code activity to the ActivityLibrary
project, and name it HostingDemoActivity
. Here is the complete code for this activity:
using System;
using System.Activities;
namespace ActivityLibrary
{
public sealed class HostingDemoActivity : CodeActivity<String>
{
public InArgument<Int32> NumberToEcho { get; set; }
protected override string Execute(CodeActivityContext context)
{
return String.Format("Result is {0} - Thread:{1}",
NumberToEcho.Get(context),
System.Threading.Thread.CurrentThread.ManagedThreadId);
}
}
}
In similar fashion to the HostingDemoWorkflow
, this activity defines a single input argument that is echoed back to the caller in a result string.
To execute an activity using the WorkflowInvoker
class, you need to use one of the Invoke
methods that accept a generic type parameter. The type that you specify must correspond to the return type of the activity. In the case of the HostingDemoActivity
that you just implemented, the return type is a string.
You can modify the Program.cs
file in the InvokerHost
project as shown here:
namespace InvokerHost
{
using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;
using ActivityLibrary;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Host: About to run Activity - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
try
{
HostingDemoActivity activity = new HostingDemoActivity();
activity.NumberToEcho = 1001;
String result = WorkflowInvoker.Invoke<String>(activity);
Console.WriteLine(
"Host: Activity completed - Thread:{0} - {1}",
System.Threading.Thread.CurrentThread.ManagedThreadId,
result);
}
catch (Exception exception)
{
Console.WriteLine("Host: Activity exception:{0}:{1}",
exception.GetType(), exception.Message);
}
}
}
}
The call to the generic version of the Invoke
method simplifies the code by eliminating the dictionary of output arguments.
When I run the revised InvokerHost
project, the results look like this:
Host: About to run Activity - Thread:1
Host: Activity completed - Thread:1 - Result is 1001 - Thread:1
The WorkflowInvoker
class also provides instance methods that allow you to execute a workflow or a single activity. One of the more interesting instance methods is InvokeAsync
. This method allows you to begin execution of the workflow using the host thread but to then switch to a thread from the thread pool when the workflow resumes after becoming idle.
Similar functionality is also available using the BeginInvoke
and EndInvoke
methods. These methods implement the standard .NET asynchronous pattern.
To demonstrate the InvokeAsync
method, you can modify the Program.cs
file in the InvokerHost
project. Here is the updated code:
namespace InvokerHost
{
using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;
using ActivityLibrary;
class Program
{
This example requires the use of a thread synchronization object such as the AutoResetEvent
shown here. The object allows the host thread to wait until the workflow execution completes on a different thread.
private static AutoResetEvent waitEvent = new AutoResetEvent(false);
static void Main(string[] args)
{
Console.WriteLine("Host: About to run workflow - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
try
{
After creating a new instance of the WorkflowInvoker
class, a handler is added to the InvokeCompleted
event. This event is the notification that the workflow has completed and enables retrieval of the output arguments.
WorkflowInvoker instance = new WorkflowInvoker(
new HostingDemoWorkflow());
instance.InvokeCompleted +=
delegate(Object sender, InvokeCompletedEventArgs e)
{
Console.WriteLine(
"Host: Workflow completed - Thread:{0} - {1}",
System.Threading.Thread.CurrentThread.ManagedThreadId,
e.Outputs["Result"]);
waitEvent.Set();
};
instance.InvokeAsync(new Dictionary<String, Object>
{
{"ArgNumberToEcho", 1001},
});
Console.WriteLine("Host: Workflow started - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
waitEvent.WaitOne();
}
catch (Exception exception)
{
Console.WriteLine("Host: Workflow exception:{0}:{1}",
exception.GetType(), exception.Message);
}
}
}
}
After building the solution, you should be ready to run the InvokeHost
project. Here are my results:
Host: About to run workflow - Thread:1
Workflow: Started - Thread:1
Workflow: About to delay
Host: Workflow started - Thread:1
Workflow: Continue after delay
Workflow: Completed - Thread:3
Host: Workflow completed - Thread:3 - Result is 1001
Notice that the workflow displays a different managed thread ID when it resumes after the short delay. Prior to the delay, the workflow was executing on the host thread. The switch to a separate thread occurred only when the workflow became idle. When that occurred, the workflow relinquished control over the original thread.
The other way to use the WorkflowInvoker
asynchronous functionality is to the use the BeginInvoke
method. This method, along with the EndInvoke
method, follows the standard .NET asynchronous pattern. The begin method returns an IAsyncResult
object that must be passed to the end method to complete the processing.
To see this in action, modify the Program.cs
once more. Here is the revised code:
namespace InvokerHost
{
using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;
using ActivityLibrary;
class Program
{
private static AutoResetEvent waitEvent = new AutoResetEvent(false);
static void Main(string[] args)
{
Console.WriteLine("Host: About to run workflow - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
try
{
This code is similar to the example that used the InvokeAsync
method. One difference is that the InvokeCompleted
event is no longer used. Instead, the output argument is retrieved by the call to EndInvoke
.
The call to BeginInvoke
specifies that the private BeginInvokeCallback
method should be invoked when the workflow completes. The WorkflowInvoker
instance is passed as the asynchronous state parameter to the BeginInvoke
method.
WorkflowInvoker instance = new WorkflowInvoker(
new HostingDemoWorkflow { ArgNumberToEcho = 1001 });
IAsyncResult ar = instance.BeginInvoke(
BeginInvokeCallback, instance);
Console.WriteLine(
"Host: Workflow started - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
waitEvent.WaitOne();
}
catch (Exception exception)
{
Console.WriteLine("Host: Workflow exception:{0}:{1}",
exception.GetType(), exception.Message);
}
}
/// <summary>
/// Callback when BeginInvoke is used
/// </summary>
/// <param name="ar"></param>
private static void BeginInvokeCallback(IAsyncResult ar)
{
To complete the asynchronous process, the code calls the EndInvoke
method, passing it the IAsyncResult
object that was passed to the callback method. The EndInvoke
method returns the dictionary of output arguments and completes the asynchronous pattern. This callback method also sets the AutoResetEvent
in order to release the host thread from its wait.
IDictionary<String, Object> output =
((WorkflowInvoker)ar.AsyncState).EndInvoke(ar);
Console.WriteLine(
"Host: BeginInvokeCallback Invoked - Thread:{0} - {1}",
System.Threading.Thread.CurrentThread.ManagedThreadId,
output["Result"]);
waitEvent.Set();
}
}
}
The results are similar to the example that used the InvokeAsync
method:
Host: About to run workflow - Thread:1
Workflow: Started - Thread:1
Workflow: About to delay
Host: Workflow started - Thread:1
Workflow: Continue after delay
Workflow: Completed - Thread:4
Host: BeginInvokeCallback Invoked - Thread:4 - Result is 1001
WF also provides a WorkflowApplication
class that you can use to invoke workflows. The WorkflowInvoker
class deliberately limits its functionality in order to provide a simple way to execute workflows and activities. In contrast with this, the WorkflowApplication
class eliminates all self-imposed limits, but at the cost of a great deal more complexity. If there is a scenario that WF supports, it is supported by the WorkflowApplication
class.
The most prominent feature that distinguishes the WorkflowApplication
class from WorkflowInvoker
is its use of a separate thread for workflow execution. Because the workflow executes asynchronously, the WorkflowApplication
class supports additional members that notify the host application of changes in the execution state of the workflow (idled, completed, unloaded, and so on). The host application may also need to use some type of thread synchronization object if it is required to wait until the workflow completes before processing can continue.
Note There is really only one scenario that is not supported by the WorkflowApplication
class: You can’t use it to host WCF workflow services—that’s a job for the WorkflowServiceHost
class, which is covered in Chapter 9.
The question remains, why use WorkflowApplication
if using it requires additional complexity? The answer is that it enables scenarios that are not available with WorkflowInvoker
.
For example, long-running workflows require the asynchronous execution, persistence, bookmark management, and instance control methods that are supported by the WorkflowApplication
class. The instance control methods enable you to take direct control over a workflow instance by canceling, aborting, terminating, or unloading it. Communication between the host application and a workflow instance can be accomplished using the bookmark resumption methods that are supported by the WorkflowApplication
class. And custom workflow extensions make additional functionality available to workflow instances.
When hosting workflows using the WorkflowApplication
class, you can generally follow these steps. Not all the steps will be necessary for all workflows that you host.
- Construct a
WorkflowApplication
instance.- Assign code to the delegate members that you want to handle.
- Add workflow extensions.
- Configure persistence.
- Start execution of the workflow instance.
- Resume bookmarks as necessary.
- Manually control the workflow instance as necessary.
In the sections that follow, I describe the most important members that are supported by the WorkflowApplication
class. This is not an exhaustive list of all members of the class.
The vast majority of the members for this class are instance members. Therefore, you will almost always start by creating an instance of the WorkflowApplication
class. You create a new instance by passing the constructor an instance of the workflow that you want to execute. You can optionally use a version of the constructor that also accepts a dictionary of input arguments.
A number of notification delegate members are provided by the WorkflowApplication
class. At first glance, these members look and feel like C# events, but they really aren’t. Instead, they are defined using the Action<>
or Func<>
general-purpose generic delegates. Any code that you assign to one of these members is executed by WorkflowApplication
at the appropriate time in the life cycle of the workflow instance. For example, the Completed
delegate is executed when the workflow has completed, the Idle
delegate is executed when the workflow becomes idle, and so on.
Since they are defined as delegates, you can choose to assign code to them in a number of ways. You can create and assign an anonymous method, define a Lambda expression, or even reference an old-fashioned, named method.
You may wonder why delegates are used instead of C# events. One reason is that some of these members require that you return a result from the handling code. An example of this is the OnUnhandledException
member, which requires that you return an UnhandledExceptionAction
from the handler. If a multicast C# event was used instead, there might be multiple handlers assigned to the event. It would then become difficult to determine which return value to use and which one to ignore. Also, C# events always pass the sender of the event as the first parameter. In this case, the sender would be the WorkflowApplication
instance. However, there are no valid operations that you can perform on the WorkflowApplication
instance while you are handling one of these events. So, the inclusion of the sender parameter doesn’t make sense.
Here is a summary of the most important delegates supported by the WorkflowApplication
class:
Note Chapter 13 covers error and exception handling. In particular, additional information on the use of the OnUnhandledException
member is provided in that chapter.
Workflow extensions are ordinary C# classes that you implement to provide needed functionality to workflow instances. Exactly what functionality they provide is completely up to you. Custom activities can be developed that retrieve and use an extension.
Note Chapter 8 discusses the use of workflow extensions for two-way communication between the host application and a workflow instance.
If used, an extension must be added to the WorkflowApplication
prior to starting the workflow instance (prior to the Run
method). Here are the most important WorkflowApplication
members that are related to extension management:
Several of the members of the WorkflowApplication
class are related to workflow persistence. Here are the most important members that fall into this category:
Note Chapter 11 covers workflow persistence.
After you have constructed a WorkflowApplication
instance, assigned code to any notification delegates that interest you, added any workflow extensions, and configured persistence, you need a way to actually execute the workflow instance. That’s the job of the Run
method, or the BeginRun
and EndRun
pair of methods that use the .NET asynchronous pattern. Here is a summary of these methods:
Warning The WorkflowApplication
class supports an overload of the Run
method that allows you to specify a TimeSpan
value. You might initially think that this value is used in the same way as the TimeSpan
that is passed to the WorkflowInvoker.Invoke
method. It isn’t. The TimeSpan
passed to the WorkflowInvoker.Invoke
method determines the maximum elapsed time that you want to allow for the execution of the workflow. The TimeSpan
passed to WorkflowApplication.Run
determines the maximum time allowed for the Run
method to complete—not for the entire workflow to complete. Remember that Run
simply starts execution of the workflow instance on another thread. It is possible that the Run
method may take longer than expected because of contention with another operation that is occurring on the same workflow runtime thread (for example, a persistence operation). This should be a highly unlikely scenario when you are first starting a workflow instance, but it is possible if you are calling Run
to resume execution of an instance.
The WorkflowApplication
class uses a SynchronizationContext
instance for scheduling workflow execution. By default, the synchronization context that is used executes workflow instances asynchronously on a thread pool thread. In most situations, this is the desired behavior. However, you can optionally set the SynchronizationContext
property prior to running the workflow instance if you need to use a different context. For example, if you are executing workflows in a WPF application, you could utilize the context that schedules work on the WPF user interface thread (DispatcherSynchronizationContext
). WinForms also provides a synchronization context that is designed to execute work on the user interface thread. If you are really ambitious (or have special workflow scheduling needs), you can even implement your own SynchronizationContext
.
Be aware that using a nondefault synchronization context potentially has a significant impact on the workflow runtime. For example, if you use the synchronization context that uses the WPF user interface thread, all workflow instances will be synchronously executed on that thread. The default WorkflowApplication
behavior of asynchronously executing workflows on a thread pool thread will be completely replaced.
A bookmark represents a resumption point within a workflow instance. You can create a custom activity that creates a new bookmark to indicate that it is waiting for input. When this occurs, the activity relinquishes control of the workflow thread since it is waiting for some external stimulus. If no other work has been scheduled within the workflow, the entire workflow instance becomes idle—waiting for input. The WorkflowApplication
class provides several methods that enable you to resume a bookmark and pass any data that the workflow requires to continue processing.
Here are the most important members related to bookmark processing:
Note Chapter 8 discusses the use of bookmarks for host-to-workflow communication.
The final category of WorkflowApplication
members allows you to manually control a workflow instance. Here are the most important members that fall into this category:
The differences between Cancel
, Terminate
, and Abort
can sometimes be confusing. All of these methods have the same net result of stopping the execution of a workflow instance. But that’s where the similarities really end.
- Canceling a workflow is the most graceful way to stop execution of an instance. It triggers any optional cancellation and compensation logic that you have declared within the workflow model, and it leaves the workflow in the canceled state. A canceled workflow cannot be resumed.
- Terminating a workflow does not trigger cancelation and compensation logic. The workflow instance is left in the faulted state and cannot be resumed. When a workflow instance has been terminated, any code that you have assigned to the
Completed
delegate member is executed.- Aborting a workflow is an ungraceful and immediate teardown of the workflow instance. However, if an aborted workflow has been previously persisted, it can be loaded and resumed from its last persistence point. The aborting of the workflow simply throws away everything since the last persistence point.
Which option should you use to stop execution of a workflow? That depends on your reason for stopping the workflow. First and foremost, do you plan on resuming the workflow later, reloading it from its last persistence point? If so, you should call the Abort
method since it is the only option that allows the workflow to be reloaded and restarted. You would use this method when there was a recoverable problem during the execution of the workflow. After aborting the workflow, you can correct the issue and resume execution. On the other hand, if you simply want to stop execution of the workflow and have no plans to restart it later, you can call the Cancel
or Terminate
method. Call Cancel
if you have cancellation or compensation logic that you want to execute. Call Terminate
if you want to more quickly stop execution without running any cancellation or compensation logic.
Warning The instance control methods such as Cancel
, Terminate
, and Unload
cannot be invoked from within code that is assigned to one of the notification delegates. For example, it is not permissible to call Cancel
from code assigned to the Idle
member. If you do, an InvalidOperationException
will be thrown.
In the examples that follow, you will execute the same HostingDemoWorkflow
that you declared and used earlier in the chapter. However, you will use the WorkflowApplication
class to execute the workflow instead of the WorkflowInvoker
class.
Create a new Workflow Console Application, and name it ApplicationHost
. Add the new project to the solution that was created for this chapter, and delete the Workflow1.xaml
file. Add a project reference to the ActivityLibrary
project in the same solution.
Revise the Program.cs
file with this code to host the workflow:
namespace ApplicationHost
{
using System;
using System.Activities;
using System.Collections.Generic;
using System.Threading;
using ActivityLibrary;
class Program
{
static void Main(string[] args)
{
AutoResetEvent waitEvent = new AutoResetEvent(false);
An instance of the workflow to execute is first constructed and passed to the WorkflowApplication
constructor. Any input arguments are also passed to the constructor in a dictionary. Optionally, you can set the input arguments directly using the generated properties of the workflow type.
WorkflowApplication wfApp = new WorkflowApplication(
new HostingDemoWorkflow(),
new Dictionary<String, Object>
{
{"ArgNumberToEcho", 1001},
});
Once a WorkflowApplication
instance has been constructed, code is assigned to the status notification delegates. The Completed
member is important since this is the mechanism by which the host application is notified of the completion of the workflow instance.
Notice that the code uses the CompletionState
property of the event arguments to determine the final outcome of the workflow instance. This is necessary since the Completed
member is executed for several different completion states, not only for a successful completion. The Closed
completion state means that the workflow completed normally and the output arguments are available for retrieval.
The AutoResetEvent
is set to a signaled state regardless of the completion state of the workflow. This releases the host application from waiting for the workflow instance to complete.
Not all of these completion states or delegate members are required for this first example. However, several of them will be used during subsequent examples.
wfApp.Completed = delegate(WorkflowApplicationCompletedEventArgs e)
{
switch (e.CompletionState)
{
case ActivityInstanceState.Closed:
Console.WriteLine("Host: {0} Closed - Thread:{1} - {2}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId,
e.Outputs["Result"]);
break;
case ActivityInstanceState.Canceled:
Console.WriteLine("Host: {0} Canceled - Thread:{1}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId);
break;
case ActivityInstanceState.Executing:
Console.WriteLine("Host: {0} Executing - Thread:{1}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId);
break;
case ActivityInstanceState.Faulted:
Console.WriteLine(
"Host: {0} Faulted - Thread:{1} - {2}:{3}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId,
e.TerminationException.GetType(),
e.TerminationException.Message);
break;
default:
break;
}
waitEvent.Set();
};
The code assigned to the OnUnhandledException
member is executed when an unhandled exception is thrown during workflow execution. The code for the Aborted
member is executed if the workflow instance is manually aborted.
wfApp.OnUnhandledException =
delegate(WorkflowApplicationUnhandledExceptionEventArgs e)
{
Console.WriteLine(
"Host: {0} OnUnhandledException - Thread:{1} - {2}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId,
e.UnhandledException.Message);
waitEvent.Set();
return UnhandledExceptionAction.Cancel;
};
wfApp.Aborted = delegate(WorkflowApplicationAbortedEventArgs e)
{
Console.WriteLine("Host: {0} Aborted - Thread:{1} - {2}:{3}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId,
e.Reason.GetType(), e.Reason.Message);
waitEvent.Set();
};
wfApp.Idle = delegate(WorkflowApplicationIdleEventArgs e)
{
Console.WriteLine("Host: {0} Idle - Thread:{1}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId);
};
The code assigned to the PersistableIdle
delegate is never executed for any of the examples in this chapter but is included for completeness. This delegate is used only when the WorkflowApplication
instance has been configured to use workflow persistence.
wfApp.PersistableIdle = delegate(WorkflowApplicationIdleEventArgs e)
{
Console.WriteLine("Host: {0} PersistableIdle - Thread:{1}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId);
return PersistableIdleAction.Unload;
};
wfApp.Unloaded = delegate(WorkflowApplicationEventArgs e)
{
Console.WriteLine("Host: {0} Unloaded - Thread:{1}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId);
};
After preparing the WorkflowApplication
instance, execution of the workflow begins with a call to the Run
method. Since the workflow execution takes place on a separate thread, the host uses the AutoResetEvent
to suspend the primary thread until the workflow has completed.
try
{
Console.WriteLine("Host: About to run {0} - Thread:{1}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId);
wfApp.Run();
waitEvent.WaitOne();
}
catch (Exception exception)
{
Console.WriteLine("Host: {0} exception:{1}:{2}",
wfApp.Id, exception.GetType(), exception.Message);
}
}
}
}
At this point you should build the solution and be able to run the ApplicationHost
project. Here are my results:
Host: About to run 5dc6752a-70b1-4d32-a1b1-3303428041df - Thread:1
Workflow: Started - Thread:3
Workflow: About to delay
Host: 5dc6752a-70b1-4d32-a1b1-3303428041df Idle - Thread:3
Workflow: Continue after delay
Workflow: Completed - Thread:3
Host: 5dc6752a-70b1-4d32-a1b1-3303428041df Closed - Thread:3 - Result is 1001
Host: 5dc6752a-70b1-4d32-a1b1-3303428041df Unloaded - Thread:3
Notice that right from the very start, the workflow instance reports a different managed thread ID. This confirms that a separate thread is being used for workflow execution. After the short delay, the same workflow thread is used to resume and complete the workflow.
Note In this example, the same workflow thread was used to continue execution of the workflow after the short delay. However, use of the same thread is not guaranteed. In this example, the same thread pool thread just happened to be available and was used.
Also notice that the host receives notification when the workflow was Idle, Closed, and Unloaded. These notifications were executed on the workflow thread, not the host thread.
And finally, notice that the workflow instance is uniquely identified with a Guid
. This is not critically important for this example where only a single workflow instance is executed. However, if your host application is managing multiple workflow instances, this instance ID is the primary means by which one instance is distinguished from another.
If you need to cancel an executing workflow instance, you can invoke the Cancel
method. To demonstrate this, make the minor changes to the Program.cs
file in the ApplicationHost
project shown here in bold:
namespace ApplicationHost
{
…
class Program
{
static void Main(string[] args)
{
…
try
{
…
wfApp.Run();
//Wait just a bit then cancel the workflow
Thread.Sleep(1000);
wfApp.Cancel();
waitEvent.WaitOne();
}
…
}
}
}
This test starts execution of the workflow and then waits one second before canceling the instance. Here are my results when I run this test:
Host: About to run fc749e0f-f3e0-4435-95cd-ce7e56d4470a - Thread:1
Workflow: Started - Thread:4
Workflow: About to delay
Host: fc749e0f-f3e0-4435-95cd-ce7e56d4470a Idle - Thread:4
Host: fc749e0f-f3e0-4435-95cd-ce7e56d4470a Canceled - Thread:4
Host: fc749e0f-f3e0-4435-95cd-ce7e56d4470a Unloaded - Thread:4
Your only notification that the workflow was canceled comes from code assigned to the Completed
delegate. When an instance is canceled, the CompletionState
is equal to ActivityInstanceState.Canceled
.
To abort a workflow instance, you invoke the Abort
method like this:
namespace ApplicationHost
{
…
class Program
{
static void Main(string[] args)
{
…
try
{
…
wfApp.Run();
//Wait just a bit then abort the workflow
Thread.Sleep(1000);
wfApp.Abort("My aborted reason");
waitEvent.WaitOne();
}
…
}
}
}
Now when I run the ApplicationHost
project, I see these results:
Host: About to run 977d3ec8-4d42-488c-b6fb-014925d0ee43 - Thread:1
Workflow: Started - Thread:4
Workflow: About to delay
Host: 977d3ec8-4d42-488c-b6fb-014925d0ee43 Idle - Thread:4
Host: 977d3ec8-4d42-488c-b6fb-014925d0ee43 Aborted - Thread:4 –
System.Activities.WorkflowApplicationAbortedException:My aborted reason
This time the code assigned to the Aborted
notification delegate is executed instead of the Completed
member. The exception that is thrown contains the abort reason that was provided to the Abort
method. Since the workflow was aborted, you no longer receive the Unloaded
notification.
The final way to stop execution of a workflow instance is to terminate it. As you might have already guessed, you accomplish this by calling the Terminate
method like this:
namespace ApplicationHost
{
…
class Program
{
static void Main(string[] args)
{
…
try
{
…
wfApp.Run();
//Wait just a bit then terminate the workflow
Thread.Sleep(1000);
wfApp.Terminate("My termination reason");
waitEvent.WaitOne();
}
…
}
}
}
The results when I run the ApplicationHost
project now look like this:
Host: About to run b7c2e287-0234-4bf2-b68b-d43dcfb6f286 - Thread:1
Workflow: Started - Thread:4
Workflow: About to delay
Host: b7c2e287-0234-4bf2-b68b-d43dcfb6f286 Idle - Thread:4
Host: b7c2e287-0234-4bf2-b68b-d43dcfb6f286 Faulted - Thread:1 –
System.Activities.WorkflowApplicationTerminatedException:My termination reason
Host: b7c2e287-0234-4bf2-b68b-d43dcfb6f286 Unloaded - Thread:1
When you call Terminate
on an instance, the code assigned to the Completed
delegate is executed. The CompletionState
is set to a value of ActivityInstanceState.Faulted
to indicate that the workflow instance was terminated. The termination reason that you passed to the Terminate
method is used as the exception message.
The WorkflowApplication
also supports the .NET asynchronous pattern with the BeginRun
and EndRun
methods. Since the WorkflowApplication
is designed to execute workflows asynchronously on another thread, I think the usefulness of this pattern for this class is limited. However, it is available if you want to asynchronously begin the process of running the workflow (which will then run asynchronously).
To demonstrate the BeginRun
method, modify the Program.cs
file in the ApplicationHost
project as shown here:
namespace ApplicationHost
{
…
class Program
{
static void Main(string[] args)
{
…
try
{
…
wfApp.BeginRun(delegate(IAsyncResult ar)
{
Console.WriteLine(
"Host: {0} BeginRunCallback - Thread:{1}",
wfApp.Id,
System.Threading.Thread.CurrentThread.ManagedThreadId);
((WorkflowApplication)ar.AsyncState).EndRun(ar);
}, wfApp);
waitEvent.WaitOne();
}
…
}
}
}
The call to this overload of the BeginRun
method takes two parameters. The first is a method that implements the AsyncCallback
delegate, and the second is an asynchronous state object. In this example, an anonymous method is used for the callback, and the WorkflowApplication
instance itself is passed as the asynchronous state object.
The callback invokes the EndRun
method of the WorkflowApplication
instance, passing the IAsyncResult
object that was passed to the callback.
Here are my results when I executed the ApplicationHost
project with this revised code:
Host: About to run b9b63d47-b5a8-4430-a680-b171dbad4547 - Thread:1
Workflow: Started - Thread:4
Workflow: About to delay
Host: b9b63d47-b5a8-4430-a680-b171dbad4547 Idle - Thread:4
Host: b9b63d47-b5a8-4430-a680-b171dbad4547 BeginRunCallback - Thread:1
Workflow: Continue after delay
Workflow: Completed - Thread:3
Host: b9b63d47-b5a8-4430-a680-b171dbad4547 Closed - Thread:3 - Result is 1001
Host: b9b63d47-b5a8-4430-a680-b171dbad4547 Unloaded - Thread:3
One of the most significant benefits of using Xaml to declare workflows is the portability and flexibility that it affords. For the most part, the workflows that you create with Visual Studio are saved as Xaml documents. The build process then compiles these files into managed types that are constructed and referenced just like any other .NET CLR type.
These compiled workflows are fine for many applications. However, since the workflow definitions are baked into a .NET assembly, they are no longer easily modified and used. If a change is necessary, you need to fire up Visual Studio, make the necessary changes to the Xaml file (probably using the Workflow designer), and then rebuild the project. The net result of that process is a revised .NET assembly that contains the compiled workflow definition.
WF also provides you with the ability to read and process a Xaml document directly, instead of using a compiled .NET type. The ActivityXamlServices
class is the key to this functionality. The static Load
method reads a Xaml document and returns a DynamicActivity
object that represents the workflow. The DynamicActivity
class derives from the base Activity
class and is designed for situations such as this where a new activity instance is dynamically created. The activity instance is then passed to the WorkflowInvoker
or WorkflowApplication
class just like a normally compiled workflow or activity. There are several overloaded versions of the Load
method that provide some flexibility in how the Xaml document is read. You can pass a Stream
, TextReader
, XmlReader
, XamlReader
, or simply the path to a Xaml file.
What this functionality provides is flexibility and portability. The workflow declaration can be maintained outside of Visual Studio (you can self-host the workflow designer) and persisted in a way that makes sense for your application. For example, you can persist Xaml documents in a database, in SharePoint, or as a file somewhere in the file system.
To demonstrate the use of the ActivityXamlServices
class, you will create a new application that loads and executes the HostingDemoWorkflow
that you developed earlier in the chapter. However, unlike the other examples in this chapter, the workflow declaration will be loaded directly from the Xaml file instead of constructing an instance of the HostingDemoWorkflow
compiled type.
Create a new Workflow Console Application, and name it InvokerHostXaml
. Add the new project to the solution that was created for this chapter, and delete the Workflow1.xaml
file. Unlike previous examples, you do not need to add a reference to any other projects in the solution. In particular, you don’t need a reference to the ActivityLibrary
project since you won’t be referencing the compiled version of the workflow.
Here is the completed code that you need for the Program.cs
in this new project:
namespace InvokerHostXaml
{
using System;
using System.Activities;
using System.Activities.XamlIntegration;
using System.Collections.Generic;
class Program
{
static void Main(string[] args)
{
Console.WriteLine("
>>>> From Xaml <<<<");
RunFromXaml();
}
private static void RunFromXaml()
{
Console.WriteLine("Host: About to run workflow - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
try
{
I’m using a relative path from this new project to the ActivityLibrary
project where the Xaml file should be located. This assumes that both of these projects are in the same solution and that you’ve used the recommended name for the project containing the Xaml file (ActivityLibrary
). If you used a different project name, you’ll need to revise this file path.
There is no magic to loading the Xaml file from the ActivityLibrary
project. I’m simply doing that because I happen to know that the file exists at that location and to eliminate the need to make a copy of the Xaml file. If you prefer, you can copy the Xaml file into the indebug
directory of the new InvokerHostXaml
project and load it from that location.
String fullFilePath =
@"......ActivityLibraryHostingDemoWorkflow.xaml";
Activity activity = ActivityXamlServices.Load(fullFilePath);
//activity is a DynamicActivity
if (activity == null)
{
throw new NullReferenceException(String.Format(
"Unable to deserialize {0}", fullFilePath));
}
The result of loading the Xaml file is an Activity
object. After verifying that the Xaml file was deserialized correctly, this object is passed to the WorkflowInvoker
class in the same manner as the examples earlier in this chapter. Please note that there is no requirement to use the WorkflowInvoker
class. You could have also used the WorkflowApplication
class to execute the workflow.
IDictionary<String, Object> output = WorkflowInvoker.Invoke(
activity, new Dictionary<String, Object>
{
{"ArgNumberToEcho", 1001},
});
Console.WriteLine("Host: Workflow completed - Thread:{0} - {1}",
System.Threading.Thread.CurrentThread.ManagedThreadId,
output["Result"]);
}
catch (Exception exception)
{
Console.WriteLine("Host: Workflow exception:{0}:{1}",
exception.GetType(), exception.Message);
}
}
}
}
After building the solution, you should be able to run the InvokerHostXaml
project. Here are my results, which are consistent with previous examples in this chapter:
Host: About to run workflow - Thread:1
Workflow: Started - Thread:1
Workflow: About to delay
Workflow: Continue after delay
Workflow: Completed - Thread:1
Host: Workflow completed - Thread:1 - Result is 1001
WF provides two options when you need to invoke a workflow from an ASP.NET web application:
- You can invoke a WCF workflow service from the ASP.NET application.
- You can host the workflow directly within the ASP.NET application.
When deciding which mechanism to use, the default answer should be to invoke a separately hosted workflow service via WCF. Doing so enforces a clear separation between the web tier containing the presentation logic and the business tier that is implemented as workflow services. And independently hosted workflow services provide more opportunities for scalability, load balancing, and overall management of the runtime environment. Workflow services can also use additional WF features such as persistence that are important for long-running workflows.
However, you do have the option of directly invoking a workflow from an ASP.NET application. You normally wouldn’t choose this option to execute the bulk of your workflow business logic. You should generally limit the use of this approach to short-lived workflows—the kind that you would normally execute just like a C# method. Since you need to execute the workflow on the ASP.NET thread, you are generally limited to using the WorkflowInvoker
class. And this class doesn’t support persistence and isn’t designed for the management of long-running workflows.
The short example that follows demonstrates how easy it can be to invoke a workflow directly from within an ASP.NET application.
Note Chapter 9 covers the use of WCF workflow services.
In this example, you will construct a very simple ASP.NET application that invokes the same HostingDemoWorkflow
that has been used throughout this chapter.
To begin this example, create a new web application using the Empty ASP.NET Web Application template. Name the application WebInvokerHost
, and add it to the existing solution for this chapter.
Add a new Web Form to the project. You can use the default name of WebForm1.aspx
. Add the following web controls to the form:
Here is the completed markup for the WebForm1.aspx
page:
<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="WebForm1.aspx.cs"
Inherits="WebInvokerHost.WebForm1" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<title></title>
</head>
<body>
<form id="form1" runat="server">
<div>
<asp:Label ID="Label1" runat="server"
Text="Enter Test Number:"></asp:Label>
<br />
<asp:TextBox ID="TextBox1" runat="server"
Width="84px">1001</asp:TextBox>
<br />
<asp:Button ID="Button1" runat="server"
OnClick="Button1_Click" Text="Run Workflow" />
<br />
<asp:Label ID="Label2" runat="server"></asp:Label>
<br />
<br />
<asp:TextBox ID="TextBox2" runat="server" Height="128px"
ReadOnly="True" TextMode="MultiLine"
Width="456px" Wrap="False"></asp:TextBox>
<br />
<br />
</div>
</form>
</body>
</html>
TextBox1
is used to enter the test number that is echoed back from the workflow as the result string. Button1
triggers execution of the workflow and requires an event handler for the OnClick
event. You can add an event handler by double-clicking the control in the designer. Label2
displays the string result. TextBox2
is a read-only, multiline control that displays all the logging messages that are produced by the hosting code and the workflow.
Figure 4-2 shows how I’ve positioned these controls. Feel free to improve on my meager efforts at user interface design.
Before you can add the code needed to host the workflow, you need to add two references to the project. Add a project reference to the ActivityLibrary
project that should be in the same solution. Also add a .NET reference to System.Activities
.
Open WebForm1
in Code View to enter the code necessary to execute the workflow. For simplicity, I’ve placed all the code in the button event handler. Here is the WebForm1.aspx.cs
file containing all the code that you need for this example:
using System;
using System.Activities;
using System.Collections.Generic;
using System.IO;
using ActivityLibrary;
namespace WebInvokerHost
{
public partial class WebForm1 : System.Web.UI.Page
{
protected void Button1_Click(object sender, EventArgs e)
{
Int32 testNumber = 0;
if (Int32.TryParse(TextBox1.Text, out testNumber))
{
After retrieving and parsing the test number, the code creates a StringWriter
. This is passed as the ArgTextWriter
input argument to the workflow. If you recall, the workflow was originally declared to assign the TextWriter
property of each WriteLine
activity to this argument. Throughout the earlier examples in this chapter, this argument was not supplied and was therefore always null.
However, in this example, you want to display these messages on the web page instead of writing them to the console. Since this code supplies a value for this argument, all WriteLine
messages in the workflow will be written to the StringWriter
instead of the console. All messages in this hosting code also write to this same StringWriter
.
StringWriter writer = new StringWriter();
try
{
writer.WriteLine("Host: About to run workflow - Thread:{0}",
System.Threading.Thread.CurrentThread.ManagedThreadId);
The actual code to execute the workflow looks similar to the previous examples in this chapter. The WorkflowInvoker
class is used in order to execute the workflow synchronously using the current ASP.NET thread.
HostingDemoWorkflow wf = new HostingDemoWorkflow();
IDictionary<String, Object> output =
WorkflowInvoker.Invoke(wf, new Dictionary<String, Object>
{
{"ArgNumberToEcho", 1001},
{"ArgTextWriter", writer},
});
Label2.Text = (String)output["Result"];
writer.WriteLine(
"Host: Workflow completed - Thread:{0} - {1}",
System.Threading.Thread.CurrentThread.ManagedThreadId,
output["Result"]);
}
catch (Exception exception)
{
writer.WriteLine("Host: Workflow exception:{0}:{1}",
exception.GetType(), exception.Message);
}
//dump the contents of the writer
TextBox2.Text = writer.ToString();
}
}
}
}
Before you test the application, you should set WebForm1.aspx
as the startup page to simplify the testing. To accomplish this, select WebForm1.aspx
in the Solution Explorer, right-click, and choose Set as Start Page.
You should now be ready to build the solution and run the web application. When you run it without debugging (Ctrl-F5), the ASP.NET development server will start in order to host the application. Your default browser should also start and present the default page (WebForm1.aspx
) for your viewing pleasure. Figure 4-3 shows the page on my system after I entered a new test number and clicked the Run Workflow button.
Since the WorkflowInvoker
class was used to execute the workflow, the same managed thread ID is shown for the hosting code as well as the workflow instance.
As you have seen, WF supports several flexible ways to execute workflows. Because of this flexibility, you have the ability to incorporate workflows into many different application types.
But WF does not include an out-of-the-box manager for multiple workflow instances. If your application requires the ability to start and manage multiple instances of a workflow, you need to develop that code yourself.
Tip WF does provide the WorkflowServiceHost
class, which allows you to manage multiple WCF-based workflow instances. However, those instances can be started only via WCF. You can find more information on the WorkflowServiceHost
class in Chapter 9.
In the example that follows, you will develop a simple Windows Presentation Foundation application that hosts multiple instances of the HostingDemoWorkflow
. The user interface allows you to start new instances of the workflow and to monitor their current status.
To begin this example, create a new project using the WPF Application project template. You can find this template under the Windows category. Name this project WpfHost
, and add it to the solution for this chapter.
Add a project reference to the ActivityLibrary
project in the same solution. Also add a .NET reference to System.Activities
.
Before you turn your attention to the WPF application, you will implement a class that handles some of the dirty work of managing multiple workflow instances. This code could have just as easily been added directly to the MainWindow
class of the WPF application. But my preference is to always move nonvisual state management code such as this to its own class.
Note This workflow manager class is tailored to the needs of this particular application and is designed to demonstrate one possible way to manage multiple workflow instances. It is likely that it won’t necessarily meet the exact needs of your particular application. However, it should be helpful as a starting point when you need to develop your own workflow applications.
Add a new C# class to the WpfHost
project, and name it WorkflowManager
. This is a normal C# class, not a workflow-related class. This class will track the state of multiple workflow instances for the application. Here is the complete code for the WorkflowManager.cs
file:
using System;
using System.Activities;
using System.Collections.Generic;
namespace WpfHost
{
public class WorkflowManager
{
This class uses a private dictionary of WorkflowApplication
instances to track each workflow instance. The key to the dictionary is the workflow instance ID, which is a Guid
.
private Dictionary<Guid, WorkflowApplication> _wfApps
= new Dictionary<Guid, WorkflowApplication>();
A generic Run
method is designed to start a workflow instance using the parameters that have been passed to the method. The generic type identifies the workflow type to create and execute.
After creating an instance of the requested workflow type, a WorkflowApplication
is created and added to the private dictionary of workflow instances. Handlers for the most important members (Completed
, Idle
, Aborted
, and OnUnhandledException
) are then added. Finally, the workflow instance is started with a call to the WorkflowApplication.Run
method.
public Guid Run<T>(IDictionary<String, Object> parameters)
where T : Activity, new()
{
Guid id = Guid.Empty;
T activity = new T();
WorkflowApplication wfApp = null;
if (parameters != null)
{
wfApp = new WorkflowApplication(activity, parameters);
}
else
{
wfApp = new WorkflowApplication(activity);
}
id = wfApp.Id;
_wfApps.Add(wfApp.Id, wfApp);
wfApp.Completed = AppCompleted;
wfApp.Idle = AppIdle;
wfApp.Aborted = AppAborted;
wfApp.OnUnhandledException = AppException;
if (Started != null)
{
Started(id, parameters);
}
wfApp.Run();
return id;
}
A set of public delegates is supported by this class. Each one is used to notify the host application of a state change for a particular workflow instance.
public Action<Guid, IDictionary<string, object>> Started { get; set; }
public Action<Guid, IDictionary<string, object>> Completed { get; set; }
public Action<Guid> Idle { get; set; }
public Action<Guid, String, String> Incomplete { get; set; }
public Int32 GetActiveCount()
{
lock (_wfApps)
{
return _wfApps.Count;
}
}
The code to handle the various WorkflowApplication
delegate members is shown next. The AppCompleted
method handles the Completed
calls from each WorkflowApplication
instance. The code notifies the host application of the new state and removes the instance from the private dictionary.
private void AppCompleted(WorkflowApplicationCompletedEventArgs e)
{
switch (e.CompletionState)
{
case ActivityInstanceState.Closed:
if (Completed != null)
{
Completed(e.InstanceId, e.Outputs);
}
RemoveInstance(e.InstanceId);
break;
case ActivityInstanceState.Canceled:
if (Incomplete != null)
{
Incomplete(e.InstanceId, "Canceled",
String.Empty);
}
RemoveInstance(e.InstanceId);
break;
case ActivityInstanceState.Faulted:
if (Incomplete != null)
{
Incomplete(e.InstanceId, "Faulted",
e.TerminationException.Message);
}
RemoveInstance(e.InstanceId);
break;
default:
break;
}
}
The code that handles the Idle
and Aborted
notifications from each WorkflowApplication
instance notifies the host application. In the case of the Aborted
notification, the instance is removed from the dictionary.
private void AppIdle(WorkflowApplicationIdleEventArgs e)
{
if (Idle != null)
{
Idle(e.InstanceId);
}
}
private void AppAborted(WorkflowApplicationAbortedEventArgs e)
{
if (Incomplete != null)
{
Incomplete(e.InstanceId, "Aborted", e.Reason.Message);
}
RemoveInstance(e.InstanceId);
}
Unhandled exceptions are managed in a similar way as the other notifications. The host application is notified of the problem, and the instance is removed from the dictionary.
private UnhandledExceptionAction AppException(
WorkflowApplicationUnhandledExceptionEventArgs e)
{
if (Incomplete != null)
{
Incomplete(e.InstanceId, "Exception",
e.UnhandledException.Message);
}
RemoveInstance(e.InstanceId);
return UnhandledExceptionAction.Cancel;
}
A private RemoveInstance
method is used by the other methods to remove a WorkflowApplication
instance from the private dictionary.
private void RemoveInstance(Guid id)
{
lock (_wfApps)
{
if (_wfApps.ContainsKey(id))
{
_wfApps[id].Completed = null;
_wfApps[id].Idle = null;
_wfApps[id].Aborted = null;
_wfApps[id].OnUnhandledException = null;
_wfApps.Remove(id);
}
}
}
}
}
Add a new C# class to the WpfHost
project, and name it InstanceInfo
. This is a normal C# class, not a workflow class. The purpose of this class is to define a structure that is used by the MainWindow
class to display a status line for each workflow instance. Here is the complete code for the InstanceInfo.cs
file:
using System;
namespace WpfHost
{
public class InstanceInfo
{
public Guid Id { get; set; }
public Int32 TestNumber { get; set; }
public String Result { get; set; }
public String Status { get; set; }
}
}
Next, open the MainWindow.xaml
file of the WpfHost
project in the designer. Add the following controls to the form:
Figure 4-4 shows the layout of the controls on MainForm.xaml
.
Note As long as the AutoGenerateColumns
property of the DataGrid
is enabled, the individual columns of the grid will be generated based on the data source for the grid (the ItemsSource
property). The initial column widths will be based on the first row of data.
Add Click
event handlers for the two buttons as listed in the previous table. Also add a handler for the Closing
event of the window. You can use the default name of Window_Closing
for the handler.
You can now turn your attention to the code needed to handle the user interface events and to populate the DataGrid
with status information for each workflow instance. Here is the complete code that you need for the MainWindow.xaml.cs
file:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using ActivityLibrary;
namespace WpfHost
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
The window creates a single instance of the WorkflowManager
class that you implemented in a previous step. It also uses an instance of the Random
class to generate a random test number that is passed to each workflow instance. Finally, a collection of InstanceInfo
instances is created. This collection is used to populate the entries in the DataGrid
.
private WorkflowManager _manager = new WorkflowManager();
private Random _rnd = new Random(Environment.TickCount);
private List<InstanceInfo> _instances = new List<InstanceInfo>();
public MainWindow()
{
During construction of the main window, code handlers are assigned to each of the notification delegates of the WorkflowManager
class. The collection of InstanceInfo
objects is assigned to the DataGrid.ItemsSource
property. This enables the data in the collection to be displayed on the DataGrid
.
InitializeComponent();
_manager.Started = Started;
_manager.Completed = Completed;
_manager.Idle = Idle;
_manager.Incomplete = Incomplete;
dataGrid1.ItemsSource = _instances;
}
Each time button1
is clicked, a new instance of the workflow is created and run. The actual work of preparing and starting the workflow is deferred to the WorkflowManager
class.
private void button1_Click(object sender, RoutedEventArgs e)
{
Int32 testNumber = _rnd.Next(9999);
//start a workflow instance
Guid id = _manager.Run<HostingDemoWorkflow>(
new Dictionary<String, Object>
{
{"ArgNumberToEcho", testNumber},
});
}
The code for button2
is used to clear the current list of instances from the DataGrid
.
private void button2_Click(object sender, RoutedEventArgs e)
{
_instances.Clear();
dataGrid1.Items.Refresh();
}
Each of the notification handlers works in a similar way. They create a new instance of the InstanceInfo
class and set the appropriate properties. The class is then passed to the private UpdateDisplay
method that updates the collection of instances with the new data.
Notice that the UpdateDisplay
method is invoked using the WPF dispatcher. This ensures that the user interface thread is used. This is necessary since these methods are triggered by a workflow instance state change that executes on the workflow thread, not the user interface thread. You can only update WPF controls from the user interface thread.
private void Started(Guid id, IDictionary<string, object> parameters)
{
Application.Current.Dispatcher.BeginInvoke(
new Action<InstanceInfo>(UpdateDisplay), new InstanceInfo
{
Id = id,
TestNumber = (Int32)parameters["ArgNumberToEcho"],
Result = String.Empty,
Status = "Started"
});
}
private void Completed(Guid id, IDictionary<string, object> outputs)
{
Application.Current.Dispatcher.BeginInvoke(
new Action<InstanceInfo>(UpdateDisplay), new InstanceInfo
{
Id = id,
Result = outputs["Result"] as String,
Status = "Completed"
});
}
private void Idle(Guid id)
{
Application.Current.Dispatcher.BeginInvoke(
new Action<InstanceInfo>(UpdateDisplay), new InstanceInfo
{
Id = id,
Status = "Idle"
});
}
private void Incomplete(Guid id, String reason, String message)
{
Application.Current.Dispatcher.BeginInvoke(
new Action<InstanceInfo>(UpdateDisplay), new InstanceInfo
{
Id = id,
Result = message,
Status = reason
});
}
private void UpdateDisplay(InstanceInfo info)
{
InstanceInfo currInfo = null;
currInfo =
(from i in _instances
where i.Id == info.Id
select i).SingleOrDefault();
if (currInfo != null)
{
currInfo.Status = info.Status;
currInfo.Result = info.Result;
}
else
{
_instances.Add(info);
currInfo = info;
}
dataGrid1.Items.Refresh();
dataGrid1.ScrollIntoView(currInfo);
}
The handler for the window’s Closing
event retrieves the active count of workflows from the WorkflowManager
instance. If the count is zero, then the window is allowed to close. If it is greater than zero, the user is asked whether they want to execute the application while one or more workflows are still executing.
private void Window_Closing(
object sender, System.ComponentModel.CancelEventArgs e)
{
Int32 count = _manager.GetActiveCount();
if (count != 0)
{
if (MessageBox.Show(String.Format(
"{0} workflows are still executing. Continue?", count),
"Workflow Executing",
MessageBoxButton.YesNo, MessageBoxImage.Warning)
!= MessageBoxResult.Yes)
{
e.Cancel = true;
}
}
}
}
}
After building the solution, you should be ready to run the WpfHost
application. Figure 4-5 shows a representative view of the application after I have started a few workflow instances.
As each workflow is started, the DataGrid
will be updated. The Status column for each instance always begins with “Started” but immediately changes to “Idle” as the workflow moves into that state. After a short delay, each instance then changes to a “Completed” status.
Since this example uses WPF, this is a good opportunity to demonstrate the use of the optional SynchronizationContext
property of the WorkflowApplication
class. By setting this property to the current synchronization context, the WPF context will be used for the scheduling of all workflows.
Note Setting the SynchronizationContext
isn’t something that you will normally need to do. It is necessary only in advanced threading scenarios where you want to provide a nondefault threading and execution model for the workflow runtime to use.
To see this in action, you need to make this small change to the WorkflowManager.cs
code:
using System;
using System.Activities;
using System.Collections.Generic;
namespace WpfHost
{
public class WorkflowManager
{
…
public Guid Run<T>(IDictionary<String, Object> parameters)
where T : Activity, new()
{
…
//Add this to schedule and execute all workflows on
//the WPF UI thread.
wfApp.SynchronizationContext =
System.Threading.SynchronizationContext.Current;
wfApp.Run();
return id;
}
…
}
}
Prior to running the workflow, the SynchronizationContext
property is set to the current synchronization context. Since the Run<T>
method is invoked on the WPF user interface thread, the current synchronization context is the one used by WPF (DispatcherSynchronizationContext
).
You shouldn’t see any observable difference in the results after making this change, other than the user interface may feel a bit sluggish. That’s because the workflows are now executing on the WPF user interface thread. Now that the workflows are executing on the user interface thread, there is no need for the calls to the Application.Current.Dispatcher.BeginInvoke
method. You could safely replace those calls with a direct call to the private UpdateDisplay
method.
This chapter focused on hosting and executing workflows. The WorkflowInvoker
and WorkflowApplication
classes are provided with WF and are used to execute workflows. WorkflowInvoker
is the simplest way to execute a workflow since it executes synchronously on the current thread. WorkflowApplication
provides the ability to execute workflows asynchronously on a separate thread, resume bookmarks, configure persistence, and manually control running workflows. The chapter presented a number of short examples that demonstrated the most important features of these two classes.
Workflows are normally compiled into .NET types that are instantiated before they are executed. However, WF also provides the ActivityXamlServices
class, which allows you to load a workflow instance directly from the Xaml declaration rather than a compiled type. The chapter included an example that demonstrated how to use this class.
Another example demonstrated how to invoke a workflow from within an ASP.NET Web Forms application. The final example demonstrated one way to execute and manage multiple instances of a workflow from a WPF application. An option to use the WPF synchronization context for scheduling and execution of the workflows was also presented.
In the next chapter, you will learn about the core procedural flow control activities that are provided with WF.
3.133.124.21