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.
TurnstileStateMachine Workflow
In project "BasicWorkflows.csproj", you will findTurnstileStateMachine.xaml:
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.
Testing
[Fact]
public void TestStateMachineWorkflow()
{
var a = new TurnstileStateMachine();
var app = new WorkflowApplication(a);
app.InstanceStore = WFDefinitionStore.Instance.Store;
app.PersistableIdle = (eventArgs) =>
{
return PersistableIdleAction.None;
};
var id = app.Id;
app.Run();
Thread.Sleep(200);
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);
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
[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;
};
var id = app.Id;
app.Extensions.Add(new Fonlow.Utilities.TraceWriter());
var stp = new StatusTrackingParticipant();
app.Extensions.Add(stp);
app.Run();
Thread.Sleep(200);
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);
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:
- Visual Studio 2015 Update 1 or Visual Studio 2013 Update 4
- xUnit (included)
- EssentialDiagnostics (included)
- FonlowTesting (included)
- 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: