using Stateless.Reflection;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Stateless
{
///
/// Enum for the different modes used when Fireing a trigger
///
public enum FiringMode
{
/// Use immediate mode when the queuing of trigger events are not needed. Care must be taken when using this mode, as there is no run-to-completion guaranteed.
Immediate,
/// Use the queued Fireing mode when run-to-completion is required. This is the recommended mode.
Queued
}
///
/// Models behaviour as transitions between a finite set of states.
///
/// The type used to represent the states.
/// The type used to represent the triggers that cause state transitions.
public partial class StateMachine
{
private readonly IDictionary _stateConfiguration = new Dictionary();
private readonly IDictionary _triggerConfiguration = new Dictionary();
private readonly Func _stateAccessor;
private readonly Action _stateMutator;
private UnhandledTriggerAction _unhandledTriggerAction;
private readonly OnTransitionedEvent _onTransitionedEvent;
private readonly OnTransitionedEvent _onTransitionCompletedEvent;
private readonly TState _initialState;
private readonly FiringMode _firingMode;
private class QueuedTrigger
{
public TTrigger Trigger { get; set; }
public object[] Args { get; set; }
}
private readonly Queue _eventQueue = new Queue();
private bool _firing;
///
/// Construct a state machine with external state storage.
///
/// A function that will be called to read the current state value.
/// An action that will be called to write new state values.
public StateMachine(Func stateAccessor, Action stateMutator) : this(stateAccessor, stateMutator, FiringMode.Queued)
{
}
///
/// Construct a state machine.
///
/// The initial state.
public StateMachine(TState initialState) : this(initialState, FiringMode.Queued)
{
}
///
/// Construct a state machine with external state storage.
///
/// A function that will be called to read the current state value.
/// An action that will be called to write new state values.
/// Optional specification of firing mode.
public StateMachine(Func stateAccessor, Action stateMutator, FiringMode firingMode) : this()
{
_stateAccessor = stateAccessor ?? throw new ArgumentNullException(nameof(stateAccessor));
_stateMutator = stateMutator ?? throw new ArgumentNullException(nameof(stateMutator));
_initialState = stateAccessor();
_firingMode = firingMode;
}
///
/// Construct a state machine.
///
/// The initial state.
/// Optional specification of firing mode.
public StateMachine(TState initialState, FiringMode firingMode) : this()
{
var reference = new StateReference { State = initialState };
_stateAccessor = () => reference.State;
_stateMutator = s => reference.State = s;
_initialState = initialState;
_firingMode = firingMode;
}
///
/// For certain situations, it is essential that the SynchronizationContext is retained for all delegate calls.
///
public bool RetainSynchronizationContext { get; set; } = false;
///
/// Default constructor
///
StateMachine()
{
_unhandledTriggerAction = new UnhandledTriggerAction.Sync(DefaultUnhandledTriggerAction);
_onTransitionedEvent = new OnTransitionedEvent();
_onTransitionCompletedEvent = new OnTransitionedEvent();
}
///
/// The current state.
///
public TState State
{
get
{
return _stateAccessor();
}
private set
{
_stateMutator(value);
}
}
///
/// The currently-permissible trigger values.
///
[Obsolete("Kept for compatibility purposes. Recommended to use PermittedTriggersAsync instead")]
public IEnumerable PermittedTriggers => GetPermittedTriggers();
///
/// The currently-permissible trigger values.
///
[Obsolete("Kept for compatibility purposes. Recommended to use GetPermittedTriggersAsync instead")]
public IEnumerable GetPermittedTriggers(params object[] args)
{
return Task.Run(() => CurrentRepresentation.GetPermittedTriggersAsync(args)).GetAwaiter().GetResult();
}
///
/// Gets the currently-permissible triggers with any configured parameters.
///
public async Task>> GetDetailedPermittedTriggers(params object[] args)
{
return (await CurrentRepresentation.GetPermittedTriggersAsync(args))
.Select(trigger => new TriggerDetails(trigger, _triggerConfiguration));
}
StateRepresentation CurrentRepresentation
{
get
{
return GetRepresentation(State);
}
}
///
/// Provides an info object which exposes the states, transitions, and actions of this machine.
///
public StateMachineInfo GetInfo()
{
var initialState = StateInfo.CreateStateInfo(new StateRepresentation(_initialState, RetainSynchronizationContext));
var representations = _stateConfiguration.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
var behaviours = _stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehaviours.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList();
behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehaviours.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList());
behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehavioursAsync.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList());
behaviours.AddRange(_stateConfiguration.SelectMany(kvp => kvp.Value.TriggerBehavioursAsync.SelectMany(b => b.Value.OfType().Select(tb => tb.Destination))).ToList());
var reachable = behaviours
.Distinct()
.Except(representations.Keys)
.Select(underlying => new StateRepresentation(underlying, RetainSynchronizationContext))
.ToArray();
foreach (var representation in reachable)
representations.Add(representation.UnderlyingState, representation);
var info = representations.ToDictionary(kvp => kvp.Key, kvp => StateInfo.CreateStateInfo(kvp.Value));
foreach (var state in info)
StateInfo.AddRelationships(state.Value, representations[state.Key], k => info[k]);
return new StateMachineInfo(info.Values, typeof(TState), typeof(TTrigger), initialState);
}
StateRepresentation GetRepresentation(TState state)
{
if (!_stateConfiguration.TryGetValue(state, out StateRepresentation result))
{
result = new StateRepresentation(state, RetainSynchronizationContext);
_stateConfiguration.Add(state, result);
}
return result;
}
///
/// Begin configuration of the entry/exit actions and allowed transitions
/// when the state machine is in a particular state.
///
/// The state to configure.
/// A configuration object through which the state can be configured.
public StateConfiguration Configure(TState state)
{
return new StateConfiguration(this, GetRepresentation(state), GetRepresentation);
}
///
/// Transition from the current state via the specified trigger.
/// The target state is determined by the configuration of the current state.
/// Actions associated with leaving the current state and entering the new one
/// will be invoked.
///
/// The trigger to fire.
/// The current state does
/// not allow the trigger to be fired.
public void Fire(TTrigger trigger)
{
InternalFire(trigger, new object[0]);
}
///
/// Transition from the current state via the specified trigger.
/// The target state is determined by the configuration of the current state.
/// Actions associated with leaving the current state and entering the new one
/// will be invoked.
///
/// The trigger to fire.
/// A variable-length parameters list containing arguments.
/// The current state does
/// not allow the trigger to be fired.
public void Fire(TriggerWithParameters trigger, params object[] args)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
InternalFire(trigger.Trigger, args);
}
///
/// Specify the arguments that must be supplied when a specific trigger is fired.
///
/// The underlying trigger value.
/// The argument types expected by the trigger.
/// An object that can be passed to the Fire() method in order to
/// fire the parameterised trigger.
public TriggerWithParameters SetTriggerParameters(TTrigger trigger, params Type[] argumentTypes)
{
var configuration = new TriggerWithParameters(trigger, argumentTypes);
SaveTriggerConfiguration(configuration);
return configuration;
}
///
/// Transition from the current state via the specified trigger.
/// The target state is determined by the configuration of the current state.
/// Actions associated with leaving the current state and entering the new one
/// will be invoked.
///
/// Type of the first trigger argument.
/// The trigger to fire.
/// The first argument.
/// The current state does
/// not allow the trigger to be fired.
public void Fire(TriggerWithParameters trigger, TArg0 arg0)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
InternalFire(trigger.Trigger, arg0);
}
///
/// Transition from the current state via the specified trigger.
/// The target state is determined by the configuration of the current state.
/// Actions associated with leaving the current state and entering the new one
/// will be invoked.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// The first argument.
/// The second argument.
/// The trigger to fire.
/// The current state does
/// not allow the trigger to be fired.
public void Fire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
InternalFire(trigger.Trigger, arg0, arg1);
}
///
/// Transition from the current state via the specified trigger.
/// The target state is determined by the configuration of the current state.
/// Actions associated with leaving the current state and entering the new one
/// will be invoked.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// Type of the third trigger argument.
/// The first argument.
/// The second argument.
/// The third argument.
/// The trigger to fire.
/// The current state does
/// not allow the trigger to be fired.
public void Fire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, TArg2 arg2)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
InternalFire(trigger.Trigger, arg0, arg1, arg2);
}
///
/// Activates current state. Actions associated with activating the current state
/// will be invoked. The activation is idempotent and subsequent activation of the same current state
/// will not lead to re-execution of activation callbacks.
///
public void Activate()
{
var representativeState = GetRepresentation(State);
representativeState.Activate();
}
///
/// Deactivates current state. Actions associated with deactivating the current state
/// will be invoked. The deactivation is idempotent and subsequent deactivation of the same current state
/// will not lead to re-execution of deactivation callbacks.
///
public void Deactivate()
{
var representativeState = GetRepresentation(State);
representativeState.Deactivate();
}
///
/// Determine how to Fire the trigger
///
/// The trigger.
/// A variable-length parameters list containing arguments.
void InternalFire(TTrigger trigger, params object[] args)
{
switch (_firingMode)
{
case FiringMode.Immediate:
InternalFireOne(trigger, args);
break;
case FiringMode.Queued:
InternalFireQueued(trigger, args);
break;
default:
// If something is completely messed up we let the user know ;-)
throw new InvalidOperationException("The firing mode has not been configured!");
}
}
///
/// Queue events and then fire in order.
/// If only one event is queued, this behaves identically to the non-queued version.
///
/// The trigger.
/// A variable-length parameters list containing arguments.
private void InternalFireQueued(TTrigger trigger, params object[] args)
{
// Add trigger to queue
_eventQueue.Enqueue(new QueuedTrigger { Trigger = trigger, Args = args });
// If a trigger is already being handled then the trigger will be queued (FIFO) and processed later.
if (_firing)
{
return;
}
try
{
_firing = true;
// Empty queue for triggers
while (_eventQueue.Any())
{
var queuedEvent = _eventQueue.Dequeue();
InternalFireOne(queuedEvent.Trigger, queuedEvent.Args);
}
}
finally
{
_firing = false;
}
}
///
/// This method handles the execution of a trigger handler. It finds a
/// handle, then updates the current state information.
///
///
///
private void InternalFireOne(TTrigger trigger, params object[] args)
{
// If this is a trigger with parameters, we must validate the parameter(s)
if (_triggerConfiguration.TryGetValue(trigger, out TriggerWithParameters configuration))
{
configuration.ValidateParameters(args);
}
var source = State;
var representativeState = GetRepresentation(source);
// Try to find a trigger handler, either in the current state or a super state.
if (!representativeState.TryFindHandler(trigger, args, out TriggerBehaviourResult result))
{
_unhandledTriggerAction.Execute(representativeState.UnderlyingState, trigger, result?.UnmetGuardConditions);
return;
}
switch (result.Handler)
{
// Check if this trigger should be ignored
case IgnoredTriggerBehaviour _:
return;
// Handle special case, re-entry in superstate
// Check if it is an internal transition, or a transition from one state to another.
case ReentryTriggerBehaviour handler:
{
// Handle transition, and set new state
var transition = new Transition(source, handler.Destination, trigger, args);
HandleReentryTrigger(args, representativeState, transition);
break;
}
case DynamicTriggerBehaviourAsync asyncHandler:
{
asyncHandler.GetDestinationState(source, args)
.ContinueWith(t =>
{
var destination = t.Result;
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
var transition = new Transition(source, destination, trigger, args);
return HandleTransitioningTriggerAsync(args, representativeState, transition);
});
break;
}
case DynamicTriggerBehaviour handler:
{
handler.GetDestinationState(source, args, out var destination);
// Handle transition, and set new state; reentry is permitted from dynamic trigger behaviours.
var transition = new Transition(source, destination, trigger, args);
HandleTransitioningTrigger(args, representativeState, transition);
break;
}
case TransitioningTriggerBehaviour handler:
{
// If a trigger was found on a superstate that would cause unintended reentry, don't trigger.
if (source.Equals(handler.Destination))
break;
// Handle transition, and set new state
var transition = new Transition(source, handler.Destination, trigger, args);
HandleTransitioningTrigger(args, representativeState, transition);
break;
}
case InternalTriggerBehaviour _:
{
// Internal transitions does not update the current state, but must execute the associated action.
var transition = new Transition(source, source, trigger, args);
CurrentRepresentation.InternalAction(transition, args);
break;
}
default:
throw new InvalidOperationException("State machine configuration incorrect, no handler for trigger.");
}
}
private void HandleReentryTrigger(object[] args, StateRepresentation representativeState, Transition transition)
{
StateRepresentation representation;
transition = representativeState.Exit(transition);
var newRepresentation = GetRepresentation(transition.Destination);
if (!transition.Source.Equals(transition.Destination))
{
// Then Exit the final superstate
transition = new Transition(transition.Destination, transition.Destination, transition.Trigger, args);
newRepresentation.Exit(transition);
_onTransitionedEvent.Invoke(transition);
representation = EnterState(newRepresentation, transition, args);
_onTransitionCompletedEvent.Invoke(transition);
}
else
{
_onTransitionedEvent.Invoke(transition);
representation = EnterState(newRepresentation, transition, args);
_onTransitionCompletedEvent.Invoke(transition);
}
State = representation.UnderlyingState;
}
private void HandleTransitioningTrigger(object[] args, StateRepresentation representativeState, Transition transition)
{
transition = representativeState.Exit(transition);
State = transition.Destination;
var newRepresentation = GetRepresentation(transition.Destination);
//Alert all listeners of state transition
_onTransitionedEvent.Invoke(transition);
var representation = EnterState(newRepresentation, transition, args);
// Check if state has changed by entering new state (by firing triggers in OnEntry or such)
if (!representation.UnderlyingState.Equals(State))
{
// The state has been changed after entering the state, must update current state to new one
State = representation.UnderlyingState;
}
_onTransitionCompletedEvent.Invoke(new Transition(transition.Source, State, transition.Trigger, transition.Parameters));
}
private StateRepresentation EnterState(StateRepresentation representation, Transition transition, object[] args)
{
// Enter the new state
representation.Enter(transition, args);
if (FiringMode.Immediate.Equals(_firingMode) && !State.Equals(transition.Destination))
{
// This can happen if triggers are fired in OnEntry
// Must update current representation with updated State
representation = GetRepresentation(State);
transition = new Transition(transition.Source, State, transition.Trigger, args);
}
// Recursively enter substates that have an initial transition
if (representation.HasInitialTransition)
{
// Verify that the target state is a substate
// Check if state has substate(s), and if an initial transition(s) has been set up.
if (!representation.GetSubstates().Any(s => s.UnderlyingState.Equals(representation.InitialTransitionTarget)))
{
throw new InvalidOperationException($"The target ({representation.InitialTransitionTarget}) for the initial transition is not a substate.");
}
var initialTransition = new InitialTransition(transition.Source, representation.InitialTransitionTarget, transition.Trigger, args);
representation = GetRepresentation(representation.InitialTransitionTarget);
// Alert all listeners of initial state transition
_onTransitionedEvent.Invoke(new Transition(transition.Destination, initialTransition.Destination, transition.Trigger, transition.Parameters));
representation = EnterState(representation, initialTransition, args);
}
return representation;
}
///
/// Override the default behaviour of throwing an exception when an unhandled trigger
/// is fired.
///
/// An action to call when an unhandled trigger is fired.
public void OnUnhandledTrigger(Action unhandledTriggerAction)
{
if (unhandledTriggerAction == null) throw new ArgumentNullException(nameof(unhandledTriggerAction));
_unhandledTriggerAction = new UnhandledTriggerAction.Sync((s, t, c) => unhandledTriggerAction(s, t));
}
///
/// Override the default behaviour of throwing an exception when an unhandled trigger
/// is fired.
///
/// An action to call when an unhandled trigger is fired.
public void OnUnhandledTrigger(Action> unhandledTriggerAction)
{
if (unhandledTriggerAction == null) throw new ArgumentNullException(nameof(unhandledTriggerAction));
_unhandledTriggerAction = new UnhandledTriggerAction.Sync(unhandledTriggerAction);
}
///
/// Determine if the state machine is in the supplied state.
///
/// The state to test for.
/// True if the current state is equal to, or a substate of,
/// the supplied state.
public bool IsInState(TState state)
{
return CurrentRepresentation.IsIncludedIn(state);
}
///
/// Returns true if can be fired
/// in the current state.
///
///
/// When the trigger is configured with parameters, the default value of each of the trigger parameter's types will be used
/// to evaluate whether it can fire, which may not be the desired behavior; to check if a trigger can be fired with specific arguments,
/// use the overload of CanFire<TArg1[, TArg2[ ,TArg3]]>(TriggerWithParameters<TArg1[, TArg2[ ,TArg3]]>, ...) that
/// matches the type arguments of your trigger.
///
/// Trigger to test.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TTrigger trigger)
{
return CurrentRepresentation.CanHandle(trigger);
}
///
/// Returns true if can be fired
/// in the current state using the supplied trigger argument.
///
/// Type of the first trigger argument.
/// Trigger to test.
/// The first argument.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TriggerWithParameters trigger, TArg0 arg0)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
return CurrentRepresentation.CanHandle(trigger.Trigger, arg0);
}
///
/// Returns true if can be fired
/// in the current state using the supplied trigger arguments.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// Trigger to test.
/// The first argument.
/// The second argument.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
return CurrentRepresentation.CanHandle(trigger.Trigger, arg0, arg1);
}
///
/// Returns true if can be fired
/// in the current state using the supplied trigger arguments.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// Type of the third trigger argument.
/// Trigger to test.
/// The first argument.
/// The second argument.
/// The third argument.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, TArg2 arg2)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
return CurrentRepresentation.CanHandle(trigger.Trigger, arg0, arg1, arg2);
}
///
/// Returns true if can be fired in the current state.
///
///
/// When the trigger is configured with parameters, the default value of each of the trigger parameter's types will be used
/// to evaluate whether it can fire, which may not be the desired behavior; to check if a trigger can be fired with specific arguments,
/// use the overload of CanFire<TArg1[, TArg2[ ,TArg3]]>(TriggerWithParameters<TArg1[, TArg2[ ,TArg3]]>, ...) that
/// matches the type arguments of your trigger.
///
/// Trigger to test.
/// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TTrigger trigger, out ICollection unmetGuards)
{
return CurrentRepresentation.CanHandle(trigger, new object[] { }, out unmetGuards);
}
///
/// Returns true if can be fired
/// in the current state using the supplied trigger argument.
///
/// Type of the first trigger argument.
/// Trigger to test.
/// The first argument.
/// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, out ICollection unmetGuards)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0 }, out unmetGuards);
}
///
/// Returns true if can be fired
/// in the current state using the supplied trigger arguments.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// Trigger to test.
/// The first argument.
/// The second argument.
/// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, out ICollection unmetGuards)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0, arg1 }, out unmetGuards);
}
///
/// Returns true if can be fired
/// in the current state using the supplied trigger arguments.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// Type of the third trigger argument.
/// Trigger to test.
/// The first argument.
/// The second argument.
/// The third argument.
/// Guard descriptions of unmet guards. If given trigger is not configured for current state, this will be null.
/// True if the trigger can be fired, false otherwise.
public bool CanFire(TriggerWithParameters trigger, TArg0 arg0, TArg1 arg1, TArg2 arg2, out ICollection unmetGuards)
{
if (trigger == null) throw new ArgumentNullException(nameof(trigger));
return CurrentRepresentation.CanHandle(trigger.Trigger, new object[] { arg0, arg1, arg2 }, out unmetGuards);
}
///
/// A human-readable representation of the state machine.
///
/// A description of the current state and permitted triggers.
public override string ToString()
{
return string.Format(
"StateMachine {{ State = {0}, PermittedTriggers = {{ {1} }}}}",
State,
string.Join(", ", Task.Run(() => GetPermittedTriggersAsync()).GetAwaiter().GetResult().Select(t => t.ToString()).ToArray()));
}
///
/// Specify the arguments that must be supplied when a specific trigger is fired.
///
/// Type of the first trigger argument.
/// The underlying trigger value.
/// An object that can be passed to the Fire() method in order to
/// fire the parameterised trigger.
public TriggerWithParameters SetTriggerParameters(TTrigger trigger)
{
var configuration = new TriggerWithParameters(trigger);
SaveTriggerConfiguration(configuration);
return configuration;
}
///
/// Specify the arguments that must be supplied when a specific trigger is fired.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// The underlying trigger value.
/// An object that can be passed to the Fire() method in order to
/// fire the parameterised trigger.
public TriggerWithParameters SetTriggerParameters(TTrigger trigger)
{
var configuration = new TriggerWithParameters(trigger);
SaveTriggerConfiguration(configuration);
return configuration;
}
///
/// Specify the arguments that must be supplied when a specific trigger is fired.
///
/// Type of the first trigger argument.
/// Type of the second trigger argument.
/// Type of the third trigger argument.
/// The underlying trigger value.
/// An object that can be passed to the Fire() method in order to
/// fire the parameterised trigger.
public TriggerWithParameters SetTriggerParameters(TTrigger trigger)
{
var configuration = new TriggerWithParameters(trigger);
SaveTriggerConfiguration(configuration);
return configuration;
}
void SaveTriggerConfiguration(TriggerWithParameters trigger)
{
if (_triggerConfiguration.ContainsKey(trigger.Trigger))
throw new InvalidOperationException(
string.Format(StateMachineResources.CannotReconfigureParameters, trigger));
_triggerConfiguration.Add(trigger.Trigger, trigger);
}
void DefaultUnhandledTriggerAction(TState state, TTrigger trigger, ICollection unmetGuardConditions)
{
if (unmetGuardConditions?.Any() ?? false)
throw new InvalidOperationException(
string.Format(
StateMachineResources.NoTransitionsUnmetGuardConditions,
trigger, state, string.Join(", ", unmetGuardConditions)));
throw new InvalidOperationException(
string.Format(
StateMachineResources.NoTransitionsPermitted,
trigger, state));
}
///
/// Registers a callback that will be invoked every time the state machine
/// transitions from one state into another.
///
/// The action to execute, accepting the details
/// of the transition.
public void OnTransitioned(Action onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionedEvent.Register(onTransitionAction);
}
///
/// Registers a callback that will be invoked every time the statemachine
/// transitions from one state into another and all the OnEntryFrom etc methods
/// have been invoked
///
/// The action to execute, accepting the details
/// of the transition.
public void OnTransitionCompleted(Action onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionCompletedEvent.Register(onTransitionAction);
}
///
/// Unregisters a previously registered callback to prevent further events from
/// being raised when the state machine transitions from one state into another.
///
///
///
public void OnTransitionedUnregister(Action onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionedEvent.Unregister(onTransitionAction);
}
///
/// Unregisters a previously registered callback to prevent further events from
/// being raised when the state machine has completed its state transition.
///
///
///
public void OnTransitionCompletedUnregister(Action onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionCompletedEvent.Unregister(onTransitionAction);
}
///
/// Unregisters all callbacks currently registered with the state machine for
/// both "transitioned" and "transition completed" events.
///
public void UnregisterAllCallbacks()
{
_onTransitionedEvent.UnregisterAll();
_onTransitionCompletedEvent.UnregisterAll();
}
}
}