image

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.

images 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.

Understanding the WorkflowInvoker 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.

Using the Static Methods

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.

Using the Instance Methods

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).

images 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.

Using the WorkflowInvoker Static Methods

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:

  1. Declare the HostingDemoWorkflow.
  2. Implement the code to host the workflow.

Declaring the HostingDemoWorkflow

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:

image

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:

  1. Add a Sequence activity to the empty workflow as the root activity.
  2. Add a WriteLine activity to the Sequence activity. Set the Text property to String.Format("Workflow: Started - Thread:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId). Set the TextWriter property to ArgTextWriter.
  3. Add another WriteLine activity below the previous one. Set the Text property to "Workflow: About to delay" and the TextWriter property to ArgTextWriter.
  4. Add a Delay activity below the previous WriteLine. Set the Duration property to TimeSpan.FromSeconds(3) to provide a three-second delay.
  5. Add another WriteLine activity below the Delay. Set the Text property to "Workflow: Continue after delay" and the TextWriter property to ArgTextWriter.
  6. Add an Assign activity below the WriteLine. Set the Assign.To property to Result and the Assign.Value property to String.Format("Result is {0}", ArgNumberToEcho).
  7. Add a final WriteLine activity below the Assign activity. Set the Text property to String.Format("Workflow: Completed - Thread:{0}", System.Threading.Thread.CurrentThread.ManagedThreadId). Set the TextWriter property to ArgTextWriter.

The completed workflow should look like Figure 4-1.

images

Figure 4-1. HostingDemoWorkflow

Simple Hosting of the Workflow

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.

images 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.

Passing Arguments with Workflow Properties

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.

images 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.

Declaring a Timeout Value

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.

Invoking a Generic Activity

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.

Implementing the HostingDemoActivity

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.

Executing the Activity

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

Using the WorkflowInvoker Instance Methods

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.

Using the InvokeAsync Method

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.

Using the BeginInvoke Method

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

Understand the WorkflowApplication Class

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.

images 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.

  1. Construct a WorkflowApplication instance.
  2. Assign code to the delegate members that you want to handle.
  3. Add workflow extensions.
  4. Configure persistence.
  5. Start execution of the workflow instance.
  6. Resume bookmarks as necessary.
  7. 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.

Constructing a WorkflowApplication

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.

Assigning Code to Delegate Members

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:

image

images Note Chapter 13 covers error and exception handling. In particular, additional information on the use of the OnUnhandledException member is provided in that chapter.

Managing Extensions

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.

images 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:

image

Configuring and Managing Persistence

Several of the members of the WorkflowApplication class are related to workflow persistence. Here are the most important members that fall into this category:

image

images Note Chapter 11 covers workflow persistence.

Executing a Workflow Instance

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:

image

images 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.

Managing Bookmarks

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:

image

images Note Chapter 8 discusses the use of bookmarks for host-to-workflow communication.

Manually Controlling a Workflow Instance

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:

image

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.

images 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.

Using the WorkflowApplication Class

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.

Hosting the Workflow with WorkflowApplication

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.

images 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.

Canceling a Workflow Instance

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.

Aborting a WorkflowInstance

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 &#8211;

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.

Terminating a WorkflowInstance

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 &#8211;

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.

Using the BeginRun Method

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

Understanding the ActivityXamlServices Class

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.

Using the ActivityXamlServices Class

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

Invoke Workflows from ASP.NET

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.

images 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.

Designing the ASP.NET Application

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:

image

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.

images

Figure 4-2. WebForm1.aspx

Hosting the Workflow

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();
            }
        }
    }
}

Testing the Application

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.

images

Figure 4-3. WebInvokerHost application

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.

Managing Multiple Workflow Instances

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.

images 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.

Implementing a Workflow Manager

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.

images 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);
                }
            }
        }
    }
}

Implementing the InstanceInfo Class

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; }
    }
}

Designing the User Interface

Next, open the MainWindow.xaml file of the WpfHost project in the designer. Add the following controls to the form:

image

Figure 4-4 shows the layout of the controls on MainForm.xaml.

images

Figure 4-4. WpfHost MainWindow.xaml

images 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.

Implementing the User Interface Code

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;
                }
            }
        }
    }
}

Testing the Application

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.

images

Figure 4-5. WpfHost application

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.

Using the WPF SynchronizationContext

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.

images 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.

Summary

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.124.21