Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

Learn Windows Workflow Foundation 4.5 through Unit Testing: StateMachine

5.00/5 (4 votes)
30 Apr 2016CPOL3 min read 14.3K  
StateMachine for State Machine

Background

State machine is one of the important data structure concepts in computing science. I believe all people with a computing science degree or software engineering degree must have studied state machine in one of the entry courses, since I had taught these data structures for fresh grade students in a university many years ago.

However, in my career as a full stack developer in the commercial world, so far I haven't done one commercial project that utilizes state machine, partly because there are many alternatives for common business applications that need to manage (finite) states: logical trees with IF and SWITCH statements, recursion, as well as logic table. In some projects, I was tempted to design with state machine because of good candidates for state machine, however, I did not want to write my own state machine framework, and also considered such codes with state machine may not be so maintainable in the eyes of developers who had not CS or SE degrees.

Nevertheless, state machine is out of the box from Windows Workflow Foundation since v3.5, and got mature/abstract enough in v4.5. I would be looking forward to using it in next project in which state machine could shine. When googling around, I found very little examples of using state machine, most of which are outdated with classes in WF 3.5 and 4.0, marked obsolete. So I wrote these example to explore the conveniences of StateMachine in WF 4.5.

Introduction

If you had never studied state machine and would be interested in it, the Wikipedia entry Finite-state machine could be good starting point. And StateMachine in WF 4.5 is matured and abstract enough for writing state machine codes easy to maintain. And I would use coin-operated turnstile as an example.

File:Torniqueterevolution.jpg File:Turnstile state machine colored.svg

 

TurnstileStateMachine Workflow

In project "BasicWorkflows.csproj", you will findTurnstileStateMachine.xaml:

Image 3

As you can see, the abstraction and the implementation are all in one through a visual model driven approach.

And the trigger is done through a bookmark call with class Wakeup.

Image 4

Testing

C#
[Fact]
public void TestStateMachineWorkflow()
{
    var a = new TurnstileStateMachine();

    var app = new WorkflowApplication(a);
    app.InstanceStore = WFDefinitionStore.Instance.Store;
    app.PersistableIdle = (eventArgs) =>
    {
        return PersistableIdleAction.None; //Must be None so 1 application instance will do all states
    };

    var id = app.Id;

    app.Run();

    Thread.Sleep(200); //Run and ResumeBookmark are all non blocking asynchronous calls, better to wait prior operation to finish.

    var br = app.ResumeBookmark("coin", null);
    Assert.Equal(BookmarkResumptionResult.Success, br);
    Thread.Sleep(200);

    br = app.ResumeBookmark("coin", null);
    Assert.Equal(BookmarkResumptionResult.Success, br);
    Thread.Sleep(200);

    br = app.ResumeBookmark("push", null);
    Assert.Equal(BookmarkResumptionResult.Success, br);
    Thread.Sleep(200);

    br = app.ResumeBookmark("push", null);
    Assert.Equal(BookmarkResumptionResult.Success, br);
    Thread.Sleep(200);

    br = app.ResumeBookmark("funky", null);
    Assert.Equal(BookmarkResumptionResult.NotFound, br);
    Thread.Sleep(200);

    br = app.ResumeBookmark("coin", null);
    Assert.Equal(BookmarkResumptionResult.Success, br);
    Thread.Sleep(200);//ResumeBookmark is asynchrounous in a new thread, so better to wait, otherwise they got killed when app.Cancel is executed.

    app.Cancel();
}

 

StateMachine Activity

In addition to drawing a workflow of state machine, you could define the definition of a state machine through codes during the instantiation of the StateMachine class.

Example

C#
    [Fact]
    public void TestTurnstile()
    {
        var lockedState = new State() { DisplayName = "Locked" };
        var unlockedState = new State() { DisplayName = "Unlocked" };

        var insertCoinWhenLocked = new Transition()
        {
            To = unlockedState,
            Trigger = new Wakeup() { BookmarkName = "coin" },
        };

        var insertCoinWhenUnlocked = new Transition()
        {
            To = unlockedState,
            Trigger = new Wakeup() { BookmarkName = "coin" },
        };

        var pushWhenLocked = new Transition()
        {
            To = lockedState,
            Trigger = new Wakeup() { BookmarkName = "push" },
        };

        var pushWhenUnlocked = new Transition()
        {
            To = lockedState,
            Trigger = new Wakeup() { BookmarkName = "push" },
        };

        lockedState.Transitions.Add(pushWhenLocked);
        lockedState.Transitions.Add(insertCoinWhenLocked);

        unlockedState.Transitions.Add(pushWhenUnlocked);
        unlockedState.Transitions.Add(insertCoinWhenUnlocked);

        var a = new StateMachine()
        {
            InitialState = lockedState,
        };

        a.States.Add(lockedState);
        a.States.Add(unlockedState);

        var app = new WorkflowApplication(a);
        app.InstanceStore = WFDefinitionStore.Instance.Store;
        app.PersistableIdle = (eventArgs) =>
        {
            return PersistableIdleAction.None; //Must be None so 1 application instance will do all states
        };

        var id = app.Id;

        app.Extensions.Add(new Fonlow.Utilities.TraceWriter());

        var stp = new StatusTrackingParticipant();
        app.Extensions.Add(stp);

        app.Run();

        Thread.Sleep(200); //Run and ResumeBookmark are all non blocking asynchronous calls, better to wait prior operation to finish.

        var br = app.ResumeBookmark("coin", null);
        Assert.Equal(BookmarkResumptionResult.Success, br);
        Thread.Sleep(200);
        Assert.Equal("Unlocked", stp.StateName);

        br = app.ResumeBookmark("coin", null);
        Assert.Equal(BookmarkResumptionResult.Success, br);
        Thread.Sleep(200);
        Assert.Equal("Unlocked", stp.StateName);

        br = app.ResumeBookmark("push", null);
        Assert.Equal(BookmarkResumptionResult.Success, br);
        Thread.Sleep(200);
        Assert.Equal("Locked", stp.StateName);

        br = app.ResumeBookmark("push", null);
        Assert.Equal(BookmarkResumptionResult.Success, br);
        Thread.Sleep(200);
        Assert.Equal("Locked", stp.StateName);

        br = app.ResumeBookmark("funky", null);
        Assert.Equal(BookmarkResumptionResult.NotFound, br);
        Thread.Sleep(200);
        Assert.Equal("Locked", stp.StateName);

        br = app.ResumeBookmark("coin", null);
        Assert.Equal(BookmarkResumptionResult.Success, br);
        Thread.Sleep(200);//ResumeBookmark is asynchrounous in a new thread, so better to wait, otherwise they got killed when app.Cancel is executed.
        Assert.Equal("Unlocked", stp.StateName);

        app.Cancel();
    }

public class StatusTrackingParticipant : System.Activities.Tracking.TrackingParticipant
{
    public string StateName { get; private set; }

    protected override void Track(TrackingRecord record, TimeSpan timeout)
    {
        var stateMachineStateRecord = record as System.Activities.Statements.Tracking.StateMachineStateRecord;
        if (stateMachineStateRecord == null)
            return;

        StateName = stateMachineStateRecord.StateName;
        Trace.TraceInformation("StateName: " + stateMachineStateRecord.StateName);
    }
}

Hints:

Class StatusTrackingParticipant is based on article: Create a Custom Tracking Participant.

Remarks:

The tracking of the states is probably not needed in real world projects, since the actions in each state could be well enough for representing the state. However, during development or debugging, adding a bit tracking of states could assure better quality.

Using the Code

The source code is available at https://github.com/zijianhuang/WorkflowDemo.

Prerequisites:

  1. Visual Studio 2015 Update 1 or Visual Studio 2013 Update 4
  2. xUnit (included)
  3. EssentialDiagnostics (included)
  4. FonlowTesting (included)
  5. Workflow Persistence SQL database, with default local database WF.

Examples in this article are from a test class:  StateMachineTests.

Points of Interest

If you google "Workflow Foundation State Machine" etc., you may find a lot information, however most of which are outdated and even obsolete.

I had also found these articles interesting:

 

 

License

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