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.
Related Links