Walkthrough: Creating a State Machine using T4 and a State Diagram
Date: Wednesday, January 30, 2013 4:36 PM
Posted by: Nico Ploner
Category: T4
Introduction
In this sample we will walk through a scenario that includes the usage of state diagrams and T4 templates. We will create a sample application that implements an alarm clock based on a state machine. The code for this machine will be generated by a T4 template based on the previously designed state diagram.
This article consists of the following chapters:
You can download the full sample code inside a Visual Studio Solution here: T4-StateMachineSample.zip (60K)
Background
On one hand the idea behind this article is to describe and explain the State Diagram of the the tangible T4 modeling tools. This includes the basic graphical elements and the code classes behind those elements. On the other hand I want to give an intermediate but non-trivial T4 scenario as an example of the power of T4 Templates.
In addition this article will provide a complete and easily adaptable sample for you to download and reuse.
The Application Scenario
It is not easy to find a scenario that fits both requirements: easy enough to fit into an example and complex enough to be non-trivial. So I chose to create a WPF Desktop Application that implements an Alarm Clock that internally is a state machine.
The State Diagram
This is the the state diagram we will be using for the Alarm Clock: It contains a StartPoint element (the black dot) and three states the state machine will transition between depending on user actions. The start point and states are connected via flows (represented as arrows) that indicate from which state the machine can go into what other state. The direction is determined by the direction of the arrows.
A Transition is triggered by a Guard (the text on an arrow before the “/” character) and might result in an Effect (the text after the “/” character). When entering a state, that state may execute some entry actions and when leaving it again, it may execute exit actions.
Usually a state diagram has got an EndPoint, but since our Application does not have and “End”, the state machine will run forever.
The Alarm Clock state machine will start in a state that is equivalent to the start point and wait for the user action “Start” and wait for the user to start the machine. When starting it will notify its environment about the start with an “MachineStarted” effect and transition to the “Unarmed” state.
In this state the user will see the current time and is given the possibility to set an alarm time. Once the user set the alarm time, the machine will go to the “Armed” state. Entering the “Armed” state it will confirm the alarm time in the entry action “DisplayArmedSign” and start an internal timer (“StartTimer”) to evaluate when the alarm is to be set off.
In the “Armed” state the user may disable the alarm which brings the state machine back to the Unarmed state. Otherwise the machine transitions to the “Alarm” state once the alarm time has been matched. On exiting the “Armed” state, the internal timer used in that state will be stopped.
The user can turn the alarm off and the Alarm Clock state machine will go to the “Unarmed” state again.
This scenario covers most of the capabilities a state diagram has.
The Application
The following screenshot displays the UI of the WPF application we are going to create:
In the center of the window is a TextBlock that displays the current time and is updated every second. On the right hand side are some controls that allow the user to interact with the Alarm Clock state machine:
- One Button labeled “1.) Start State Machine” which allows the user to initially start the state machine.
This button is only enabled when the state machine is in the “Start” state. - A TextBox in which the user may enter the alarm time and a Button “2.) Arm AlarmClock” to set the alarm.
Those controls are only enabled when the state machine is in the “Unarmed” state. - A Button labeled “2.) Disarm AlarmClock” which is only enabled when the machine is in the “Armed” state.
- A Button labeled “3.) Turn alarm off”. With this button the user can stop the alarm after it went off. It is only enabled when the state machine is in the “Alarm” state.
Using the code
The code sample consists of multiple steps:
- 1. Create the state diagram
- 2. Create the reusable T4 template that generates all necessary classes for a generic state machine.
- 3. Complete the generated code manually to reach the full functionality needed for the Alarm Clock
- 4. Create the User Interface and connect the state machine to the WPF controls.
I will be using Visual Studio 2012 for the screenshots but this sample works perfectly with Visual Studio 2010, too. You might want to start with a new WPF Application project.
1. Create the state diagram
Add a new tangible State Diagram from the “Add New Item” dialog and name it “AlarmClock.tastate”.
Open the diagram and start dragging Elements from the Toolbox onto the diagram pane. Since every state diagram needs a start point, start by adding a Start Point to the diagram.
The same way drag a new State into the diagram. Change its name in the Property Grid to “Unchanged”.
Connect the Start Point to the “Unchanged” state by selecting the Flow item in the toolbox and dragging an arrow from the Start Point to the “Unchanged” state.
In the Property Grid of the Flow element set the “Event [Guard]” value to “Start” and the “Effect” to “MachineStarted”.
Continue adding States and Flows to the diagram until you complete the Alarm Clock Model.
2. T4 Template for generic state machines
The purpose of the T4 template is to create all necessary classes and general logic in order to create a ready-to-use state machine. Additionally we want to be able to reuse the template for any state machine we will design in the future.
So the first step is to think of basic elements every state machine needs. In the second step the special elements of the specific machine needs to be generated.
For our basic model I decided to create the following structure:
- An abstract class StateMachine that holds the current state of the machine and provides methods to transition between states that have to be implemented by the specific state machine.
- An abstract class StateBase as a base class for each state in the state machine. A state always contains a reference to its state machine to be able to trigger transitions and provides Entry and Exit logic.
- An abstract class TriggerBase. Each Guard in the state diagram will be turned into a trigger and can be passed to the state machine to make it transition between states.
In the specific model for each state in the diagram a partial class that inherits from StateBase will be created and it will contain partial methods for all Entry actions and all Exit actions. Each guard of a flow element will be turned into a partial class that inherits from TriggerBase. Thus the user can extend the logic of the triggers.
Finally a partial class for the state machine will be created and inherit from StateMachine. Based on all Flows the transition logic of the State Machine will be created. Each Effect on a Flow element will be turned into an C#-Event of the state machine class.
Beginning of the Template
Add a new blank T4 template file to your solution next to the AlarmClock.tastate file. Name it “StateMachineTemplate.tt”.
The first lines of the T4 template need to look like this, telling the T4 engine that we are processing a tangible State Diagram in this template. Those lines also provide us with IntelliSense for the State Diagram object model in the template:
<#@ template
inherits="Microsoft.VisualStudio.TextTemplating.VSHost.ModelingTextTransformation"
debug="true" hostSpecific="true" #>
<#@ output extension=".cs" #>
<#@ TangibleStateDiagram processor="TangibleStateDiagramDirectiveProcessor"
requires="fileName='AlarmClock.tastate'"
provides="StateGraph" #>
<#@ assembly name="tangible.Modelling.StateDiagrams.Dsl.10.dll" #>
There are other assemblies and namespaces we are going to use inside the T4 template. Thus we need to add the following lines to the template:
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Collections" #>
<#@ import namespace="System.Collections.Generic" #>
Creating the Base classes
After having all references and namespaces included for developing the T4 template, we can start generating C# code. At first we need to create the base classes TriggerBase, StateBase and StateMachine. To provide all people who are going to use our template with those base classes, we’re going to include them in the template. This is not much of dynamic code generation but rendering text.
So directly after the Template declarations add the static text for the base classes:
Namespace imports
The generated code needs namespace imports, too, so that in can be compiled. Add the following imports to the output code of the T4 template:
using System;
using System.ComponentModel;
TriggerBase class
The TriggerBase class is an abstract class without any functionality. Add the following line of code to the output code of the T4 template:
public abstract class TriggerBase { }
StateBase class
The StateBase class also is an abstract class with a public constructor that takes the instance of a state machine as a parameter. (Don’t worry that we haven’t done anything for a StateMachine class yet. This is just not-compiled text so far.)
The StateBase class has a method “Initialize” that can be implemented by inheriting classes and two methods “OnEntry” and “OnExit” that must be implemented by inheriting classes. Since transitions between states are invoked by triggers, the “OnEntry” and “OnExit” methods take a Trigger as a parameter. (Again, no worries because Triggers don’t exist yet.).
public abstract class StateBase
{
public StateBase(StateMachine machine)
{
this.StateMachine = machine;
this.Initialize();
}
public StateMachine StateMachine { get; private set; }
protected virtual void Initialize()
{
}
public abstract void OnEntry(TriggerBase trigger);
public abstract void OnExit(TriggerBase trigger);
}
StateMachine class
The StateMachine class again is an abstract class providing a public constructor and an Initialization method. It contains a property “CurrentState” that holds an instance of the State class the machine is currently in. The method “TransitionToNewState” allows the state machine to go into another state. This method takes the instance of the new state and the trigger that caused this transition as parameters. The state machine also contains an abstract Method “ProcessTrigger” that will be called from outside the machine to trigger transitions between states.
For convenience the StateMachine class implements the interface INotifyPropertyChanged.
public abstract partial class StateMachine : INotifyPropertyChanged
{
public StateMachine()
{
this.Initialize();
}
protected virtual void Initialize() { }
public void TransitionToNewState(StateBase newState, TriggerBase causedByTrigger)
{
// exit the current state
if (this.CurrentState != null)
this.CurrentState.OnExit(causedByTrigger);
this.CurrentState = newState;
// enter the new state
if (this.CurrentState != null)
this.CurrentState.OnEntry(causedByTrigger);
}
private StateBase _CurrentState;
public StateBase CurrentState
{
get { return _CurrentState; }
private set
{
_CurrentState = value; OnPropertyChanged("CurrentState");
}
}
public abstract void ProcessTrigger(TriggerBase trigger);
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
So far this is no magic. You may try to Transform the template by right clicking it in the Solution explorer and select “Run Custom Tool”. It will generate the classes in C# code and you can compile this code.
Now we need to generate the code that is different depending on the input State Diagram file.
Creating the Triggers
At first we are going to create all triggers that may cause the transition between two states and generate classes that inherit from TriggerBase for each of them. Those generated Trigger classes are going to be partial classes so that they may be customized without losing all manual changes when retransforming the template.
In the State Diagram Guards are a property of the Flow class. Flow elements can only accessed via the State Elements they are connected to. To get a list of all guards we need to query all States and select all ingoing Flows. Since a trigger might trigger more than one transition, we need to assure that we generate each trigger only once.
This time we need to write T4 code blocks. Inside the code block we acquire the trigger names and iterate them in a loop. For each item we generate output code that builds up the resulting class.
T4 code blocks are embraced by <# and #> tags. I’ve marked the output code with a white background color. Printing computed values from the T4 code is marked with <#= and #>.
<# // find all trigger names
var allTriggerNames = new List<string>();
foreach(var state in this.StateGraph.FlowElements.OfType<State>())
foreach(var flow in state.FlowFrom.SourceDomainRole.GetElementLinks<Flow>(state))
if (!allTriggerNames.Contains(flow.Guard))
allTriggerNames.Add(flow.Guard);
// iterate all triggers
foreach(var triggerName in allTriggerNames)
{
#> public partial class <#= triggerName #>Trigger : TriggerBase { }
<# }
#>
If you are going to transform the T4 template now, the code block above will produce the following output C# code:
public partial class DisableAlarmTrigger : TriggerBase { }
public partial class UserTurnedAlarmOffTrigger : TriggerBase { }
public partial class StartTrigger : TriggerBase { }
public partial class SetAlarmTimeTrigger : TriggerBase { }
public partial class AlarmTimeMatchesTrigger : TriggerBase { }
Creating the States
The same way we are going to create all states of the state machine. But this time we need to more code generation – which makes the T4 template code more confusing.
We will iterate all states and find all entry and exit actions. Since we allow multiple actions on entry or exit, we’re going to split the values in the “Entry Action” and “Exit Action” properties by the NewLine-Character and generate partial methods for each of them.
<# // iterate all states of the diagram
foreach(var state in this.StateGraph.FlowElements.OfType<State>())
{
// find all entry and exit actions
var entryActions = state.EntryAction.Split(new string[] { Environment.NewLine },
StringSplitOptions.RemoveEmptyEntries);
var exitActions = state.ExitAction.Split(new string[] { Environment.NewLine },
StringSplitOptions.RemoveEmptyEntries );
// start generating the class
#> public partial class <#= state.Name #>State : StateBase
{
public <#= state.Name #>State(StateMachine machine) : base(machine) { }
#region Entry
public override void OnEntry(TriggerBase causedByTrigger)
{
// call all entry actions
<# // render the method call for each entry action
foreach(var action in entryActions)
{
#> <#= action #>(causedByTrigger);
<# }
#> }
<# // render the method definition for each entry action
foreach(var action in entryActions)
{
#> partial void <#= action #>(TriggerBase causedByTrigger);
<# }
#> #endregion
#region Exit
public override void OnExit(TriggerBase causedByTrigger)
{
// call all exit actions
<# // render the method call for each exit action
foreach(var action in exitActions)
{
#> <#= action #>(causedByTrigger);
<# }
#> }
<# // render the method definition for each exit action
foreach(var action in exitActions)
{
#> partial void <#= action #>(TriggerBase causedByTrigger);
<# }
#> #endregion
}
<# }
The Start State
The initial state of the state machine is represented by the StartPoint Element in the diagram. So this state is not covered in the loop over all states above. We need to provide this class manually outside any T4 code block:
public partial class StartState : StateBase
{
public StartState(StateMachine machine) : base(machine) { }
public override void OnEntry(TriggerBase causedByTrigger) { }
public override void OnExit(TriggerBase causedByTrigger) { }
}
Creating the State machine
Generating the state machine now is straight forward: At first we get a list of all triggers and all effects and start generating the state machine. For each effect we are going to create an EventHandler. The logic of the state machine goes in the ProcessTrigger method: for each state we need to check if the trigger (that has been passed as a parameter) may cause a transition. If so, we need to perform the transition. The Start state has to be processed manually again.
<# // get a (default) name for the state machine
var machineName = !string.IsNullOrEmpty(this.StateGraph.Name)
? this.StateGraph.Name
: "GeneratedStateMachine";
// get all triggers and effects
var allTriggers = new List<string>();
foreach(var state in this.StateGraph.FlowElements.OfType<State>())
foreach(var flow in state.FlowFrom.SourceDomainRole.GetElementLinks<Flow>(state))
if (!allTriggers.Contains(flow.Guard))
allTriggers.Add(flow.Guard);
var allEvents = new List<string>();
foreach(var state in this.StateGraph.FlowElements.OfType<State>())
foreach(var flow in state.FlowFrom.SourceDomainRole.GetElementLinks<Flow>(state))
if (!allEvents.Contains(flow.Guard))
allEvents.Add(flow.Guard);
// start generating the state machine
#> public partial class <#= machineName #> : StateMachine
{
public <#= machineName #>()
{
// initially we go into the StartState.
this.TransitionToNewState(new StartState(this), null);
}
public override void ProcessTrigger(TriggerBase trigger)
{
if (this.CurrentState == null) return;
if (trigger == null) throw new ArgumentException("tigger must not be null");
// determine what action to take based on the current state
// and the given trigger.
<# // iterate all states in the diagram
foreach(var state in this.StateGraph.FlowElements.OfType<State>())
{
// check what transitions may be made from this state
var possibleTransitions = state.FlowTo.SourceDomainRole
.GetElementLinks<Flow>(state);
#> if (this.CurrentState is <#= state.Name #>State)
{
<# foreach(var transition in possibleTransitions)
{
#> if (trigger is <#= transition.Guard #>Trigger)
{
this.TransitionToNewState(new <#= transition.FlowTo.Name #>State(this),
trigger);
<# if (!string.IsNullOrEmpty(transition.Effect))
{
#> On<#= transition.Effect #>();
<# }
#> return;
}
<# }
#> }
<# }
#>
// the start state
if (this.CurrentState is StartState)
{
<# var startPoint = this.StateGraph.FlowElements.OfType<StartPoint>()
.FirstOrDefault();
if (startPoint == null)
throw new NullReferenceException("There is no start point in the diagram.");
// get all transitions from the start point
var possibleStartTransitions =
startPoint.FlowTo.SourceDomainRole.GetElementLinks<Flow>(startPoint);
foreach(var transition in possibleStartTransitions)
{
#> if (trigger is <#= transition.Guard #>Trigger)
{
this.TransitionToNewState(new <#= transition.FlowTo.Name #>State(this),
trigger);
<# if (!string.IsNullOrEmpty(transition.Effect))
{
#> On<#= transition.Effect #>();
<# }
#> return;
}
<# }
#> }
}
#region Events
<# // create events for all Effects that are raised
// by a transition
foreach(var eventName in allEvents)
{
#> protected void On<#= eventName #>()
{
if (this.<#= eventName #> != null)
this.<#= eventName #>(this, new EventArgs());
}
public event EventHandler <#= eventName #>;
<# }
#> #endregion
}
3. Complete generated code
With the complete template we generated in step 2 the state machine, triggers and states can be generated by right clicking the T4 file in the Solution Explorer and choosing “Run Custom Tool”. But there are still a few steps missing to achieve the full functionality of the Alarm Clock. Unfortunately these steps cannot be generated, because they cannot be designed in the state diagram.
The missing steps are:
- We cannot pass an alarm time to the state machine
- We cannot test for when the alarm is to go off
- We don’t have a reference to the current time yet.
Since all classes that are generated by the T4 template are partial classes, we can extend them in another file without losing the extensions when the template regenerates the code. Now add a C# code file to the solution next to the T4 template and name it “StateMachineExtensions.cs”.
Pass an alarm to the state machine
In order to pass an alarm time to the state machine, we’re going to extend the Trigger class by a property that stores the time when the alarm has to go off. Add the following code to the “StateMachineExtensions.cs” file:
partial class SetAlarmTimeTrigger
{
public DateTime AlarmTime { get; set; }
}
Test when the alarm is to go off
To achieve this we’re going to use the entry action “StartTimer” and “EndTimer” of the "Armed" state. In the “StartTimer” method we’re going to instantiate a Timer object that checks every second if the current time matches the time set by the SetAlarmTimeTrigger that has been passed as a parameter. In the exit action the timer is deactivated again.
Add the following code to the “StateMachineExtensions.cs” file:
partial class ArmedState
{
private DateTime alarmTime;
private Timer _t;
partial void StartTimer(TriggerBase causedByTrigger)
{
_t = new Timer(1000) { Enabled = true };
_t.Elapsed += _t_Elapsed;
}
void _t_Elapsed(object sender, ElapsedEventArgs e)
{
if (DateTime.Now >= alarmTime)
{
EndTimer(null);
this.StateMachine.ProcessTrigger(new AlarmTimeMatchesTrigger());
}
}
partial void EndTimer(TriggerBase causedByTrigger)
{
_t.Enabled = false;
try
{
_t.Elapsed -= _t_Elapsed;
}
catch { }
}
}
Reference the current time
In our scenario we want the Alarm Clock only to work when the state machine has been started. So we’re going to subscribe to the “MachineStarted” event and start another timer that only produces the current time to be displayed in the UI. Additionally we’re going to add a “CurrentTime” property to the AlarmClock state machine to be able to bind the current time to the UI.
partial class AlarmClock
{
protected override void Initialize()
{
base.Initialize();
// in order to display the current time we need a timer
// that automatically updates the CurrentTime-Property
this._Timer = new Timer(1000) { Enabled = false };
this._Timer.Elapsed += _Timer_Elapsed;
// this timer is only enabled after the state machine
// was started. So we're listening to the Machine-Started Event.
this.MachineStarted += AlarmClock_MachineStarted;
}
void AlarmClock_MachineStarted(object sender, EventArgs e)
{
this._Timer.Enabled = true;
}
void _Timer_Elapsed(object sender, ElapsedEventArgs e)
{
this.CurrentTime = DateTime.Now;
}
private Timer _Timer;
private DateTime _CurrentTime;
public DateTime CurrentTime
{
get { return _CurrentTime; }
set { _CurrentTime = value; OnPropertyChanged("CurrentTime"); }
}
}
4. Create and connect UI
Now all that is left to do is to create a nice user interface for the Alarm Clock state machine. For this sample I built a simple WPF window containing a TextBlock displaying the current time, a TextBox where the user can enter the desired alarm time and some Buttons: The first button starting the alarm clock state machine, the second one arming it, the third disarming it and the fourth ending the alarm.
The buttons are only enabled when clicking them triggers a transition in the state machine. For example the button that starts the state machine is only enabled when the machine is in the “Start” state.
At first we need to create some converters that allow us to use bindings to make the state machine have effect to the UI. Then we need to provide the Window with a ViewModel. Finally we can design the User Interface and start the application.
Converters
I decided to store all converters in a subdirectory “Converter” in the Visual Studio Solution.
DateTimeConverter
This converter will format a given DateTime object by a given format string. This way you can customize the look of the display of the alarm clock. Add a new C# code file to the “Converter” subdirectory and name it “DateTimeConverter.cs”. Let the class “DateTimeConverter” implement the interface IValueConverter.
This converter is going to be a one-direction-converter. That’s why I am only writing code in the “Convert” method.
public class DateTimeConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!(value is DateTime))
throw new ArgumentException("value must be of type DateTime");
var dtValue = (DateTime)value;
var param = (parameter as string) ?? "HH:mm:ss";
return dtValue.ToString(param);
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
InStateConverter
This converter will determine if the alarm clock state machine is in a given state. The name of the state to be tested will be passed in the parameter value. The value to convert will be the current state of the state machine. If the name of the current state equals the parameter value, the converter returns “true”, otherwise “false”.
public class InStateToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!(value is StateBase))
throw new ArgumentException("value must be of type StateBase");
var state = value as StateBase;
var param = parameter as string;
if (state == null) return false;
else return (state.GetType().Name == param);
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
InStateToVisibilityConverter
This converter is very similar to the InStateConverter but instead of a boolean value it returns a visibility value: if the name of the current state equals the parameter value, the converter returns “Visible”, otherwise “Collapsed”. We’re going to use this to display UI elements when the state machine is in a certain state.
public class InStateConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
if (!(value is StateBase))
throw new ArgumentException("value must be of type StateBase");
var state = value as StateBase;
var param = parameter as string;
if (state == null) return Visibility.Collapsed;
else return (state.GetType().Name == param) ? Visibility.Visible
: Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter,
System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
ViewModel
To be able to use the state machine in the application, create a ViewModel class for the MainWindow. I did this inside the MainWindow.xaml.cs file and created a public class named “MainWindowViewModel”. The view model implements the INotifyPropertyChanged interface and stores the state machine in a property “AlarmClock”.
Then the view model provides methods to interact with the state machine. There are for Methods that send a trigger to the state machine: “StartAlarmClock”, “SetAlarmTime”, “DisarmAlarmClock” and “TurnAlarmOff”.
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
this.AlarmClock = new AlarmClock();
}
private AlarmClock _AlarmClock;
public AlarmClock AlarmClock
{
get { return _AlarmClock; }
set { _AlarmClock = value; OnPropertyChanged("AlarmClock"); }
}
public void StartAlarmClock()
{
AlarmClock.ProcessTrigger(new StartTrigger());
}
public void SetAlarmTime(DateTime alarmTime)
{
AlarmClock.ProcessTrigger(new SetAlarmTimeTrigger() { AlarmTime = alarmTime });
}
public void DisarmAlarmClock()
{
AlarmClock.ProcessTrigger(new DisableAlarmTrigger());
}
public void TurnAlarmOff()
{
AlarmClock.ProcessTrigger(new UserTurnedAlarmOffTrigger());
}
protected void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
public event PropertyChangedEventHandler PropertyChanged;
}
MainWindow User Interface
Next we’re going to design the user interface. There’s not much to talk about the UI design. Make sure you reference the XML namespace of the previously created converters in the <Window /> tag. Then create an instance of each converter in the <Window.Resources /> tag. After that you can design the user interface to match the image above.
Here is a single example how to bind the current state of the state machine to the “Enabled” property of a button.
<Button IsEnabled="{Binding Path=AlarmClock.CurrentState,
Converter={StaticResource InStateConverter},
ConverterParameter='StartState'}" />
With this in mind the rest of the XAML should be self explanatory:
<Window x:Class="StateMachineSample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:converter="clr-namespace:StateMachineSample.Converter"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<!-- Converts a DateTime into a given Format -->
<converter:DateTimeConverter x:Key="DateTimeConverter" />
<!-- Converts a State into a boolean compared by its name -->
<converter:InStateConverter x:Key="InStateConverter" />
<!-- Converts a State into a Visibility value compared by its name -->
<converter:InStateToVisibilityConverter x:Key="InStateToVisibilityConverter" />
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="75" />
<RowDefinition Height="*" />
<RowDefinition Height="75" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="150" />
</Grid.ColumnDefinitions>
<!-- This Textblock displays the current time -->
<TextBlock Text="{Binding Path=AlarmClock.CurrentTime,
Converter={StaticResource DateTimeConverter},
ConverterParameter='HH:mm:ss'}"
FontSize="40"
Margin="5"
Grid.Row="1"
HorizontalAlignment="Center" />
<!-- This button starts the state machine alarm clock
It is only enabled when the machine has not been started yet -->
<Button x:Name="cmdStart"
Content="1.) Start State Machine"
IsEnabled="{Binding Path=AlarmClock.CurrentState,
Converter={StaticResource InStateConverter},
ConverterParameter='StartState'}"
Grid.Column="2"
Margin="5"
Click="cmdStart_Click" />
<!-- This area manages the alarm time -->
<StackPanel Grid.Column="1" Grid.Row="1" Margin="5">
<!-- In this texbox goes the desired alarm time
It is only enabled when the AlarmClock is not armed yet. -->
<TextBox x:Name="txtAlarm"
Text="12:00:00"
IsEnabled="{Binding Path=AlarmClock.CurrentState,
Converter={StaticResource InStateConverter},
ConverterParameter='UnarmedState'}" />
<!-- This button arms the Alarm clock
It is only enabled when the AlarmClock is not armed yet. -->
<Button x:Name="cmdArm"
IsEnabled="{Binding Path=AlarmClock.CurrentState,
Converter={StaticResource InStateConverter},
ConverterParameter='UnarmedState'}"
Content="2.) Arm AlarmClock"
Margin="0,5,0,0"
Click="cmdArm_Click" />
<!-- This button disarms the Alarm clock
It is only enabled when the AlarmClock is armed. -->
<Button x:Name="cmdDisarm"
Content="2.) Disarm AlarmClock"
IsEnabled="{Binding Path=AlarmClock.CurrentState,
Converter={StaticResource InStateConverter},
ConverterParameter='ArmedState'}"
Margin="0,5,0,0"
Click="cmdDisarm_Click" />
</StackPanel>
<!-- This image is the actual "alarm"
It is only visible when the Alarm goes off. -->
<Image Source="Images/bell.png"
Visibility="{Binding Path=AlarmClock.CurrentState,
Converter={StaticResource InStateToVisibilityConverter},
ConverterParameter='AlarmState'}"
Grid.Row="2" />
<!-- This button turns the alarm off again.
It is only enabled when the Alarm goes off. -->
<Button x:Name="cmdOff"
Content="3.) Turn alarm off"
IsEnabled="{Binding Path=AlarmClock.CurrentState,
Converter={StaticResource InStateConverter},
ConverterParameter='AlarmState'}"
Margin="5"
Grid.Row="2"
Grid.Column="1"
Click="cmdOff_Click" />
</Grid>
</Window>
Last but not least we need to instantiate the view model and connect the Click-Event handlers of all the buttons to the View Model. This happens in the MainWindow.xaml.cs:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
this.Loaded += MainWindow_Loaded;
}
MainWindowViewModel _ViewModel;
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
_ViewModel = new MainWindowViewModel();
this.DataContext = _ViewModel;
}
private void cmdStart_Click(object sender, RoutedEventArgs e)
{
_ViewModel.StartAlarmClock();
}
private void cmdArm_Click(object sender, RoutedEventArgs e)
{
var date = DateTime.Parse(txtAlarm.Text);
_ViewModel.SetAlarmTime(date);
}
private void cmdDisarm_Click(object sender, RoutedEventArgs e)
{
_ViewModel.DisarmAlarmClock();
}
private void cmdOff_Click(object sender, RoutedEventArgs e)
{
_ViewModel.TurnAlarmOff();
}
}
Voilà, that’s it! Now just hit F5 and check this awesome AlarmClock based on a state machine out.
Summary
In this non-trivial sample of how to use tangible State Diagrams and T4 templates we developed a state machine that implements an alarm clock.
First we designed the logic of the alarm clock with a state diagram. Then we created a basic and reusable state machine that is generated by a T4 template and transformed the state diagram into a specific implementation of that state machine.
After that we completed the logic of the alarm clock by extending the generated classes with individual code. Finally we accessed the state machine from a WPF sample application.
Download the sample
You can download the full sample code inside a Visual Studio Solution here: T4-StateMachineSample.zip (60K)