WPF – State machine and ICommand

Published by

on

The goal of this post is to test the usage of MVVM Delegate commands together with a state machine.  You can read more about the state machine pattern here, and about commands here.

WPF allows to bind a command to some controls, the ICommand interface has two methods:  one that executes an action and one that returns  a Boolean indicating if the command can be executed or not.

E.g. Command bound to a button: When the button is clicked the action defined within the command is executed, when the command cannot be executed the button is disabled.

This is the ICommand interface.

public interface ICommand {
	event EventHandler CanExecuteChanged;
	bool CanExecute(object parameter);
	void Execute(object parameter);
}

I will use the Microsoft PRISM 5 Delegate ICommand implementation and the stateless state machine.

ICommand + State Machine Trigger

On a state machine there are states and triggers, within one state some triggers are forbidden and other are allowed, when a trigger is fired the state machine could change its state.

Our commands shall be associated to a trigger, the behaviour should be:

  • CanExecute: Shall return true only if the associated trigger can be fired on the current state.
  • Execute: Shall fire the associated trigger, the machine could change its state.

The coffee machine

The application that we will write will be a very simple coffee machine with only one type of coffee:

States

  1. Idle: Machine waiting to be used.
  2. With money: The user entered some money but is not enough to get a coffee.
  3. Can get coffee: The user entered enough money to get a coffee.
  4. Preparing coffee: The coffee is being prepared.
  5. Coffee ready: The user can take the coffee.
  6. Refunding money: The machine is returning the money to the user.

Triggers

  1. Insert money: Fired when the user insert money.
  2. Refund money: Fired when the user press the refund money button and after the user takes the coffee.
  3. Enough money: Automatically fired when the user inserted enough money.
  4. Prepare coffee: Fired when the user select a coffee, (our machine is that simple that has only one…)
  5. Coffee prepared: Automatically fired when the coffee is prepared.
  6. Take coffee: Fired when the user takes the coffee.
  7. Money refunded: Automatically fired when the machine refunded the money.

The diagram

image

Implementation

States

    public enum CoffeeMachineState
    {
        Idle,
        WithMoney,
        CanSelectCoffee,
        PreparingCoffee,
        CoffeeReady,
        RefundMoney
    }

Triggers

    public enum CoffeeMachineTrigger
    {
        InsertMoney,
        RefundMoney,
        PrepareCoffee,
        TakeCoffe,

        // Automatic triggers
        EnoughMoney,
        CoffeePrepared,
        MoneyRefunded
    }

Configuration

        /// <summary>
        /// Configures the machine.
        /// </summary>
        private void ConfigureMachine()
        {
            // Idle
            this.Configure(CoffeeMachineState.Idle)
                .Permit(CoffeeMachineTrigger.InsertMoney, CoffeeMachineState.WithMoney);

            // Refund money
            this.Configure(CoffeeMachineState.RefundMoney)
                .OnEntry(RefundMoney)
                .Permit(CoffeeMachineTrigger.MoneyRefunded, CoffeeMachineState.Idle);


            // WithMoney
            this.Configure(CoffeeMachineState.WithMoney)
                .PermitReentry(CoffeeMachineTrigger.InsertMoney)
                .Permit(CoffeeMachineTrigger.RefundMoney, CoffeeMachineState.RefundMoney)
                .Permit(CoffeeMachineTrigger.EnoughMoney, CoffeeMachineState.CanSelectCoffee);

            // CanSelectCoffee
            this.Configure(CoffeeMachineState.CanSelectCoffee)
                .PermitReentry(CoffeeMachineTrigger.InsertMoney)
                .Permit(CoffeeMachineTrigger.RefundMoney, CoffeeMachineState.RefundMoney)
                .Permit(CoffeeMachineTrigger.PrepareCoffee, CoffeeMachineState.PreparingCoffee);

            // PreparingCoffee
            this.Configure(CoffeeMachineState.PreparingCoffee)
                .OnEntry(PrepareCoffee)
                .Permit(CoffeeMachineTrigger.CoffeePrepared, CoffeeMachineState.CoffeeReady);

            // CoffeeReady
            this.Configure(CoffeeMachineState.CoffeeReady)
                .Permit(CoffeeMachineTrigger.TakeCoffe, CoffeeMachineState.RefundMoney);

            this.OnTransitioned(NotifyStateChanged);
        }

StateMachine command factory

The factory will create commands associated to a trigger, the command will fire the trigger when it is executed and can only executed only if the trigger can be fired on the current state.

The factory also support to add an additional action to execute when the trigger is fired and an additional function to evaluate the “CanExecute” result.

using System;
using Microsoft.Practices.Prism.Commands;
using Stateless;

namespace CoffeeMachine.Wpf.Comands
{
    public static class StateMachineCommandEx
    {
        /// <summary>
        /// Creates a DelegateCommand using a trigger and a state machine.
        /// The command can be executed if the trigger can be executed on the current state machine status and the specified "CanExecute" function is null or returns true.
        /// When the command is executed the specified action is executed and then the trigger is fired
        /// </summary>
        /// <typeparam name="TState">State machine status type.</typeparam>
        /// <typeparam name="TTrigger">State machine status trigger.</typeparam>
        /// <param name="stateMachine">A state machine instance</param>
        /// <param name="trigger">A trigger.</param>
        /// <param name="execute">Action to execute when the command is executed.</param>
        /// <param name="canExecute">The command can be executed only if this function is null or return true and the current status of the machine supports the trigger.</param>
        public static DelegateCommand CreateCommand<TState, TTrigger>(this StateMachine<TState, TTrigger> stateMachine, TTrigger trigger, Action execute = null, Func<bool> canExecute = null)
        {
            if (canExecute == null)
            {
                canExecute = () => true;
            }

            if (execute == null)
            {
                execute = delegate { };
            }

            return new DelegateCommand(
                executeMethod: delegate
                {
                    execute();
                    stateMachine.Fire(trigger);
                },
                canExecuteMethod: () => stateMachine.CanFire(trigger) && canExecute());
        }

        /// <summary>
        /// Creates a DelegateCommand using a trigger and a state machine.
        /// The command can be executed if the trigger can be executed on the current state machine status and the specified "CanExecute" function is null or returns true.
        /// When the command is executed the specified action is executed and then the trigger is fired
        /// </summary>
        /// <typeparam name="TState">State machine status type.</typeparam>
        /// <typeparam name="TTrigger">State machine status trigger.</typeparam>
        /// <typeparam name="TCommandParam">Command parameter type</typeparam>
        /// <param name="stateMachine">A state machine instance</param>
        /// <param name="trigger">A trigger.</param>
        /// <param name="execute">Action to execute when the command is executed.</param>
        /// <param name="canExecute">The command can be executed only if this function is null or return true and the current status of the machine supports the trigger.</param>
        public static DelegateCommand<TCommandParam> CreateCommand<TState, TTrigger, TCommandParam>(this StateMachine<TState, TTrigger> stateMachine, TTrigger trigger, Action<TCommandParam> execute = null, Func<bool> canExecute = null)
        {
            if (canExecute == null)
            {
                canExecute = () => true;
            }

            if (execute == null)
            {
                execute = delegate { };
            }

            return new DelegateCommand<TCommandParam>(
                executeMethod: delegate(TCommandParam param)
                {
                    execute(param);
                    stateMachine.Fire(trigger);
                },
                canExecuteMethod: arg => stateMachine.CanFire(trigger) && canExecute());
        }
    }
}

Sample of usage:

        // Initializes a new instance of the <see cref="ShellViewModel"/> class.
        public CoffeeMachineViewModel()
        {

            this.CoffeeMachine = new CoffeeMachineModel();

            // Create commands
            InsertCoinCommand = CoffeeMachine.CreateCommand<CoffeeMachineState, CoffeeMachineTrigger, double?>(
                trigger: CoffeeMachineTrigger.InsertMoney,
                execute: param => this.CoffeeMachine.InsertCoin(param ?? 0));
            RefundMoneyCommand = CoffeeMachine.CreateCommand(CoffeeMachineTrigger.RefundMoney);
            PrepareCoffeeCommand = CoffeeMachine.CreateCommand(CoffeeMachineTrigger.PrepareCoffee);
            TakeCoffeeCommand = CoffeeMachine.CreateCommand(CoffeeMachineTrigger.TakeCoffe);
        }

Results (Sorry for the ugly UI):

Idle state screenshot

WPF State machine commands screenshot.

Can get coffee state screenshot

WPF State machine commands screenshot.

Preparing coffee state screenshot

WPF State machine commands screenshot.

Download the code.

Reference:

http://msdn.microsoft.com/en-us/magazine/dn818499.aspx

7 responses to “WPF – State machine and ICommand”

  1. Gustavo Souza Avatar

    Could you share the code through another file host?

    Like

  2. Juan Carlos Sánchez Avatar

    Hi Gustavo, sorry for the late response.

    You can find the file here:
    https://mega.co.nz/#!KoUGEDzR!RN4QE2M11_hIEqES7QK2JyNSTR1_Ow6qFQQdJJ3Q1JA

    Like

  3. Thomas Rotz Avatar
    Thomas Rotz

    Hi,

    I know this is old, but do you still have the source code?
    The download links don’t include any source.

    Like

    1. kinaski Avatar
      kinaski

      do you still have the source code?
      The download links don’t include any source.

      Like

    2. kinaski Avatar
      kinaski

      did youget the source code?

      Like

  4. kinaski Avatar
    kinaski

    can you please put a link here with working files?

    Like

  5. Juan Carlos Sánchez Avatar

    You can find the source code here:
    https://github.com/softwarejc/statemachine

    Regards

    Like

Your feedback is important…

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Blog at WordPress.com.