Click here to Skip to main content
15,885,860 members
Articles / Programming Languages / C#
Article

Windows Workflow Foundation ASP.NET State Machine

Rate me:
Please Sign up or sign in to vote.
4.00/5 (17 votes)
12 May 2006CPOL5 min read 154.2K   1.5K   46   23
Single Page State Machine workflow

Introduction

Windows Workflow Foundation (WF) implements workflow design patterns to solve real-world problems. In particular, State Machine models interaction between an external event and internal state transition,which is very similar to Web Page Flow. In this article, I will build a Web Site Registration System to illustrate how to implement an ASP.NET based WF State Machine. (For additional information, you may read Don Box and Dharma Shukla, Jon Flanders, Dino Esposito.)

Real-World Problem Described

A financial Web site has three types of users trying to register: Casual users without any business relationship, Owners of contracts and agents of owners of contracts. Casual Users directly go to login data entry page while the other two need to go through a validation process before hitting login Data Entry page. If validation fails, the workflow will end. 

ISMConnector -- WF State Machine Connector Interface

State Machine does just one thing: waits for a specific set of events to arrive. When an event does arrive, State Machine will do some back-office processing and move to the next state to wait for another event. So it is not surprising to see the following Interface for communication to a State Machine (SM) with four events and correlations to the next state:

C#
[ExternalDataExchange]
[CorrelationParameter("nextStage")]
public interface ISMConnector
{
    [CorrelationAlias("nextStage", "e.Command")]
    event EventHandler<RegEventArgs> RegTypeSelected;
    [CorrelationAlias("nextStage", "e.Command")]
    event EventHandler<RegEventArgs> AgentInfoSubmitted;
    [CorrelationAlias("nextStage", "e.Command")]
    event EventHandler<RegEventArgs> OwnerInfoSubmitted;
    [CorrelationAlias("nextStage", "e.Command")]
    event EventHandler<RegEventArgs> RegInfoSubmitted;
    [CorrelationInitializer()]
    void SMSnapshot( string nextStage);
}

As always, attributes are hints to compilers to generate extra code or to runtime to setup context. Specifically, [ExternalDataExchange] asks WF tool wca.exe to consume this interface and generate the following activities:

  1. Four eventSinks (HandleExternalEventActivity) for one-way communication into State Machine. Also Registration Data (such as AgentID, SSN, Address) are passed as part of RegEventArgs.
  2. One CallExternalMethodActivity to be inserted just before each event sink to take a snapshot of State Machine.

Note that [CorrelationInitializer()] on SMSnapshot() mandates WF Runtime to set function parameter "nextStage" when executing snapshot activity, while the other correlation related attributes hint the compiler to generate property such as the following:

C#
[System.Workflow.ComponentModel.Compiler.ValidationOptionAttribute(...)]
public string nextStage {
get ..

set..
}


This property maps to EventArgs property "Command".  These "compiler hints" are mostly confusing because they not only generate code but also require correctly setting properties using WF IDE designer in the following section.

Registration State Machine

This State Machine has three states: WaitingRegTypeSelection, WaitingValidationSubmit and WaitingRegInfoSubmit. Each state has one  "SnapShot" Container Activity and one or more event sinks container activities:

RegSMWorkflowDiagram

Each Event sink moves the State Machine into another state by "SetState" Activity after some processing:

AgentInfoSubmitted

If validation fails, "Error Out" activity will be dynamically inserted and the State Machine will end:

C#
WorkflowChanges wfc = new WorkflowChanges(this);
StateActivity sa = wfc.TransientWorkflow.Activities["WaitingRegInfoSubmit"] 
				as StateActivity;
SetStateActivity ssa = new SetStateActivity();
ssa.TargetStateName = "ErrorOut";
   `(sa.Activities["initializeWaitingRegInfoSubmit"] 
		as StateInitializationActivity).Activities.Insert(0, ssa);
ValidationErrorCollection err = wfc.Validate();
this.ApplyWorkflowChanges(wfc);

Note that properties on activities such as "nextStage", "Command" must match the intention of correlation attributes by setting itself to RegistrationMachine.NextStage property through IDE property grid. Each setting is scoped to the state hosting the activity by choosing "Correlation Token" to be owned by a corresponding state through IDE property grid as well.

Up to this point, we merely set up objects, component and attributes through IDE designer and code generation tools. To load these objects into memory for execution, we need to set up WF runtime environment and services.

WF Runtime vs. WF Instance

WinFx requires that each AppDomain can have only one WF runtime. This requirement is easy to meet for Windows Forms or Console Application, since one user interacts with just one AppDomain and a different user interacts with a different AppDomain; by embedding one WF runtime in the unique AppDomain, all workflows will be isolated.

For ASP.NET Web site or Web application, we no longer can create one AppDomain per user or per page request since all pages mostly share an AppDomain and therefore a single WF runtime. Specifically, we write the following HttpModule to create a single WF runtime for all workflows in the Web site /Web application to use:

C#
public class WorkflowHost : IHttpModule
{
    ......
    public static WorkflowRuntime RuntimeWithServices
    {
	get {
	.....
	wr = new WorkflowRuntime();
	wr.AddService(new ManualWorkflowSchedulerService());
	ExternalDataExchangeService de = new ExternalDataExchangeService();
	wr.AddService(de);
	de.AddService(new JQD.LocalService()); 
	wr.StartRuntime();
	.....
    }
    ......
}

But clearly, users still need to isolate their Workflow instance so that State Transition will not trash each other. We accomplish this goal by writing the following code in each page's load event handler:

C#
WorkflowRuntime wr;
WorkflowInstance wi;
ManualWorkflowSchedulerService ms;
   protected void Page_Load(object sender, EventArgs e)
{
    .....
    wr = JQD.WorkflowHost.RuntimeWithServices;
    wi = wr.CreateWorkflow(typeof(RegMachineWorkflow.RegStateMachine));
    ms = wr.GetService<ManualWorkflowSchedulerService>();
    ms.RunWorkflow(wi.InstanceId);
    ...
}

Note that we used ManualWorkflowScheduler to run Workflow instance, rather than directly call Start() method on each WF instance. This will guarantee that different user page requests will be "serialized" by the scheduler  to avoid collision. In this manner, we have achieved isolation of users and page requests even we have just one AppDomain hosting WF Runtime.
 
Finally, I have implemented ISMConnector in class JQD.LocalService and use it to raise event behind page button click event handler and receive "SnapShot" in Page PreRender Event Handler.

About Sample Code Download

You need to install the following to run the sample solution: .NET 2.0, Visual Studio.NET 2005 professional, WinFx runtime and Windows SDK, Orcas WinFx Development Tool, VS.NET Extension for Windows Workflow Foundation.
All these are Feb 2006 CTP and as of May 12, 2006, you can still download parts of the required software here.

The sample solution is made up of one Website (RegWebSite), one WF State Machine Library project (RegMachineWorkflow) and one Class Library Project (LocalService).

SolutionExplorer

LocalService project has a build event to run wca.exe tool and copy generated code to the upper directory. You can  run multiple instances of Browser for registration and they will hit the same WF runtime but different WF instances. And upon completion or error condition, you will return to the first step to try out more. Note that if you hit the back button and try to re-execute the previous step, you will get an error due to "miss-step". I did not handle exception for simplicity.

Conclusion

I demonstrated to you how to model an ASP.NET State Machine by visualizing through the concept of Connector, SnapShot and EventSink. Hopefully, this article will help you to solve real-world problems using WF. 

License

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


Written By
Web Developer
United States United States
I am a Microsoft Certified Application Developer (MCAD), currently focusing on using .Net Framework to develop Business Solutions. I am mostly language neutral. I have used C, C++, ATL, MFC, VB.Net, C#, VB 6, PL/SQL, Transact SQL, ASP, Fortran, etc.

Comments and Discussions

 
GeneralWorkflow Application Pin
vkkishore_s17-Mar-09 3:43
vkkishore_s17-Mar-09 3:43 
Questionworkflow persistence store error Pin
Ni Na10-Mar-08 22:50
Ni Na10-Mar-08 22:50 
GeneralRe: workflow persistence store error Pin
jqd200116-Mar-08 6:56
jqd200116-Mar-08 6:56 
QuestionWca.exe Pin
Hardy Wang12-Jun-07 7:27
Hardy Wang12-Jun-07 7:27 
AnswerRe: Wca.exe Pin
jqd200113-Jun-07 9:21
jqd200113-Jun-07 9:21 
GeneralSome post the following but did not show up Pin
jqd200115-Feb-07 12:24
jqd200115-Feb-07 12:24 
GeneralRe: Some post the following but did not show up Pin
jqd200115-Feb-07 12:25
jqd200115-Feb-07 12:25 
GeneralAplication architecture Pin
frun29-Jan-07 2:26
frun29-Jan-07 2:26 
GeneralRe: Aplication architecture Pin
jqd200129-Jan-07 8:37
jqd200129-Jan-07 8:37 
GeneralRe: Aplication architecture Pin
frun30-Jan-07 0:42
frun30-Jan-07 0:42 
GeneralRe: Aplication architecture [modified] Pin
jqd200130-Jan-07 17:43
jqd200130-Jan-07 17:43 
GeneralIs this solution still valid Pin
Dewey21-Jan-07 14:52
Dewey21-Jan-07 14:52 
GeneralRe: Is this solution still valid Pin
jqd200121-Jan-07 17:50
jqd200121-Jan-07 17:50 
GeneralRe: Is this solution still valid Pin
Dewey21-Jan-07 23:06
Dewey21-Jan-07 23:06 
GeneralRe: Is this solution still valid Pin
jqd200125-Jan-07 5:52
jqd200125-Jan-07 5:52 
GeneralClarification -- regarding &quot;Workflow Serialization&quot; [modified] Pin
jqd20012-Jun-06 18:26
jqd20012-Jun-06 18:26 
GeneralWorkflows not serialized Pin
robert.chaplin+junk@gmail.com25-May-06 20:28
robert.chaplin+junk@gmail.com25-May-06 20:28 
GeneralRe: Workflows not serialized [modified] Pin
jqd200126-May-06 17:50
jqd200126-May-06 17:50 
GeneralRe: Workflows not serialized [modified] Pin
robert.chaplin+junk@gmail.com1-Jun-06 13:01
robert.chaplin+junk@gmail.com1-Jun-06 13:01 
If I take out the Sleep() the results are the same.

Besides, I don't think sleep==idle, because my workflows containing Thread.Sleep() continue to block on RunWorkflow(), which normally returns even when a manually scheduled workflow becomes idle.

My workflow has three steps which print "Hello 1", "Hello 2" and "Hello 3" along with the WorkflowInstanceId. I'm getting output like this when I launch three workflows on separate threads using the manual scheduler.

5838e9fe-e82a-4371-af68-74fd2d6905e4 Hello 1
de5c8e26-e105-4f73-8858-766009e18eae Hello 1
de5c8e26-e105-4f73-8858-766009e18eae Hello 2
b6fa45a5-48d8-4b46-b657-f24e422e121b Hello 1
5838e9fe-e82a-4371-af68-74fd2d6905e4 Hello 2
5838e9fe-e82a-4371-af68-74fd2d6905e4 Hello 3
de5c8e26-e105-4f73-8858-766009e18eae Hello 3
b6fa45a5-48d8-4b46-b657-f24e422e121b Hello 2
b6fa45a5-48d8-4b46-b657-f24e422e121b Hello 3


The only way I can force one workflow to block another is with an explicit lock around RunWorkflow():
lock (typeof(Program))
{
    _scheduler.RunWorkflow(instance.InstanceId);
}


As for the Microsoft documentation, I agree that it is unclear, but I think it must mean "blocks the execution of the host application [thread]", not "blocks the execution of [all other workflows in] the host application".

I have a hyperthreaded processor, which could make a difference. I'm using WF beta 2.2.

Try it yourself. I've pasted my code below.

Great article otherwise. Very useful. The fact that threads in WF work this way is probably a good thing, otherwise web performance would really suffer if all requests were forced to be serialized by WF. I guess the important thing is to be aware that manually scheduled workflows can overlap in a multithreaded environment such as ASP.NET and to code accordingly.

Regards,
Bobby

Code for Program.cs:
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading;
using System.Workflow.Runtime;
using System.Workflow.Runtime.Hosting;

namespace WorkflowConsoleApplication2
{
    class Program
    {
        static WorkflowRuntime _runtime;
        static ManualWorkflowSchedulerService _scheduler;

        static void Main(string[] args)
        {
            _runtime = new WorkflowRuntime();
            _scheduler = new ManualWorkflowSchedulerService();
            _runtime.AddService(_scheduler);
            _runtime.StartRuntime();

            // Start 1st workflow instance on another thread.
            ThreadPool.QueueUserWorkItem(new WaitCallback(StartWorkflow));

            // Start 2nd workflow instance on another thread.
            ThreadPool.QueueUserWorkItem(new WaitCallback(StartWorkflow));

            // Start 3rd workflow instance on this thread.
            StartWorkflow(null);

            Console.Read();
        }

        static void StartWorkflow(Object stateInfo)
        {
            WorkflowInstance instance = _runtime.CreateWorkflow(typeof(WorkflowConsoleApplication2.Workflow1));
            instance.Start();
            _scheduler.RunWorkflow(instance.InstanceId);
        }
    }
}


Code for Workflow1.cs:
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;
using System.Threading;

namespace WorkflowConsoleApplication2
{
	public sealed partial class Workflow1: SequentialWorkflowActivity
	{
		public Workflow1()
		{
			InitializeComponent();
		}

        private void codeActivity1_ExecuteCode(object sender, EventArgs e)
        {
            Console.WriteLine("{0} Hello 1", WorkflowInstanceId);
        }

        private void codeActivity2_ExecuteCode(object sender, EventArgs e)
        {
            Console.WriteLine("{0} Hello 2", WorkflowInstanceId);
        }

        private void codeActivity3_ExecuteCode(object sender, EventArgs e)
        {
            Console.WriteLine("{0} Hello 3", WorkflowInstanceId);
        }
	}

}


Code for Workflow1.designer.cs:
using System;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Collections;
using System.Drawing;
using System.Reflection;
using System.Workflow.ComponentModel.Compiler;
using System.Workflow.ComponentModel.Serialization;
using System.Workflow.ComponentModel;
using System.Workflow.ComponentModel.Design;
using System.Workflow.Runtime;
using System.Workflow.Activities;
using System.Workflow.Activities.Rules;

namespace WorkflowConsoleApplication2
{
	partial class Workflow1
	{
		#region Designer generated code
		
		/// <summary> 
		/// Required method for Designer support - do not modify 
		/// the contents of this method with the code editor.
		/// </summary>
        [System.Diagnostics.DebuggerNonUserCode]
		private void InitializeComponent()
		{
            this.CanModifyActivities = true;
            this.codeActivity3 = new System.Workflow.Activities.CodeActivity();
            this.codeActivity2 = new System.Workflow.Activities.CodeActivity();
            this.codeActivity1 = new System.Workflow.Activities.CodeActivity();
            // 
            // codeActivity3
            // 
            this.codeActivity3.Name = "codeActivity3";
            this.codeActivity3.ExecuteCode += new System.EventHandler(this.codeActivity3_ExecuteCode);
            // 
            // codeActivity2
            // 
            this.codeActivity2.Name = "codeActivity2";
            this.codeActivity2.ExecuteCode += new System.EventHandler(this.codeActivity2_ExecuteCode);
            // 
            // codeActivity1
            // 
            this.codeActivity1.Name = "codeActivity1";
            this.codeActivity1.ExecuteCode += new System.EventHandler(this.codeActivity1_ExecuteCode);
            // 
            // Workflow1
            // 
            this.Activities.Add(this.codeActivity1);
            this.Activities.Add(this.codeActivity2);
            this.Activities.Add(this.codeActivity3);
            this.Name = "Workflow1";
            this.CanModifyActivities = false;

		}

		#endregion

        private CodeActivity codeActivity2;
        private CodeActivity codeActivity3;
        private CodeActivity codeActivity1;
    }
}



-- modified at 19:52 Thursday 1st June, 2006
GeneralRe: Workflows not serialized Pin
luhur24-Aug-07 3:50
luhur24-Aug-07 3:50 
GeneralGain runtime error Pin
ruddy.tw18-May-06 23:09
ruddy.tw18-May-06 23:09 
GeneralRe: Gain runtime error Pin
jqd200120-May-06 15:11
jqd200120-May-06 15:11 
GeneralRe: Gain runtime error Pin
4th1-Jun-06 4:00
4th1-Jun-06 4:00 

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.