Wednesday, October 18, 2006 1:04 AM bart

WF - Handling external events with HandleExternalEvent, External Data Exchange and Local Communications Services

Introduction

In yesterday's post, you learned that the vast majority of workflows need to exchange data with other parties to get their jobs done. Just one of the benefits of workflows is the possibility to visualize this kind of interaction by means of different activities, like the CallExternalMethodActivity that was explained in the previous post. Based on a contract definition (read: interface) a workflow can be defined while the choice of the service implementation is left as a decision for the workflow host application. In today's post, we continue our journey on the Local Communication Services and External Data Exchange path with the HandleExternalEventActivity.

Scenario

The CallExternalMethodActivity explained in the previous post is used to perform methods calls from inside the workflow to some service. Based on an interface and method contract, the workflow can be defined. At runtime, an appropriate service implementation is attached to the workflow and chosen by the engine to process service requests. An example is a workflow calling to an order processing system in some way defined by the contract (e.g. PlaceOrder).

Beside making calls from the workflow to some party outside, one can also rely on the host application to notify the workflow when it needs to do something. This is done using the HandleExternalEventActivity which makes a workflow block till the corresponding event (again in a contract-based manner) is raised by some service. As an example, think of a workflow waiting for approval based on some event (e.g. OrderApproved).

A simple demo

Workflow definition

As usual on this blog, we'll try to keep things simple and approachable, to focus on the basics of the topic discussed. Create a simple Sequential Workflow Console application called EventsDemo:

Next, create the following workflow definition in Workflow1.cs:

This needs some elaboration. On top, we have some CodeActivity called start with the simple ExecuteCode event handler displayed below. At the bottom, there's a similar activity with the event handler shown below as well:

private void start_ExecuteCode(object sender, EventArgs e)
{
   Console.ForegroundColor = ConsoleColor
.Green;
   Console.WriteLine("Waiting for clients..."
);
   Console
.ResetColor();
}

private void stop_ExecuteCode(object sender, EventArgs
e)
{
   Console.ForegroundColor = ConsoleColor
.Red;
   Console.WriteLine("Served 5 clients; time for early retirement!"
);
   Console
.ResetColor();
}

Next, there's an activity called clientListener of the type WhileActivity. This one just loops till five clients have been served, based on the following Declarative Rule Condition:

this.clientCount < 5

relying on the following private variable in the code-behind:

private int clientCount = 0;

A WhileActivity can only contain one single child activity. Because of this, a SequenceActivity is added to the body of the WhileActivity. Inside this SequenceActivity, two child activities are added:

  • clientArrival is of type HandleExternalEventActivity and will be discussed below shortly
  • doWork is of type CodeActivity and has the following ExecuteCode event handler:

    private void doWork_ExecuteCode(object sender, EventArgs e)
    {
       Console.ForegroundColor = ConsoleColor
    .Yellow;
       Console.WriteLine("Event captured - Hello {0}. You're client number {1}."
    , args.Name, clientCount);
       Console
    .ResetColor();
    }

The core of the workflow is the HandleExternalEventActivity called clientArrival. This activity works in a similar way as the CallExternalMethodActivity and relies on an interface and in this case an event to wait for. The logical next step is to define this interface (IBar.cs):

using System;
using
System.Workflow.Activities;

namespace
EventsDemo
{
   [
ExternalDataExchange
]
   public interface
IBar
   {
      event EventHandler<FooEventArgs
> Foo;
   }

   [
Serializable
]
   public class FooEventArgs :
ExternalDataEventArgs
   {
      public FooEventArgs(Guid instanceId, string name) : base
(instanceId)
      {
         this
.name = name;
      }

      private string
name;

      public string
Name
      {
         get { return
name; }
         set { name = value
; }
      }
   }
}

Notice the interface definition being annotated with the ExternalDataExchangeAttribute, which is required for workflow to communicate with it using Local Communication Services. Next, the defined event has an event arguments object derived from ExternalDataEventArgs. The constructor of this event arguments object is worth to mention because it requires a base call to one of the base class's constructors that require a workflow instance identifier to be passed on:

public FooEventArgs(Guid instanceId, string name) : base(instanceId)
{

This is required for the workflow runtime to be able to correlate the event with the right workflow instance.

When you've defined the interface with the event, you can continue to set up the HandleExternalEvent activity. Start by setting the InterfaceType to the IBar interface. Next, set the EventName to Foo. In order to capture the event arguments in the workflow instance to d something useful with it in a later stage, you can bind the e parameter (à la EventArgs e) to some local variable:

private FooEventArgs args;

public FooEventArgs
Args
{
   get { return
args; }
   set { args = value
; }
}

Finally hook up an event handler for the Invoked event of the HandleExternalEvent activity. This will be used to increment the counter that keeps track of the number of served clients (you could do this inside the doWork CodeActivity too):

private void clientArrival_Invoked(object sender, ExternalDataEventArgs e)
{
   clientCount++;
}

Notice you could use this event handler too in order to extract information from the event args that were raised with the exception.

Finally, the property grid of the HandleExternalEventActivity clientArrival should look like this:

The host

On to the host side now. What we want to do, is ask the end-user for a name and then raise the event to the workflow instance to indicate a "client arrival". The workflow should then proceed in the WhileActivity loop and perform work for the newly arrived user.

To do this, we'll first implement IBar as follows:

class Bar : IBar
{
   public event EventHandler<FooEventArgs
> Foo;

   public void RaiseEvent(Guid instanceId, string
name)
   {
      if (Foo != null
)
      {
         EventHandler<FooEventArgs
> evt = Foo;
         FooEventArgs args = new FooEventArgs
(instanceId, name);
         evt(
null
, args);
      }

   }
}

Next, to establish the communication between the workflow and the "Bar" service, we need to register an ExternalDataExchangeService (from System.Workflow.Activities):

bar = new Bar();
ExternalDataExchangeService svc = new ExternalDataExchangeService
();
workflowRuntime.AddService(svc);
svc.AddService(bar);

I've created bar as a local variable in the host class:

private static Bar bar;

Once the workflow instance is started, we'll start a new thread (rather quick-n-dirty) to accept user input till the workflow terminates:

WorkflowInstance instance = workflowRuntime.CreateWorkflow(typeof(EventsDemo.Workflow1));
instance.Start();

Thread userInput = new Thread
(UserInput);
userInput.Start(instance.InstanceId);

waitHandle.WaitOne();

userInput.Abort();

That is, the background thread userInput is started with a parameter to indicate the workflow instance to raise events in. When the workflow completes (when waitHandle is set), the background thread is aborted (there exist cleaner ways to implement this idea, but for demo purposes this should be okay). This background thread is defined as follows:

static void UserInput(object instanceId)
{
   for
(; ; )
   {
      Thread.Sleep(500);
//dirty demo trick
      Console.Write("User name: "
);
      string name = Console
.ReadLine();
      bar.RaiseEvent((
Guid
) instanceId, name);
   }
}

The core line of code is indicated in bold and does the real work of notifying the workflow instance of the simulated "client arrival". Don't worry about the Thread.Sleep call which is just there to keep the console output nice and smooth in a very dirty way to keep things simple and clean.

Now run the application, it should produce the following output (enter a few names and see what happens):

Conclusion

Local communication between a workflow and external data services is an absolute must for a lot of workflow scenarios. In this post and the previous post, you learned how to establish this kind of communication with a workflow in both directions, by making method calls and by waiting for events to occur. Enjoy!

Del.icio.us | Digg It | Technorati | Blinklist | Furl | reddit | DotNetKicks

Filed under: ,

Comments

No Comments