A look at simple, yet powerful, navigation driven by a state machine, and how this can be used with asp.net MVC, asp.net Web forms or a test scenario.
Implementing navigation in web applications is often shown as a menu, implemented using li and various form of styling. Another case of navigation is a flow or wizard type where the user navigates from page to page towards one or more exits. We recently implemented a checkout flow which may range from a 4 page next next next process to 8 steps with possible loops based on the user selection. In such case one might start out with links from one view to the other, perhaps adding a few if's and switch statements to the cocktail, soon to realize your navigation is like getting lost in shanty town or other less desirable places.
A state machine is one way to solve this in a readble, controlled and testable manner. By expressing the navigational flow using a state machine we have a single place of configuration with explicit paths towards the goal. Stateless by Nicholas Blumhardt is an open source state machine implemented in C#, and it's available on nuget. Stateless is configured in code with states and triggers. For each state we can configure allowed triggers and the resulting state. It's also possible to evaluate data dynamically, pass data along with the trigger, create sub-states, and adding entry and exit actions for a state.
So to navigate through 4 pages, with one optional page, we will use an enum for the different states and another enum for the trigger.
We can then configure our state machine to navigate through these steps with the following code.
public enum Step
{
CustomerInformation,
AdditionalItems,
AdditionalItemConfiguration,
PaymentInformation,
OrderConfirmation
}
public enum Trigger
{
Previous,
Next
}
public class Navigator
{
private readonly StateMachine<Step, Trigger> _machine;
public Step CurrentStep { get; set; }
public Navigator(ICurrentOrder currentOrder)
{
_machine = new StateMachine<Step, Trigger>(() => CurrentStep, s => CurrentStep = s);
_machine.Configure(Step.CustomerInformation)
.Permit(Trigger.Next, Step.AdditionalItems);
_machine.Configure(Step.AdditionalItems)
.Permit(Trigger.Previous, Step.CustomerInformation)
.PermitDynamicIf(Trigger.Next,() => Step.AdditionalItemConfiguration, () => currentOrder.HasAdditionalItem)
.PermitDynamicIf(Trigger.Next,() => Step.PaymentInformation, () => !currentOrder.HasAdditionalItem);
_machine.Configure(Step.AdditionalItemConfiguration)
.SubstateOf(Step.AdditionalItems)
.Permit(Trigger.Previous, Step.AdditionalItems)
.Permit(Trigger.Next, Step.PaymentInformation);
_machine.Configure(Step.PaymentInformation)
.PermitDynamicIf(Trigger.Previous, () =>Step.AdditionalItems, ()=> !currentOrder.HasAdditionalItem)
.PermitDynamicIf(Trigger.Previous, () => Step.AdditionalItemConfiguration, () => currentOrder.HasAdditionalItem)
.Permit(Trigger.Next, Step.OrderConfirmation);
}
public Step Next()
{
_machine.Fire(Trigger.Next);
return CurrentStep;
}
public Step Previous()
{
_machine.Fire(Trigger.Previous);
return CurrentStep;
}
public bool AllowPrevious { get { return _machine.CanFire(Trigger.Previous); }}
public bool AllowNext { get { return _machine.CanFire(Trigger.Next); }}
}
It may be worth noting the initalization of the statemachine, where the Currentstep is passed in as a backing store for the machine. It is also possible to let the state machine maintain it's state internally.
We can use this in both webforms and MVC. For web forms we can implement a user control, access our navigator class and based on the allowed triggers of the current step display a previous and/or next button.
Our navigator fits just as well with MVC, use the string interpretation of the state and map it to controllers by convention.
With the navigator isolated on it's own, testing and verifying our navigation is like a summer breeze
This really comes in handy when we need to navigate safely through a maze of steps. And the best thing, we can assert that it works as required.