Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -212,16 +212,70 @@ Stateless supports 2 types of state machine events:

#### State transition
```csharp
// Synchronously
stateMachine.OnTransitioned((transition) => { });

// Asynchronously
stateMachine.OnTransitionedAsync((transition) => { return Task.FromResult(0); });
```
This event will be invoked every time the state machine changes state.

#### State machine transition completed
```csharp
// Synchronously
stateMachine.OnTransitionCompleted((transition) => { });

// Asynchronously
stateMachine.OnTransitionCompletedAsync((transition) => { return Task.FromResult(0); });
```
This event will be invoked at the very end of the trigger handling, after the last entry action has been executed.

---

In addition to this, Stateless also provides you with the ability to unregister from state machine events in 3 ways.
* State transition unregister (sync/async)
* State machine transition completed (sync/async)
* State machine unregister from all (sync and async)


#### State machine transition unregister (synchronous)
```csharp
// Keep a reference to the synchronous callback action we want to unregister later.
Action transitionCallbackAction = (transition) => { };
stateMachine.OnTransitionedUnregister(transitionCallbackAction);
```
This method will unregister the specified action callback from the transition event.

#### State machine transition unregister (asynchronous)
```csharp
// Keep a reference to the asynchronous callback function we want to unregister later.
Func<Transition, Task> transitionAsyncCallback => (transition) => { return Task.FromResult(0); };
stateMachine.OnTransitionedAsyncUnregister(transitionAsyncCallback);
````
This method will unregister the specified async function callback from the transition event.

#### State machine transition completed unregister (synchronous)
```csharp
// Keep a reference to the synchronous callback action we want to unregister later.
Action transitionCompletedCallbackAction = (transition) => { });
stateMachine.OnTransitionCompletedUnregister(transitionCompletedCallbackAction);
```
This method will unregister the specified action callback from the transition completed event.

#### State machine transition completed unregister (asynchronous)
```csharp
// Keep a reference to to the asynchronous callback function we want to unregister later.
Func<Transition, Task> transitionCompletedAsyncCallback => (transition) => { return Task.FromResult(0); });
stateMachine.OnTransitionCompletedAsyncUnregister(transitionCompletedAsyncCallback);
```
This method will unregister the specified async function callback from the transition completed event.

#### Unregister all registered callbacks (sync and async)
```csharp
stateMachine.UnregisterAllCallbacks();
```
This method will unregister all synchronous and asynchronously registered callbacks from the state machine.

### Export to DOT graph

It can be useful to visualize state machines on runtime. With this approach the code is the authoritative source and state diagrams are by-products which are always up to date.
Expand Down
32 changes: 30 additions & 2 deletions example/OnOffExample/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Stateless;
using Stateless.Graph;

namespace OnOffExample
{
Expand All @@ -9,6 +10,11 @@ namespace OnOffExample
/// </summary>
class Program
{
static void TransitionAnnounce(StateMachine<string, char>.Transition transition)
{
Console.WriteLine($"State Machine Transitioning from '{transition.Source}' to '{transition.Destination}'");
}

static void Main(string[] args)
{
const string on = "On";
Expand All @@ -28,9 +34,31 @@ static void Main(string[] args)
{
Console.WriteLine("Switch is in state: " + onOffSwitch.State);
var pressed = Console.ReadKey(true).KeyChar;

// Check if user wants to exit
if (pressed != space) break;
if (pressed != space)
{
// Before exiting this is how you can safely register and unregister from transition events.
// Keys 'r' or 'R' = Register for transition events with double subscription prevention built-in.
// Keys 'u' or 'U' = Unregister from transition events to prevent memory leaks in long running applications.
switch (pressed)
{
case 'r':
case 'R':
onOffSwitch.OnTransitioned(TransitionAnnounce);
Console.WriteLine("Now subscribed to transition events..");
continue;

case 'u':
case 'U':
onOffSwitch.OnTransitionedUnregister(TransitionAnnounce);
Console.WriteLine("Successfully unsubscribed from transition events..");
continue;
}

Console.WriteLine("Exiting program");
break;
}

// Use the Fire method with the trigger as payload to supply the state machine with an event.
// The state machine will react according to its configuration.
Expand Down
32 changes: 30 additions & 2 deletions src/Stateless/OnTransitionedEvent.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ namespace Stateless
{
public partial class StateMachine<TState, TTrigger>
{
class OnTransitionedEvent
internal class OnTransitionedEvent
{
event Action<Transition> _onTransitioned;
readonly List<Func<Transition, Task>> _onTransitionedAsync = new List<Func<Transition, Task>>();

public void Invoke(Transition transition)
{
if (_onTransitionedAsync.Count != 0)
Expand All @@ -33,13 +33,41 @@ public async Task InvokeAsync(Transition transition, bool retainSynchronizationC

public void Register(Action<Transition> action)
{
_onTransitioned -= action;
_onTransitioned += action;
}

public void Register(Func<Transition, Task> action)
{
_onTransitionedAsync.Remove(action);
_onTransitionedAsync.Add(action);
}

public void Unregister(Action<Transition> action)
{
if (_onTransitioned != null)
{
_onTransitioned -= action;
}
}

public void Unregister(Func<Transition, Task> action)
{
_onTransitionedAsync.Remove(action);
}

public void UnregisterAll()
{
if (_onTransitioned != null)
{
foreach (Delegate eventHandler in _onTransitioned.GetInvocationList())
{
_onTransitioned -= (Action<Transition>)eventHandler;
}
}

_onTransitionedAsync.Clear();
}
}
}
}
24 changes: 24 additions & 0 deletions src/Stateless/StateMachine.Async.cs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,30 @@ public void OnTransitionCompletedAsync(Func<Transition, Task> onTransitionAction
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionCompletedEvent.Register(onTransitionAction);
}

/// <summary>
/// Unregisters a previously registered callback to prevent further events from
/// being raised when the state machine transitions from one state into another.
/// </summary>
/// <param name="onTransitionAction"></param>
/// <exception cref="ArgumentNullException"></exception>
public void OnTransitionedAsyncUnregister(Func<Transition, Task> onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionedEvent.Unregister(onTransitionAction);
}

/// <summary>
/// Unregisters a previously registered callback to prevent further events from
/// being raised when the state machine has completed its state transition.
/// </summary>
/// <param name="onTransitionAction"></param>
/// <exception cref="ArgumentNullException"></exception>
public void OnTransitionCompletedAsyncUnregister(Func<Transition, Task> onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionCompletedEvent.Unregister(onTransitionAction);
}
}
}

Expand Down
36 changes: 35 additions & 1 deletion src/Stateless/StateMachine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -821,5 +821,39 @@ public void OnTransitionCompleted(Action<Transition> onTransitionAction)
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionCompletedEvent.Register(onTransitionAction);
}

/// <summary>
/// Unregisters a previously registered callback to prevent further events from
/// being raised when the state machine transitions from one state into another.
/// </summary>
/// <param name="onTransitionAction"></param>
/// <exception cref="ArgumentNullException"></exception>
public void OnTransitionedUnregister(Action<Transition> onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionedEvent.Unregister(onTransitionAction);
}

/// <summary>
/// Unregisters a previously registered callback to prevent further events from
/// being raised when the state machine has completed its state transition.
/// </summary>
/// <param name="onTransitionAction"></param>
/// <exception cref="ArgumentNullException"></exception>
public void OnTransitionCompletedUnregister(Action<Transition> onTransitionAction)
{
if (onTransitionAction == null) throw new ArgumentNullException(nameof(onTransitionAction));
_onTransitionCompletedEvent.Unregister(onTransitionAction);
}

/// <summary>
/// Unregisters all callbacks currently registered with the state machine for
/// both "transitioned" and "transition completed" events.
/// </summary>
public void UnregisterAllCallbacks()
{
_onTransitionedEvent.UnregisterAll();
_onTransitionCompletedEvent.UnregisterAll();
}
}
}
}
Loading
Loading