Click here to Skip to main content
15,887,175 members
Articles / Programming Languages / C#

State Pattern in C#

Rate me:
Please Sign up or sign in to vote.
4.64/5 (4 votes)
3 Mar 2010CPOL4 min read 58.3K   1K   42   6
A C# implementation of the Gang of Four State Pattern using features like Generics, enumeration, and Reflection to make life easier.

Introduction

This article presents a C# implementation of the Gang of Four State Pattern using features like Generics, enumeration, and Reflection to make life easier. The reader is assumed to be familiar with UML State Charts and the Gang of Four State Pattern; this article is not intended as a tutorial on those subjects.

Motivation

I regularly use UML State Charts to design state behavior. UML State Charts are very useful in discussions with users. When it comes to implementing, I like to use the Gang of Four State Pattern. The pattern enables a clear translation of a State Chart into code. But a couple of things annoyed me with the standard implementations:

  • StateContext needs to be updated in several places when adding concrete states.
  • Intellisense/auto-complete is not really helpful when setting a property of abstract type.
  • Concrete states need constructors with a reference to the StateContext. This contradicts with the clear translation of State Chart into code, and is distracting for the casual reader, especially while state classes can be very small.

These issues are addressed in a neat little package called fsm4net. Let's have a look.

Example

Let's dive in. Suppose we need to implement the following State Chart. We have a light that we need to turn on and off with a toggle event. And furthermore, the light should be turned off after a timeout. This simple example uses states, transitions, triggers, actions, and timeout.

ExampleStateChart.jpg

Using fsm4net, the implementation of our concrete states looks like this (actual code):

C#
enum States { OnState, OffState, EmptyState }

class OffState : StateBase
{
    public override void EntryAction()
    {
        Context.Light.Off();
    }

    protected override void OnToggle()
    {
        Context.Next = States.OnState;
    }
}


class OnState : StateBase
{
    public override void EntryAction()
    {
        Context.Timeout = TimeSpan.FromSeconds(3);
        Context.Light.On();
    }

    protected override void  OnToggle()
    {
        Context.Next = States.OffState;
    }

    public override void TimeoutHandler()
    {
        Context.Next = States.OffState;
    }
}

What I really like about this code is that it translates very well to the State Chart and is also understandable for non-programmers. If you like it so far, then keep reading.

Overview

We'll take a look at the big picture first and then we'll get back to the example.

fsm4netUsage.jpg

This class diagram shows what you get when using fsm4net, i.e., inherit the abstract base classes and interface of the fsm4net assembly. The classes will look familiar if you've seen the State Pattern before. Let's go over them:

StatesEnum is an enumeration that identifies the concrete states in your machine and the empty or final state that is used for termination. It is very convenient while entering state change code to be able to select values from an enum using intellisense/autocomplete.

C#
enum StatesEnum { ConcreteState1, ConcreteState2, Empty }

StateBase is the abstract base class for all your concrete states. The simplest form is:

C#
abstract class StateBase : State<statecontext,>
{
}

StateBase is typically expanded with virtual handlers for specific triggers, as we'll see in the example later on.

StateContext is the bookkeeper. The base constructor takes two parameters: the enum values of the first state and the final state. The simplest form is:

C#
interface IStateContext : IStateContext<statesenum> { }

class StateContext : StateContext<statesenum>, IStateContext
{
    public StateContext() 
        : base(StatesEnum.ConcreteState1, StatesEnum.Empty)
    {
    }
}

StateContext can be reached from every state using their inherited Context property. Typically, StateContext is expanded with properties that allow for either persisting data across states or accessing interfaces from states (Light, in our example).

IstateContext provides an interface for the thread that runs the state machine. The simplest implementation looks like:

C#
IMyStateContext machine = new MyStateContext();
while (machine.IsActive)
{
    machine.Handle();
}

Every time Handle() is called, it will either:

  • Switch state (when a Next state is set) (calling ExitAction() on the current state, and EntryAction() on the new state)
  • Notify timeout (when timeout is elapsed) (calling TimeoutHandler() on the current state)
  • Notify trigger (when a trigger event is queued) (calling TriggerHandler() on the current state)
  • Otherwise: Call DoAction() on the current state

An example of a simple concrete state:

C#
class ConcreteState1 : StateBase
{
    public override void EntryAction()
    {
        Context.Next = States.EmptyState;
    }
}

Look mum, no constructor (distractor)!

Details

The base constructor of StateContext performs all the magic, and uses Reflection to find out which states are implemented; instantiates the concrete states and matches them to StatesEnum. Some rules for the definition of states apply:

  • Every concrete state needs a corresponding enum value of the exact same name.
  • One extra enum value needs to be defined for the exit or terminating state, and cannot have an associated class.
  • No more rules :-)

StateContext contains an EventArgs Queue to allow for thread-safe handling of events / triggers.

C#
void EnqueueTriggerEventHandler(object sender, EventArgs e);

The enqueue method has the EventHandler signature, and can therefore directly subscribe to events. The sender object will be discarded.

StateContext contains a Timeout property of type TimeSpan that can be used for state timeouts. Timeouts are typically set in the EntryAction of a concrete state. Timeouts are reset when state is changed. When a timeout occurs, the TimeoutHandler() on the current state is called. The default action is to throw a NotImplementedException("Unhandled timeout").

Back to the example

We have already seen the concrete state implementations of our example state chart. Now, let's go over the other classes.

ExampleClassDiagram.jpg

IStateContext is the interface for the thread that runs the state machine. All members are inherited:

C#
interface IStateContext : IStateContext<statesenum /> { }</statesenum /> 

StateContext publishes the ILight interface so states can control the light. Furthermore, StateContext subscribes to the OnToggle events:

C#
class StateContext : StateContext<states>, IStateContext
{
    private ILight m_Light;

    public StateContext(ILight light)
        : base(States.OffState, States.EmptyState)
    {
        light.OnToggle += new EventHandler(EnqueueTriggerEventHandler);
        m_Light = light;
    }

    public ILight Light 
    {
        get { return m_Light; }
    }
}

And finally, StateBase will translate the triggers into OnToggle calls.

C#
abstract class StateBase : State<statecontext,>
{
    public override void TriggerHandler(EventArgs e)
    {
        OnToggle();
    }

    protected virtual void OnToggle() { }
}

Our example is very simple. Typically, TriggerHandler will check the type of the argument and possibly its properties to distribute the events to separate method calls.

Conclusion

fsm4net provides a neat way to implement UML State Charts in C# using the GoF State Pattern. There is a good separation between functionality and implementation details, and intellisense / auto-complete really helps with the coding of the states. I hope it will be useful to you too.

History

  • 1.00: Initial release.

License

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


Written By
Software Developer (Senior) PROMEXX Technical Automation B.V.
Netherlands Netherlands
Senior Software engineer specialized in embedded software and machine control. Like to play my guitars or work in the studio.

Comments and Discussions

 
GeneralMy vote of 5 Pin
Jerry van Kranenburg22-Jan-13 23:19
Jerry van Kranenburg22-Jan-13 23:19 
QuestionThank you for this article Pin
Hyland Computer Systems30-Jul-12 10:48
Hyland Computer Systems30-Jul-12 10:48 
Questiondatabase implementation...? Pin
ColinBashBash3-Mar-10 8:33
ColinBashBash3-Mar-10 8:33 
Ok, I liked the idea of having a generic implementation of a FSM, but... Smile | :)

1) I really dislike the idea of having a process that continually 'polls' the status... it feels like it should be more event driven. ('dislike' isn't much of an objection)
2) Most of my projects would have dynamic states with static events, with the information saved in the database.

So, I drew up a draft of how I would do it... but I'm not sure that I'm keeping everything in mind. Can you send me an email to discuss?

Interfaces / Abstract Classes
public interface IStateEvent<T> { }
public interface IState<T> { }
public interface IStateAction<T> {
    bool DoTestAndMove(T o);
}
public abstract class StateManager<T> {
    public void ProcessEvent(T myObject, IStateEvent<T> eventOccurred) {
        foreach (IStateAction<T> action in GetActionsByStateAndEvent(GetObjectCurrentState(myObject), eventOccurred)) {
            if (action.DoTestAndMove(myObject)) {
                return;
            }
        }
    }
    public void ProcessUnattended(params IStateEvent<T>[] unattendedEvents) {
        foreach (IStateEvent<T> unattendedEvent in unattendedEvents) {
            foreach (T myObject in GetObjectsForPossibleAction(unattendedEvent)) {
                foreach (IStateAction<T> action in GetActionsByStateAndEvent(GetObjectCurrentState(myObject), unattendedEvent)) {
                    if (action.DoTestAndMove(myObject)) {
                        return;
                    }
                }
            }
        }
    }
    /// <summary>
    /// select * from StateAction where StartingStateId = ? and StateEventId = ?
    /// </summary>
    /// <param name="state"></param>
    /// <param name="stateEvent"></param>
    /// <returns></returns>
    protected abstract IEnumerable<IStateAction<T>> GetActionsByStateAndEvent(IState<T> state, IStateEvent<T> stateEvent);
    /// <summary>
    /// select * from <typeparamref name="T"/> where StateIsActive = 1 and StateId in (select distinct StartingStateId from StateAction where StateEventId = ?)
    /// </summary>
    /// <param name="stateEvent"></param>
    /// <returns></returns>
    protected abstract IEnumerable<T> GetObjectsForPossibleAction(IStateEvent<T> stateEvent);
    /// <summary>
    /// return myObject.State
    /// </summary>
    /// <param name="myObject"></param>
    /// <returns></returns>
    protected abstract IState<T> GetObjectCurrentState(T myObject);
}


Back-End Database
alter table <Base Table,,> add StateId int
alter table <Base Table,,> add StateIsActive bit

create table State(
  StateId int identity(1,1) not null PRIMARY KEY,
  Name varchar(255) not null,
  IsStartState bit not null default(0),
  IsEndState bit not null default(0)
)

create table StateEvent(
  StateEventId int identity(1,1) not null PRIMARY KEY,
  Name varchar(255) not null
)

create table StateAction(
  StateActionId int identity(1,1) not null PRIMARY KEY,

  StateEventId int not null,
  StartingStateId int not null,
  EndingStateId int not null,

  CriteriaBlob image
)


Implementation Example:
using System.Linq;

enum StateEventEnum {
    ChangeApprovalStatus = 1,
    ChangeData = 2,
    TimePeriodElapsed = 3
}

public class ManufacturingPart {
    private State currentState;
    public State CurrentState {
        get { return currentState; }
    }
}

public class StateEvent : IStateEvent<ManufacturingPart> {
    //int id;
    //string name;
    //StateEventEnum EnumValue { get; }
}

public class State : IState<ManufacturingPart> {
    //int id;
    //string name;
    //bool isStartState, isEndState;
}

public class StateAction : IStateAction<ManufacturingPart> {
    //int id;
    //StateEvent stateEvent;
    //State startState, endState;
    //StateActionCriteria criteria;

    public bool DoTestAndMove(ManufacturingPart o) {
        throw new NotImplementedException();
    }
}

public class ManufacturingPartStateManager : StateManager<ManufacturingPart> {
    protected override IEnumerable<IStateAction<ManufacturingPart>> GetActionsByStateAndEvent(IState<ManufacturingPart> state, IStateEvent<ManufacturingPart> stateEvent) {
        List<StateAction> actions = DataAccess.GetActionsByStateAndEvent(state, stateEvent);
        return actions.Cast<IStateAction<ManufacturingPart>>();
        //3.5 framework
    }

    protected override IEnumerable<ManufacturingPart> GetActiveObjectsForPossibleAction(IStateEvent<ManufacturingPart> stateEvent) {
        List<ManufacturingPart> parts = DataAccess.GetActiveObjectsForPossibleAction(stateEvent);
        return parts;
    }

    protected override IState<ManufacturingPart> GetObjectCurrentState(ManufacturingPart myObject) {
        return myObject.CurrentState;
    }
}

[Serializable]
public abstract class StateActionCriteria {
    public byte[] ToBlob(StateActionCriteria criteria) {
        System.Runtime.Serialization.Formatters.Binary.BinaryFormatter serializer =
            new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
        System.IO.MemoryStream memStream = new System.IO.MemoryStream();
        serializer.Serialize(memStream, criteria);
        return memStream.ToArray();
    }
    static StateActionCriteria FromBlob(byte[] blob) {
        System.IO.MemoryStream memStream = new System.IO.MemoryStream(blob);
        memStream.Position = 0;
        System.Runtime.Serialization.Formatters.Binary.BinaryFormatter deserializer =
            new System.Runtime.Serialization.Formatters.Binary.BinaryFormatter();
        object newobj = deserializer.Deserialize(memStream);
        memStream.Close();
        return (StateActionCriteria)newobj;
    }
}

AnswerRe: database implementation...? Pin
ColinBashBash3-Mar-10 9:07
ColinBashBash3-Mar-10 9:07 
AnswerRe: database implementation...? Pin
Martin de Liefde3-Mar-10 9:30
Martin de Liefde3-Mar-10 9:30 
GeneralRe: database implementation...? Pin
Martin de Liefde3-Mar-10 9:32
Martin de Liefde3-Mar-10 9:32 

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.