Windows Workflow Tutorial: Handling an Event from the Host in a Workflow

Ken Getz
MCW Technologies LLC

Published: January, 2009

Articles in this series

Download the code for this article

Introduction

In an earlier tutorial in this series, you learned how to call a method in the host application from a workflow (and how to pass information from the workflow to the host, in the parameters to the method). If you haven’t worked through that tutorial, you should do so now—without that information, this tutorial will not make much sense.

(This tutorial starts with the finished solution from the earlier tutorial. If you don’t have that solution handy, you can download it here)

This technique you’ve already learned doesn’t help, however, if you need to send information from the host application to the running workflow. Maybe you need to indicate to the workflow that a particular condition has been met, or that the application has gathered some information it needs to send to the workflow.

Imagine that the host application started the workflow, but the workflow progresses to a point at which it needs input. It must get that input from the host application, so it must wait until the host has gathered the necessary information. In order to pass the information from the host application to the workflow, the host application can raise an event, so that the workflow can handle the event using the HandleExternalEvent activity.

In order to pass information from the host to the workflow, you’ll need a class that defines the event arguments. If you weren’t building an application using Windows Workflow Foundation, you could simply inherit from the System.EventArgs class, adding the specific information required by your event. In this case, however, the stakes are a bit higher, and your event argument class must meet specific requirements. Your class must support these characteristics:

•             It must inherit from System.Workflow.Activities.ExternalDataEventArgs.

•             It must be serializable.

•             It must provide a non-default constructor that accepts a workflow instance ID (a Guid) as its parameter. Using this information, the event argument object can determine the specific instance of the workflow to which it was sent.

In order to pass information to the workflow, you’ll create a class that meets these criteria. Just as in the previous example, you’ll rely on the Windows Workflow Foundation’s ExternalDataExchange service. In this exercise, you’ll modify the workflow you created in the previous exercise, and have the workflow wait for you to supply a path in which to search, in the host application.

Modify the Workflow

In order to handle the event raised by the host application, the workflow needs to include a HandleExternalEvent activity, and this activity is useful when placed within a Listen activity. The Listen activity contains two or more branches, and the first activity in each branch must implement the IEventActivity interface—that is, the first activity must be configured so that it waits for some event to occur (either an event raised externally, or a timer event within the workflow, for example). The Windows Workflow Foundation includes two activities that implement this interface—the Delay activity and the HandleExternalEvent activity. Each branch, then, waits until its first child activity’s event occurs, and the Listen activity executes the remainder of the activities within the single branch. All other branches never execute. In other words, the first branch whose event occurs “wins”. For this simple example, however, you’ll work with the HandleExternalEvent activity on its own.

If you closed the solution you created in the first part of this tutorial, re-open it in Visual Studio 2008 now. In the Solution Explorer window, double-click the Workflow1.vb or Workflow1.cs item, loading it into the Workflow designer.

From the Toolbox, drag a HandleExternalEvent activity immediately above the existing While activity. When you’re done, the workflow should look like Figure 1.

 

Figure 1. The completed layout should look like this.

You should imagine that this small workflow is actually part of a larger workflow in which the workflow has reached a point in its processing at which it requires input from the user. For this simple workflow, you could simply supply the path as a parameter, as you did in the previous exercise. Use your imagination here.

At this point, the workflow is broken—until you supply information about the event to the HandleExternalEvent activity, the application can’t run. The next few sections walk you through building all of the necessary infrastructure so that the workflow can handle the event.

Add the Event Argument Class

In order to pass information from the host to the workflow, you must create a class that meets specific requirements to act as the event argument. In the Solution Explorer window, right-click the FindFilesWorkflow project, and select Add | Class from the context menu. Name the new class InfoEventArgs, and click Add.

Add the following statement to the top of the new code file:

Imports System.Workflow.Activities

using System.Workflow.Activities;

Modify the InfoEventArgs class so that it inherits from the ExternalDataEventArgs class, and ensure that the class is public. Next, add the Serializable attribute to the class:

<Serializable()> _

Public Class InfoEventArgs

  Inherits ExternalDataEventArgs

End Class

[Serializable()]

ublic class InfoEventArgs: ExternalDataEventArgs

Inside the InfoEventArgs class, add a property to contain the path in which the workflow should search:

Private pathValue As String

Public Property Path() As String

  Get

    Return pathValue

  End Get

  Set(ByVal value As String)

    pathValue = value

  End Set

End Property

public string Path { get; set; }

Inside the class, create a constructor that accepts a Guid value and the path value, and pass the Guid value to the base class’ constructor:

Public Sub New(ByVal instanceId As Guid, Path As String)

  MyBase.New(instanceId)

  Me.Path = Path

End Sub

public InfoEventArgs(Guid instanceId, string Path):

  base(instanceId)

{

  this.Path = Path;

}

Select File | Save All to save the entire solution.

Modify the Interface

In the previous exercise, you created an interface that defines the interaction between the host and the workflow. In this exercise, you’ll extend that interface, adding information about the event the host application will raise in order to send information to the workflow.

In the Solution Explorer window, in the FindFilesWorkflow project, double-click the ICommunicate.vb or ICommunicate.cs item. Add the following declaration to the existing interface:

Event PathReceived As EventHandler(Of InfoEventArgs)

event EventHandler<InfoEventArgs> PathReceived;

Note: In a real application, you are better off placing the shared interface in a separate assembly. As it is, in this example, if you modify the workflow, you modify the versioning associated with the interface. This can cause trouble with persisted workflows. For this simple demonstration, it’s not worth the overhead of creating a separate project, but in more complex projects, place the shared interface in a separate assembly and reference it from both the host and the workflow assemblies.

Finish the Workflow

Now that you have completed the interface and the event arguments, you can finish the workflow by indicating the specific event that the workflow should wait for. Again open Workflow1 in the Workflow designer, and select the HandleExternalEvent activity. In the Properties window, select the InterfaceType property, click the ellipsis to the right of the property value, and select ICommunicate from the list of available interfaces. Select the EventName property, and from the drop-down list of event names, select PathReceived. (Note that this action adds e and sender properties to the Properties window, corresponding to the parameters passed into the event handler. You don’t need to interact with the sender property, but the e property provides information sent from the host, and you’ll need to gather than information.) Figure 2 shows the current state of the Properties window.

 

Figure 2. The properties window, after you have set the InterfaceType and EventName properties.

Because your workflow needs to capture the Path property of the event argument sent by the host to the workflow, you must modify the workflow so that it includes an InfoEventArgs variable into which to place the value. Select View | Code to load the workflow’s code, and add the following property to the class (note that this property sets the value of the workflow’s Path property, in the property setter):

Private argsValue As InfoEventArgs

Public Property args() As InfoEventArgs

  Get

    Return argsValue

  End Get

  Set(ByVal value As InfoEventArgs)

    argsValue = value

    Path = args.Path

  End Set

End Property

private InfoEventArgs argsValue;

public InfoEventArgs args

{

  get { return argsValue; }

  set

  {

    argsValue = value;

    this.Path = value.Path;

  }

}

Select View | Designer. Select the HandleExternalEvent activity, and in the Properties window, select the e property. Click the ellipsis to the right of the property, and select the args property from the list of values, as shown in Figure 3. Click OK when you’re done. You have specified that the HandleExternalEvent activity should store the value in its e parameter into the workflow’s args property, which, in turn, sets the value of the workflow’s Path property. (Note that if you need addition processing when the event occurs, you can create a handler for the activity’s Invoked event. You can access the argument from this event handler, as well, and you can add any necessary processing in this handler.)

 

Figure 3. Bind the second event parameter to the args property in the workflow.

Modify the Host Interface Implementation

Because you have modified the communications interface, you must modify the class within the host application that implements this interface. In the Solution Explorer window, in the ConsoleHost application, double-click the UserInterface.vb or UserInterface.cs item. In Visual Basic, click on the line of code including the Implements keyword, press End, and press Enter. This forces the editor to add the event declaration to the implementation. In C#, right-click the interface name, and select Implement Interface | Implement Interface from the context menu. This action adds the following statement to the implementation:

Public Event PathReceived(ByVal sender As Object, _

  ByVal e As FindFilesWorkflow.InfoEventArgs) _

  Implements FindFilesWorkflow.ICommunicate.PathReceived

public event EventHandler<InfoEventArgs> PathReceived;

It’s up to you to add the code that raises this event, and to do that, add the following procedure to the UserInterface class. This code accepts the workflow’s instance ID (which you don’t yet have a way to capture) and the path to be searched. It sets up the new InfoEventArgs object, and raises the PathReceived event:

Public Sub RaisePathReceived( _

 ByVal instanceId As Guid, _

 ByVal Path As String)

  Dim args As New InfoEventArgs(instanceId, Path)

  RaiseEvent PathReceived(Me, args)

End Sub

public void RaiseNameReceived(

  Guid instanceId, string Path)

{

  if (PathReceived != null)

  {

    InfoEventArgs args = new InfoEventArgs(instanceId, Path);

    PathReceived(null, args);

  }

}

Modify the Startup Code

Everything is in place to send information from the host to the workflow, except for the actual code that raises the event. In this example, you must modify the application’s Main procedure, to prompt the user for a path. Given the path, the host raises the appropriate event to indicate to the workflow that the user has entered the information.

(Note that you although you could place this user-interface code in one of the workflow events, such as the WorkflowIdled event, doing so would cause a problem. These events block the workflow thread and any long-running work. If you want to gather user input in one of the workflow events, you should place the code in a separate thread. Investigate the System.Threading.ThreadPool.QueueUserWorkItem method to gather user input in a separate thread.)

In the Solution Explorer window, double-click the Program.cs or Module1.vb item. Note that the Main procedure already includes code that adds the ExternalDataExchangeService, and an instance of the UserInterface class as the communications class—you don’t have to add this code, it already exists:

Dim dataService As New ExternalDataExchangeService

workflowRuntime.AddService(dataService)

dataService.AddService(ui))

var dataService = new ExternalDataExchangeService();

workflowRuntime.AddService(dataService);

dataService.AddService(ui);

The code currently passes a dictionary containing information about the Path property to the workflow. To test the event mechanism, remove that code. To do that, start by commenting out the following two lines of code:

' Dim parameters As New Dictionary(Of String, Object)

' parameters.Add("Path", "C:\")

// var parameters = new Dictionary<String, Object>();

// parameters.Add("Path", "C:\\");

Modify the call to the CreateWorkflow method, removing the parameters variable as the second parameter. When you’re done, the call to the CreateWorkflow method should look like this:

workflowInstance = workflowRuntime.CreateWorkflow( _

  GetType(FindFilesWorkflow.Workflow1))

WorkflowInstance instance = workflowRuntime.

  CreateWorkflow(typeof(FindFilesWorkflow.Workflow1));

In the Main procedure, immediately preceding the call to the waitHandle.WaitOne method, add the following code:

Console.WriteLine("Enter the search path:")

Dim path As String = Console.ReadLine()

ui.RaisePathReceived(workflowInstance.InstanceId, path)

Console.WriteLine("Enter the search path:");

string path = Console.ReadLine();

ui.RaisePathReceived(workflowInstance.InstanceId, path);

.Finally, press Ctrl+F5 to execute the workflow. When the workflow executes the HandleExternalEvent activity, it idles, waiting for the host to raise the PathReceived event. This code retrieves the path from you, and once you supply it, the code raises the event to the workflow, allowing it to continue processing, given the information you supplied.

Conclusion

Raising an event from the host application to the workflow sure seems like a lot of steps! It does require a lot of steps, and you need to pay attention to a lot of detail. To raise an event from the host to the workflow, you must at least accomplish these goals:

1.            Handle event arguments. You must create a class that contains the event argument information you want your event to be able to send to the workflow. This class must inherit from System.Workflow.Activities.ExternalDataEventArgs, and it must be serializable.

2.            Define the communications interface. You must create an interface marked with the System.Workflow.Activities.ExternalDataExchange attribute applied to it. In the interface, add the definition of any event you want to raise from the host to the workflow.

3.            Define the host. Create the host application (a console application, a Windows application, or any other kind of application), including an implementation of the communications interface. The class that implements the interface must also be serializable.

4.            Handle the event in the workflow. Add a HandleExternalEvent activity, and set at least its InterfaceType and EventName properties. If you want to receive information from the event, consider binding the event argument to a corresponding property in the workflow’s class.

5.            Create and configure the data service. In the host application, create a new instance of the ExternalDataExchangeService class, and add it as a service to the Workflow Runtime. Then, create an instance of the communication class (the class that implements the communication interface) and add it as a service to the new ExternalDataExchangeService instance.

6.            Raise the event. In the host application, at the appropriate time, raise the event. If you’ve followed all the steps carefully, the workflow receives the event, and handles it.

Although it seems difficult to communicate between the host application and the workflow, it’s really not. Follow the steps carefully, and you will be able to send information from the host application to the workflow without any problems.

About the Author

Ken Getz is a developer, writer, and trainer, working as a senior consultant with MCW Technologies, LLC. In addition to writing hundreds of technical articles over the past fifteen years, he is lead courseware author for AppDev (http://www.appdev.com). Ken has co-authored several technical books for developers, including the best-selling ASP.NET Developer’s Jumpstart, Access Developer’s Handbook series, and VBA Developer’s Handbook series, and he’s a columnist for both MSDN Magazine and CoDe magazine. Ken is a member of the INETA Speakers Bureau, and speaks regularly at a large number of industry events, including 1105 Media’s VSLive, and Microsoft’s Tech-Ed.