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(); } } }