Windows Workflow Tutorial: Calling a Method in a Host from a Running Workflow

Ken Getz
MCW Technologies LLC

Published: January, 2009

Articles in this series

Download the code for this article

Introduction

Although it may not otherwise be obvious, a running workflow and its host application run in separate threads. You may have a need, as you build your application, to provide communication between the host application and the running workflow; although the .NET Framework provides several cross-application communication mechanisms, none are suited specifically for use with Windows Workflow Foundation (WF). Instead, WF provides its own mechanism for allowing the host application to call a method in (and pass information to) a running workflow, and for a running workflow to call a method in the host application.

The bi-directional communication between a workflow and its host is loosely coupled, and the mechanism makes it easy for a variety of different applications (perhaps a Windows application, a Web application, a Console application, and a Windows Service) to each host the same workflow. Because the workflow contains no information about the specific implementer of the method it calls in its host, the workflow can simply execute a method, and know that its host reacts to the method call.

Clearly, the the host and the running workflow need some mediation—some third party needs to control the communication. The Windows Workflow runtime takes on this task, and as you’ll see in this tutorial, you must indicate to the WF Runtime  exactly where it should look for the class that provides the implementation of the interface that defines the communication layer between the host and the workflow.

Although setting up the communication between the workflow and the host isn’t difficult, it requires several steps, and you must follow the instructions carefully. Therefore, the examples you’ll find in this tutorial aren’t even vaguely useful, but they do indicate the steps you’ll need to follow in order to communicate both from the host to the workflow, and from the workflow to the host.

Calling a Method in the Host Application from the Running Workflow

Imagine that you’ve created a workflow, and this workflow processes files in a folder within the file system. As the workflow processes each file, you’d like the host application to display the file’s name, indicating the current progress of the workflow. Clearly, a Console application must handle this display differently than does a Windows application, but either way, the running workflow simply needs to execute a method in the host application which displays the file name using appropriate means.

In order to call a method in the host application, the workflow must include an instance of the CallExternalMethod activity. This activity requires you to indicate a particular interface that defines the external procedure, and allows you to bind parameters (and the return value) for the procedure to properties of the workflow. You interact with the CallExternalMethod activity as you follow the steps in this tutorial.

In order to create a method in the host application that you can call from the workflow, you’ll need to tackle a series of steps. You must:

  • Create an interface that defines the procedure you want to call. You must attach the ExternalDataExchange attribute to this interface, which marks the interface as a local service interface. In this interface, define any methods that you want to be able to call from the workflow. (Note that overloading isn’t allowed in this context, even though it is a valid code construct.)
  • Create the workflow, including an instance of the CallExternalMethod activity. Supply the activity with information about the interface, including the specific method it should call.
  • Create the interface implementation, in the host application. Any class can implement the interface, and in this class, you must provide the actual method(s) that the workflow will call. This implementation of the interface provides the code that the WF Runtime executes when the workflow invokes the method.
  • Hook up the plumbing, in the code that starts up the Workflow Runtime. Here, you must add a new instance of the ExternalDataExchangeService class as a service for the Workflow Runtime, and you must indicate to the new service exactly which class instance it should look in for the method that the workflow called.

Create the Console Application

To get started, in Visual Studio 2008, create a new Sequential Workflow Library project named FindFilesWorkflow. Once you’ve created the new solution, add a new Sequential Workflow Console Application project to your solution, and name it ConsoleHost. (At this point, your solution contains two projects, each of which contains an empty workflow designer. You might wonder why you created both a workflow library project and a sequential workflow Console application—each project template makes it easier to interact with WF, because each project already includes the necessary assembly references, and the Sequential Workflow Console Application template includes the necessary Workflow startup code. By creating this type of application, you don’t need to write that startup code yourself.) In order to create the separation between the host application and the workflow itself, for the purposes of this demonstration, you’ll delete the workflow designer from the host application, and modify the host’s startup code to refer to the workflow in the library application, instead. (See Figure 1.)

Figure 1. Your solution should contain two projects.

In the Solution Explorer window, right-click the ConsoleHost project, and select Select as Startup Project from the context menu. Right-click the project again, and select Add Reference from the context menu. In the Add Reference dialog box, select the Projects tab, select the FindFilesWorkflow project, and click OK to add the reference. Now that you’ve added the reference to the workflow library, you can modify the console application’s start-up code so that it creates an instance of the library’s workflow. To do that, in the Solution Explorer window, double-click Module1.vb or Program.cs, loading the class into the code editor. In the code, find the call to the WorkflowRuntime.CreateWorkflow method. Replace the reference to ConsoleHost.Workflow1 so that the code creates an instance of FindFilesWorkflow.Workflow1:

workflowInstance = workflowRuntime.CreateWorkflow( _
  GetType(FindFilesWorkflow.Workflow1))
WorkflowInstance instance =
  workflowRuntime.CreateWorkflow(
  typeof(FindFilesWorkflow.Workflow1));

In the Solution Explorer window, in the ConsoleHost project, delete the Workflow1.cs or Workflow1.vb project item.

Create the Interface

The goal of your workflow is to search for files and take some action as it finds each file; you need some way to report the progress within the host application. Because the workflow has no information about the host application, it can’t display information itself. Instead, it must call a method, defined in an interface, that the host application implements. Although the shared interface could exist within any assembly, for the purposes of this tutorial, you’ll place it within the workflow library.

Note: In a real application, this choice may not be a good one because of versioning issues. When using the same assembly, the whoe library’s version number will change every time you change a workflow in the library. You are better off placing the interface into a separate assembly. Once you’ve created the separate assembly, you will simply add references from both the host and the workflow assembly to the interface assembly. We will use the same assembly for the simplicity of this demonstration, but keep it in mind as you create your own applications.

In the Solution Explorer window, select the FindFilesWorkflow project. In the menus, select Project | Add New Item. In the Add New Item dialog box, select Interface. Name the new interface ICommunicate, and click Add to create the interface. In C#, add the public keyword, so that the interface is publicly available:

Public Interface ICommunicate
End Interface
namespace FindFilesWorkflow
{
  public interface ICommunicate
  {
  }
}

Modify the ICommunicate interface, adding the ReportProgress method definition:

Public Interface ICommunicate
  Sub ReportProgress(ByVal fileName As String)
End Interface
namespace FindFilesWorkflow
{
  public interface ICommunicate
  {
    void ReportProgress(String fileName);
  }
}

In order for the workflow to be able to interact with the interface you’ve created, it must include a marker attribute; that is, an attribute that indicates that the interface can be added as a service to the ExternalDataExchange service in Windows Workflow. To do this, add the ExternalDataExchange attribute to the ICommunicate interface, so that the code looks like the following:

<ExternalDataExchange()> _
Public Interface ICommunicate
  Sub ReportProgress(ByVal fileName As String)
End Interface
[ExternalDataExchange]
public interface ICommunicate
 void ReportProgress(String fileName);

In C#, you must also add a using statement to the top of the file (Visual Basic adds a project-wide Imports statement for the correct namespace). In C#, right-click the new attribute, and select Resolve from the context menu. In the fly-out menu, select the first option, which adds a using statement for the System.Workflow.Activities namespace to the file for you. (Take this as a warning: If you don’t add the ExternalDataExchange attribute to your interface, your workflow will not be able to call the corresponding method(s) in the host application.)

Create the Workflow

Now that you’ve created the interface, you can set up the workflow so that it calls the method described in the interface. In the Solution Explorer window, in the FindFilesWorkflow project, double-click the Workflow1.vb or Workflow.cs item, opening the workflow in the Workflow designer. Add a While activity. Within the While activity, add a CallExternalMethod activity. When you’re done laying out activities, the workflow should look like Figure 2.

Figure 2. Lay out the sample workflow so that it looks like this.

Select View | Code. Add the following statement at the top of the file:

Imports System.IO
using System.IO;

In the Workflow1 class, add the following variable declarations:

Public currentFileName As String
Private files As String()
Private totalFiles As Integer
Private currentFile As Integer
public string currentFileName;
private string[] files;
private int totalFiles;
private int currentFile;

In the Workflow1 class, add the following procedure:

Private Sub SetFiles(ByVal Path As String)
  If Not String.IsNullOrEmpty(Path) Then
    files = Directory.GetFiles(Path)
    totalFiles = files.Length
    currentFile = 0
  End If
End Sub
private void SetFiles(string Path)
{
  if (!String.IsNullOrEmpty(Path))
  {
    files = Directory.GetFiles(Path);
    totalFiles = files.Length;
    currentFile = 0;
  }
}

Within the Workflow1 class, add the following property (note that the property setter calls the SetFiles method, filling in the list of files in the selected path):

Private pathValue As String
Public Property Path() As String
  Get
    Return pathValue
  End Get
  Set(ByVal value As String)
    pathValue = value
    SetFiles(value)
  End Set
End Property
private string pathValue;
public string Path
{
  get { return pathValue;}
  set
  {
    pathValue = value;
    SetFiles(value);
  }
}

Select View | Designer. Select the While activity, and, in the Properties window, set the Condition property to Declarative Rule Condition. Expand the + to the left of the Condition property, and set the ConditionName property to MoreFiles. Select the Expression property, click the ellipsis to the right of the property value, and in the Rule Condition Editor window, add the following expression (see Figure 3), and click OK when you’re done:

this.CurrentFile < this.totalFiles

Figure 3. Set the While activity’s condition.

After configuring the While activity, the Properties window should look like Figure 4.

Figure 4. Configure the While activity.

In order to set up the CallExternalMethod activity so that it can call a method outside the workflow, you must supply the name of the interface that defines the method the workflow will call, along with the name of the specific method (and how to bind any parameters, and the return value, if any) for the method.

Select Build | Rebuild Solution. Visual Studio will return errors (because you haven’t completed setting up the required properties of some of the workflow activities). In the Workflow designer, select the CallExternalMethod activity. In the Properties window, select the InterfaceType property. Click the ellipsis to the right of the property value, displaying the dialog box shown in Figure 5. This dialog box includes a list of all the available interfaces that have been marked with the ExternalDataExchange attribute. In this case, the list contains only the ICommunicate interface, so select that interface and click OK.

Figure 5. Select from the list of interfaces marked with an ExternalDataExchange attribute.

In the Properties window, select the MethodName property. Click the drop-down button to the right of the property value, and select ReportProgress (the name of the method within the interface) from the list. Of course, the ReportProgress method requires a single parameter (the name of the file that the workflow has found), and the Workflow designer adds this property to the Properties window once you select the name of the method you want to call (see Figure 6). Because the parameter name appears within the Properties window, you can bind it to a property of the workflow, just like any other bindable Workflow property.

Figure 6. Once you specify an interface and a method within that interface, the designer offers to allow you to bind a value to each of the method’s parameters.

In the Properties window, select the fileName property, and click the ellipsis to the right of the property value. In the dialog box, select the workflow’s currentFileName property, as shown in Figure 7. Click OK to create the data binding.

Figure 7. Bind the ReportProgress procedure’s fileName parameter to the workflow’s currentFileName property.

Now that you have bound the ReportProgress procedure’s fileName parameter to the workflow’s currentFileName property, when the workflow calls the procedure, it will pass the value in the currentFileName property as the parameter to the ReportProgress procedure. (Note that you haven’t yet implemented the ICommunicate interface, which means you haven’t yet created the ReportProgress method that the workflow will call.)

Of course, you must set the private currentFileName property each time the CallExternalMethod activity is about to call the external method. The activity provides its MethodInvoking property so that you can specify such a procedure. In the Properties window, select the MethodInvoking property, and supply the name for the procedure, SetFileName. Press Enter to create the procedure, and modify it so that it sets the current file name and increments the current file number:

Private Sub SetFileName( _
 ByVal sender As System.Object, ByVal e As System.EventArgs)
  currentFileName = files(currentFile)
  currentFile += 1
End Sub
private void SetFileName(object sender, EventArgs e)
{
  currentFileName = files[currentFile];
  currentFile++;
}

Select Build | Build Solution, and verify that the entire solution builds correctly.

Create the Interface Implementation

You have created the workflow and the interface; now it’s time to create the interface implementation. In the Solution Explorer, right-click the ConsoleHost project, and select Add | Class from the context menu. In the Add New Item dialog box, set the name of the new class to UserInterface, and click Add.

At the top of the new class file, add the following statement:

Imports FindFilesWorkflow
using FileFilesWorkflow;

Modify the new class so that it’s serializable, and so that it implements the ICommunicate interface.

<Serializable()> _
Public Class UserInterface
  Implements ICommunicate
  End Sub
End Class
[Serializable()]
class UserInterface: ICommunicate

In Visual Basic, adding the Implements statement also adds the stub for the ReportProgress procedure. In C#, right-click the name of the interface, and select Implement Interface | Implement Interface from the context menu—this creates the ReportProgress procedure stub for you.

Modify the new ReportProgress procedure, adding code to display the output in the Console window:

Public Sub ReportProgress(ByVal fileName As String) _
  Implements ICommunicate.ReportProgress
  Console.WriteLine("Processing file: {0}", fileName)
End Sub
public void ReportProgress(string fileName)
{
  Console.WriteLine("Processing file: {0}", fileName);
}

Build the entire solution again, and verify that it builds without errors.

Hook Up the Plumbing

You’ve done most of the work, but the Workflow Runtime still doesn’t know how to find the code that you’ve just added, so it calls the code when the workflow executes its CallExternalMethod activity. To hook things up, you must configure the Workflow Runtime, adding an additional service to the runtime, and telling the service to look in a specific instance of the class you just created when it needs to call the ReportProgress method.

In the Solution Explorer window, in the ConsoleHost project, double-click the Program.cs or Module1.vb item. In C#, at the top of the file, add the following statement:

using System.Workflow.Activities;

Within the Program class, add the following declaration:

Private Shared ui As New UserInterface()
private static UserInterface ui = new UserInterface();

In the Main procedure, immediately above the declaration of the workflowInstance variable, add the following code which creates and adds an instance of the ExternalDataExchangeService class as a service:

Dim dataService As New ExternalDataExchangeService
workflowRuntime.AddService(dataService)
var dataService = new ExternalDataExchangeService();
workflowRuntime.AddService(dataService);

Continue the same block of code, adding the following line. This code adds an instance of the UserInterface class (the class that implements the ICommunicate interface), as a service for the ExternalDataExchange service—in other words, the code indicates to the ExternalDataExchangeService instance where it can find the code it needs to call when the workflow instance calls its external method:

dataService.AddService(ui)
dataService.AddService(ui);

You still need to specify a path in which to search for files, as you create the workflow. Continue the same code block (immediately above the call to the CreateWorkflow method), adding the following code (this example looks for files in the C:\ folder—use any folder you like, in your code):

Dim parameters As New Dictionary(Of String, Object)
parameters.Add("Path", "C:\")
var parameters = new Dictionary<String, Object>();
parameters.Add("Path", "C:\\");

Finally, modify the call to the CreateWorkflow method, so that you pass the parameters dictionary to the workflow runtime as you create the workflow:

Dim workflowInstance As WorkflowInstance
workflowInstance = workflowRuntime.CreateWorkflow( _
  GetType(FindFilesWorkflow.Workflow1), parameters)
WorkflowInstance instance = workflowRuntime.CreateWorkflow(
  typeof(FindFilesWorkflow.Workflow1), parameters);

At this point, you’ve done all the work necessary to try out your workflow. The workflow looks in the path you’ve specified for files. As it finds each one within its While activity, it calls the external method you created outside of the workflow. In order to do this, the Workflow Runtime uses the ExternalDataExchange service that you created. Because you specified to the ExternalDataExchange service instance where it could find the instance of the class that implements the interface and method that the workflow expects to call, it routes the method call appropriately.

Press Ctrl+F5 to run the project. You should see output as shown in

Figure 8. Although the output is somewhat underwhelming, it proves a point: You were able to call a method in the host application from the running workflow, using the ExternalDataExchange service.

Figure 8. The output should look like this.

Conclusion

It seems like a lot of work to call a method in the host application from a workflow, but it’s not difficult as long as you follow the required steps. You must:

  1. Define the interface. Create a public interface that defines the communication between the workflow and the host. Make sure to add the System.Workflow.Activities.ExternalDataExchange attribute to the interface.
  2. Add the CallExternalMethod activity to the workflow. Set at least the InterfaceType and MethodName properties. Bind parameters and the return value, if you like, to properties of the workflow.
  3. Implement the method(s) in the host. Create a class that implements the communication interface, adding code for the implemented methods. Make this class serializable.
  4. 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.

If you follow these steps carefully, you should be able to call methods defined in the host application from your running workflows. In a future tutorial, you’ll learn how to communicate from the host application to the workflow, sending information to the workflow, by raising an event in the host application. The workflow can use its HandleExternalEvent activity to handle the event, and retrieve the information passed in from the host. For now, try creating your own workflow that calls a method in the host application, and verify that you can follow the list of steps presented here.

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.