Click here to Skip to main content
15,867,756 members
Articles / Programming Languages / C#

Microsoft Windows Workflow Foundation

Rate me:
Please Sign up or sign in to vote.
5.00/5 (9 votes)
31 Aug 2009CPOL12 min read 74.1K   1.6K   52   5
An artilce that explains how to call external data and methods.

Calling External Data and Methods

Basic Workflow Concepts to Start

A workflow is defined as the basic tasks, procedures, people and organizations, system informational input and output, policies and rules, and tools needed for each step in a business process. The sum of these parts, or components, are required to comprise the whole process required in order to make a business process work. Anyone who has followed Microsoft Corporation’s technological advances may have noticed that one of their first-in-rank goals is to automate the inter-workings of these business process elements with software to enable a going concern to achieve their expected level of progress and profit from those processes. Applications that transfer data use automation, and achieve interoperability with other software vendors’ software. Workflow processing stems from documents having to go from place to place for approval and review. But a specific set of tasks, coupled with decision making, is something that we can generalize. In other words, if a set of financial people need a software package to perform a certain service, then they would need to communicate this need with the IT people and the software developers in order to set up a blue print plan as a baseline. Their terminology may differ, just as the same word may be defined differently by the legal profession and the accounting profession. With this generalized definition of a workflow, this article will examine how to build and call local data services that are external to the workflow. The material for this article is largely referenced from Kenn Scribner's book, Windows Workflow Foundation Step by Step, chapter 8. Having said that, let's explore an informal definition of a workflow. In software, a workflow can be viewed as an executable's representation of a work process. A workflow is a means to organize and run tasks, procedural steps, or activities, and it can involve people either in the same organization or belonging to multiple organizations.

The Microsoft Windows Workflow Foundation provides a programming framework and tools for developing and executing a wide variety of workflow-based applications. Assume, for example, an insurance company that wants to ensure performance -- a request for a proposal is handled efficiently from the draft to the final version, which would comprise either the policy or the guidelines under which a policy can be authorized. A workflow can take care of this process and ensure that every person involved in handling the request for the proposal uses the correct document and successfully completes their part in the process before allowing the workflow process to continue to the next step, to the next person.

Now, if a local data service is going to communicate with a workflow, then we must understand how interfaces are used to communicate between the host process and the workflow. How? The Microsoft Windows Workflow Foundation makes the creation of applications that contain asynchronous, stateful, long-running workflows a lot easier. The runtime engine manages workflow execution, and allows workflows to remain active for long periods of time and survive computer restarts. Runtime services offer functionality, such as transactions and persistence to manage errors correctly. So, thus far, we now understand that the Windows Workflow is not a standalone application, as it always needs to be hosted by another application.

To build a practical workflow, we will use external data sources designed to transfer data between our workflow and the host application. This article will assume that the reader has dealt with the Microsoft Workflow Foundation before. The purpose of this article is to step through the process of a workflow that seeks and processes data from external sources and returns that data in some processed form to our application (a Windows Forms application that will provide the user interface for the returned data). But first a reminder of activities.

Activities are units of organization of a workflow. They are performed by system functions or people, making them the basic building blocks that comprise a workflow. When you build workflows, you gather the individual activities together and move from one activity to the next. Assume that you have to FTP a file. You open the FTP site, log in, type binary, and type hash for hash printing, and then get the file. This is a task. So, if you divide a business process into parts, you would find that it is composed of smaller, granular tasks. Some activities form a single task. Other activities function as a container for other activities. Here is the basic precept: a container-based activity is chosen to hold all of the rest – that is the root activity. The root activity will either be a sequential activity or a state-machine activity.

When a workflow communicates with its host application (and especially when it receives and sends data), the workflow does so using queues and messages, which is actually handled internally by the workflow. This simplifies the overall process, and allows us to concentrate on writing our application tasks, as the Workflow uses an abstraction layer to buffer the workflow from the host. That abstraction layer simply enables fed input to be flown as output information. In this case, that layer of abstraction is known as the local communication service. Like any service in WF terms, it’s another pluggable service. The difference is that we will write part of the service rather than use the prebuilt workflow service. Why not use the prebuilt service? Because the data you pass between your host application and your workflow is specific to the application. Think of this local service as a connector. Using a tool, I’ll generate activities designed to send and receive data from the workflow’s perspective. From the host application’s perspective, receiving data amounts to an event, while sending data is basically a method call on the service object. Apart from the aforementioned, we also need to add the ExternalDataService to our workflow runtime. ExternalDataService is a pluggable service that facilitates transferring serialized data between workflow instances and the Windows Forms host applications. Note that whatever data we try to transfer, it must be serializable. Whether the transferred data is a string or a set of objects, we must then design an interface that the ExternalDataService can bind to. This interface will contain the needed methods. Noting that we will use a connector, or a “bridge”, we will need to create two classes: a connector class and a service class. With the interface created at our disposal, we’ll execute a tool, which is normally located in the Microsoft SDKs C:\Program Files\Microsoft SDKs\Windows\v6.0A\bin directory that is called wca.exe. This tool is called the Workflow Communications Activity generator utility, and given an interface, it will generate two activities we can use to bind the interface to our workflow instance: one for sending data, the invoker, and one for receiving data, the sink.

Our Department of Motor Vehicles (DMV) Data Checker

This section below is, again, referenced from Kenn Scribner's book, Windows Workflow Foundation Step by Step, chapter 8, a book which I highly recommend. While this application uses Windows Forms, it could just as well have used an ASP.NET 2.0 web application. The application that we will build is a Windows Forms (C#) application that provides a user interface for gathering motor-vehicle data for specified drivers. This is a very basic application, and the reader should know that calling external data sources actually stresses a concept called “correlation”, a topic that should be studied independently. Below is an image of the main user interface form:

Capture.JPG

So, when building a workflow, we can generalize at least four steps:

  1. Create the Workflow.
  2. Set the workflow initiation settings according to the scope of your project.
  3. Configure the predefined conditions and activities.
  4. Generate the workflow definition and deployment.

When you click the Retrieve MV Data button, you initiate a workflow instance. The user interface disables the Retrieve button as well as the driver drop-down list control, and displays a type of searching notice (note that it is not an actual progress bar control). The picture below shows the image while it is searching or retrieving the external data that it will process and display.

1.JPG

A Brief Note of Comparison

If you have ever worked with Excel Services, then you have built a repository of Excel documents to manage. When the search engine calls for a specific document, the numerical data is called from an external data source and the Excel workbook (or spreadsheet) is displayed in the form of an HTML web page. The numerical data is checked for data integrity to then populate the Excel workbook that now appears in the form of a web page. While Excel and Internet Explorer are both ActiveX technologies, Excel Services does not allow the same data on its own spreadsheet to appear on another’s desktop without calling an external data source to verify both the calculations and numerical data. This same principle applies to the workflow runtime engine. Every running workflow instance is created and maintained by an in-process runtime engine that is commonly referred to as the workflow runtime engine. There can be several workflow runtime engines within an application domain, and each instance of the runtime engine can support multiple workflow instances running concurrently. When a workflow model is compiled, it can be executed inside any Windows process including console applications, forms-based applications, Windows Services, ASP.NET Web sites, and Web services. Because a workflow is hosted in process, a workflow can easily communicate with its host application.

When the workflow instance has completed its work, it uses an activity we’ll create to fire an event the host application intercepts, which tells the host application that the data is available. Now, because Windows Forms ListView controls don’t bind directly to DataTable objects, we will iterate through the tabular data and insert the rows in the view ourselves after receiving data from the workflow. The completed ListView controls are shown below:

2.JPG

Here is the interface:

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
using System.Workflow.Runtime;
using System.Data;

namespace MVDataService
{
    [ExternalDataExchange]
    public interface IMVDataService
    {
        void MVDataUpdate(DataSet mvData);
    }
}

The interface above, IMVDataService, requires the ExternalDataExchange attribute in order to be used as an interface for data communication. The ExternalDataExchange attribute is a marker that the workflow uses to identify interfaces suitable for local communications service use.

Using ExternalDataEventArgs

Earlier in this article, I mentioned that, to the host application, communications from the executing workflow appear as events. The host application cannot possibly know beforehand when the workflow instance will have data. So rather than polling the data, as if we were to wait and iterate until the operation is complete, we let WF use the asynchronous model that .NET uses, and fire events when data is available. The host application hooks those events and reads the data. Because we want to send information to the recipient of our event, we need to create a customized event argument class. Anyone who has ever created a custom event argument class has probably used System.EventArgs as the base class. WF external data events, however, require a different argument base class: ExternalDataEventArgs, which itself derives from System.EventArgs, so we are in sync with the FCL namespace hierarchy. Below is the code:

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
using System.Workflow.Runtime;

namespace MVDataService
{
    [Serializable]
    public class MVDataAvailableArgs : ExternalDataEventArgs
    {
        public MVDataAvailableArgs(Guid instanceId)
            : base(instanceId)
        {
        }
    }
}

Now is the tricky part, as we must create an external data service to function as a bridge. OK. So, why is this turning into a complicated production? Well, unlike traditional .NET objects, workflow instances are executing within the confines of the workflow. We are not designing a type, defining its members, and creating an object to have the CLR lay out the memory, activate and execute the object. We are bounded in the confines of the workflow defined. That is, events into and out of the workflow instance are brokered by the workflow runtime. Here is the code for the bridge connector class:

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
using System.Workflow.Runtime;
using System.Data;

namespace MVDataService
{
    public sealed class MVDataConnector : IMVDataService 
    {
        private DataSet _dataValue = null;
        private static WorkflowMVDataService _service = null;
        private static object _syncLock = new object();

        public static WorkflowMVDataService MVDataService
        {
            get { return _service; }
            set
            {
                if (value != null)
                {
                    lock (_syncLock)
                    {
                        // Re-verify the service isn't null
                        // now that we're locked...
                        if (value != null)
                        {
                            _service = value;
                        } // if
                        else
                        {
                            throw new InvalidOperationException(
                                    "You must provide a service instance.");
                        } // else
                    } // lock
                } // if
                else
                {
                    throw new InvalidOperationException(
                                 "You must provide a service instance.");
                } // else
            }
        }

        public DataSet MVData
        {
            get { return _dataValue; }
        }

        // Workflow to host communication method
        public void MVDataUpdate(DataSet mvData)
        {
            // Assign the field for later recall
            _dataValue = mvData;

            // Raise the event to trigger host read
            _service.RaiseMVDataUpdateEvent();
        }
    }
}

Remember that connector mentioned earlier? That it required two classes: a connector class and a service class. Here is the code for the WorkflowMVDataService class:

C#
using System;
using System.Collections.Generic;
using System.Text;
using System.Workflow.Activities;
using System.Workflow.Runtime;
using System.Data;

namespace MVDataService
{
    public class WorkflowMVDataService
    {
        static WorkflowRuntime _workflowRuntime = null;
        static ExternalDataExchangeService _dataExchangeService = null;
        static MVDataConnector _dataConnector = null;
        static object _syncLock = new object();

        public event EventHandler<mvdataavailableargs> MVDataUpdate;

        private Guid _instanceID = Guid.Empty;

        public Guid InstanceID
        {
            get { return _instanceID; }
            set { _instanceID = value; }
        }

        public static WorkflowMVDataService CreateDataService(Guid instanceID, 
                                            WorkflowRuntime workflowRuntime)
        {
            lock (_syncLock)
            {
                // If we're just starting, save a copy of the workflow runtime reference
                if (_workflowRuntime == null)
                {
                    // Save instance of the workflow runtime.
                    _workflowRuntime = workflowRuntime;
                } // if

                // If we're just starting, plug in ExternalDataExchange service
                if (_dataExchangeService == null)
                {
                    // Data exchange service not registered, so create an 
                    // instance and register.
                    _dataExchangeService = new ExternalDataExchangeService();
                    _workflowRuntime.AddService(_dataExchangeService);
                } // if

                // Check to see if we have already added this data exchange service
                MVDataConnector dataConnector = (MVDataConnector)workflowRuntime.
                    GetService(typeof(MVDataConnector));
                if (dataConnector == null)
                {
                    // First time through, so create the connector and 
                    // register as a service with the workflow runtime.
                    _dataConnector = new MVDataConnector();
                    _dataExchangeService.AddService(_dataConnector);
                } // if
                else
                {
                    // Use the retrieved data connector.
                    _dataConnector = dataConnector;
                } // else

                // Pull the service instance we registered with the connection object
                WorkflowMVDataService workflowDataService = MVDataConnector.MVDataService;
                if (workflowDataService == null)
                {
                    // First time through, so create the data service and
                    // hand it to the connector.
                    workflowDataService = new WorkflowMVDataService(instanceID);
                    MVDataConnector.MVDataService = workflowDataService;
                } // if
                else
                {
                    // The data service is static and already registered with
                    // the workflow runtime. The instance ID present when it 
                    // was registered is invalid for this iteration and must be
                    // updated.
                    workflowDataService.InstanceID = instanceID;
                } // else

                return workflowDataService;
            } // lock
        }

        public static WorkflowMVDataService GetRegisteredWorkflowDataService(Guid instanceID)
        {
            lock (_syncLock)
            {
                WorkflowMVDataService workflowDataService = MVDataConnector.MVDataService;

                if (workflowDataService == null)
                {
                    throw new Exception("Error configuring data service..." + 
                                        "service cannot be null.");
                } // if

                return workflowDataService;
            } // lock
        }

        private WorkflowMVDataService(Guid instanceID)
        {
            _instanceID = instanceID;
            MVDataConnector.MVDataService = this;
        }

        ~WorkflowMVDataService()
        {
            // Clean up
            _workflowRuntime = null;
            _dataExchangeService = null;
            _dataConnector = null;
        }
            
        public DataSet Read()
        {
            return _dataConnector.MVData;
        }

        public void RaiseMVDataUpdateEvent()
        {
            if (_workflowRuntime == null)
                _workflowRuntime = new WorkflowRuntime();

            _workflowRuntime.GetWorkflow(_instanceID); // loads persisted workflow instances
            if (MVDataUpdate != null)
            {
                MVDataUpdate(this, new MVDataAvailableArgs(_instanceID));
            } // if
        }
    }
}

Creating and Using Custom External Data Service Activities

The reader should read the section below, although all you have to do is unzip the files into a folder, open up MVDataCheckerCompleted, and click the solution file. This will build the application. But below are some of the steps that I followed. The reader should try and get acquainted with the wca.exe tool.

The wca.exe tool can be used to interpret the data-transfer interface, marked with the ExternalDataExchange attribute, to automatically generate activities. If you click “new folder” and name it for these files to be extracted to in your c:\Users\name\documents\visual studio 2008\projects\new folder, you can then use the command:

C:\Users\Dave\Documents\Microsoft Visual Studio 2008\Project\DMV\MVDataChecker Completed\
   MVDataService\bin\Debug>"c:\program files\microsoft sdks\windows\
   v6.0a\bin\wca.exe" MvDataService.dll
Processing interface 'MVDataService.IMVDataService'
  Generating InvokeMethod for 'MVDataUpdate'
Wrote file: c:\Users\Dave\Documents\ Microsoft Visual Studio 2008\Project\DMV\\
                 MVDataChecker Completed\MVDataService\bin\Debug\IMVDataService.Invokes.cs
Wrote file: c:\Users\Dave\Documents\ Microsoft Visual Studio 2008\Project\DMV\\
                 MVDataChecker Completed\MVDataService\bin\Debug\IMVDataService.Sinks.cs
Done processing.  Exiting.

Now, we want to rename IMVDataService.Invokes.cs by using the command shell “rename” command: rename IMVDataService.Invokes.cs MVDataUpdate.cs.

And then, move the file with the move command: >move MVDataUpdate.cs ..\..\..\MVWorkFlow. When you return (while building this manually), right-click the MVWorkflow project’s tree node and click “Add Existing Item”. Now, add the MVDataUpdate.cs file. Compile the MVWorkflow project. If you have succeeded, then you have an MVDataUpdate activity in the toolbox:

3.JPG

Adding the workflow external data service to our host application

Here is the code for the form. Notice the event handler that hooks the returned data event:

C#
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Text;
using System.Windows.Forms;
using System.Workflow.Runtime;

namespace MVDataChecker
{
    public partial class Form1 : Form
    {
        // Our workflow runtime instance
        WorkflowRuntime _workflowRuntime = null;

        // Currently executing workflow instance (we'll only have
        // one).
        WorkflowInstance _workflowInstance = null;

        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            // Initialize the driver combobox
            cmbDriver.SelectedItem = cmbDriver.Items[0];

            // Create an instance of the workflow runtime
            _workflowRuntime = WorkflowFactory.GetWorkflowRuntime();
            _workflowRuntime.WorkflowTerminated += 
                new EventHandler<workflowterminatedeventargs>(
                workflowRuntime_WorkflowTerminated);
            _workflowRuntime.WorkflowCompleted += 
                new EventHandler<workflowcompletedeventargs>(
                workflowRuntime_WorkflowCompleted);
        }

        void workflowRuntime_WorkflowCompleted(object sender, 
                             WorkflowCompletedEventArgs e)
        {
            // Remove the local data service
            MVDataService.WorkflowMVDataService dataService = 
              MVDataService.WorkflowMVDataService.CreateDataService(
              _workflowInstance.InstanceId, _workflowRuntime);
            dataService.MVDataUpdate -= 
              new EventHandler<mvdataservice.mvdataavailableargs>(
              dataService_MVDataUpdate);

            // Clear instance (for application termination purposes)
            _workflowInstance = null;

            // Update the user interface
            WorkflowCompleted();
        }

        void workflowRuntime_WorkflowTerminated(object sender, WorkflowTerminatedEventArgs e)
        {
            // Remove the local data service
            MVDataService.WorkflowMVDataService dataService = 
                MVDataService.WorkflowMVDataService.CreateDataService(
                _workflowInstance.InstanceId, _workflowRuntime);
            dataService.MVDataUpdate -= 
                new EventHandler<mvdataservice.mvdataavailableargs>(
                dataService_MVDataUpdate);

            // Clear instance (for application termination purposes)
            _workflowInstance = null;

            // Some error...
            MessageBox.Show(String.Format("Your vehicle record search was " + 
                            "terminated! Error: {0}", e.Exception.Message));

            // Update the user interface
            WorkflowCompleted();
        }

        private void cmdRetrieve_Click(object sender, EventArgs e)
        {
            // Disable the search button
            cmdRetrieve.Enabled = false;

            // We've selected a driver, so disable the driver selection
            // combobox
            cmbDriver.Enabled = false;

            // Clear the lists
            lvVehicles.Items.Clear();
            lvViolations.Items.Clear();

            // Show the search controls
            lblSearching.Visible = true;
            pbSearching.Visible = true;

            // Set the cursor to "app starting"
            Cursor = Cursors.AppStarting;

            // Process the request, starting by creating the parameters
            Dictionary<string,> parms = new Dictionary<string,>();
            parms.Add("DriverName", cmbDriver.SelectedItem);

            // Create instance.
            _workflowInstance = 
              _workflowRuntime.CreateWorkflow(typeof(MVWorkflow.Workflow1), parms);

            // Hook returned data event
            MVDataService.WorkflowMVDataService dataService = 
              MVDataService.WorkflowMVDataService.CreateDataService(
              _workflowInstance.InstanceId, _workflowRuntime);
            dataService.MVDataUpdate += 
              new EventHandler<mvdataservice.mvdataavailableargs>(dataService_MVDataUpdate);

            // Start instance.
            _workflowInstance.Start();
        }

        void dataService_MVDataUpdate(object sender, MVDataService.MVDataAvailableArgs e)
        {
            IAsyncResult result = this.BeginInvoke(
                new EventHandler(
                       delegate
                       {
                           // Retrieve connection. Note we could simply cast the sender as
                           // our data service, but we'll instead be sure to retrieve
                           // the data meant for this particular workflow instance.
                           MVDataService.WorkflowMVDataService dataService =
                               MVDataService.WorkflowMVDataService.
                               GetRegisteredWorkflowDataService(e.InstanceId);

                           // Read the motor vehicle data
                           DataSet ds = dataService.Read();

                           // Bind the vehicles list to the vehicles table
                           ListViewItem lvi = null;
                           foreach (DataRow row in ds.Tables["Vehicles"].Rows)
                           {
                               // Create the string array
                               string[] items = new string[4];
                               items[0] = (string)row["Plate"];
                               items[1] = (string)row["Make"];
                               items[2] = (string)row["Model"];
                               items[3] = (string)row["Color"];

                               // Create the list item
                               lvi = new ListViewItem(items);

                               // Add to the list
                               lvVehicles.Items.Add(lvi);
                           } // foreach

                           // Bind the violations list to the violations table
                           foreach (DataRow row in ds.Tables["Violations"].Rows)
                           {
                               // Create the string array
                               string[] items = new string[4];
                               items[0] = (string)row["ID"];
                               items[1] = (string)row["Plate"];
                               items[2] = (string)row["Violation"];
                               items[3] = ((DateTime)row["Date"]).ToString("MM/dd/yyyy");

                               // Create the list item
                               lvi = new ListViewItem(items);

                               // Add to the list
                               lvViolations.Items.Add(lvi);
                           } // foreach
                       } // delegate
                    ), null, null
            ); // BeginInvoke
            this.EndInvoke(result);

            // Reset for next request
            WorkflowCompleted();
        }

        private void cmdQuit_Click(object sender, EventArgs e)
        {
            // Check for executing workflows
            if (_workflowInstance != null)
            {
                // Cease processing
                _workflowInstance.Abort();
            } // if

            // Quit...
            Application.Exit();
        }

        private delegate void WorkflowCompletedDelegate();

        private void WorkflowCompleted()
        {
            if (this.InvokeRequired)
            {
                // Wrong thread, so switch to the UI thread...
                WorkflowCompletedDelegate d = delegate() { WorkflowCompleted(); };
                this.Invoke(d);
            } // if
            else
            {
                // Hide the search controls
                lblSearching.Visible = false;
                pbSearching.Visible = false;

                // Reset the cursor
                Cursor = Cursors.Arrow;

                // Enable the driver selection combobox
                cmbDriver.Enabled = true;

                // Enable the search button
                cmdRetrieve.Enabled = true;
            } // else
        }
    }
}

Admittedly, this data transfer example does not exemplify a bidirectional data transfer, but rather exemplifies calling data and methods from an external source. One common attribute should be clear, however, and that is there was one to one correspondence between the application and its workflow instance. If you communicate with a workflow instance, you do so with assurance that any data that passes between the application and the workflow should not be confused in any way: one application, one workflow. Other, more advanced cases are possible, however. In my limited knowledge, I have to state that in order to gain a sharper focus on the Windows Workflow Foundation, refer to the referenced material.

References

  • Windows Workflow Foundation Step by Step, by Kenn Scribner.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer Monroe Community
United States United States
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
Questionto enable a "going" concern Pin
egalt22-Apr-15 5:20
egalt22-Apr-15 5:20 
GeneralMy vote of 5 Pin
Kanasz Robert5-Nov-12 0:41
professionalKanasz Robert5-Nov-12 0:41 
QuestionWWF4 project? Pin
John Ortiz14-Jul-10 12:56
John Ortiz14-Jul-10 12:56 
GeneralWell written Pin
Michael Brookie21-Jun-09 2:37
Michael Brookie21-Jun-09 2:37 
GeneralRe: Well written Pin
logicchild22-Jun-09 18:52
professionallogicchild22-Jun-09 18:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.