跳至主要内容

GST Stateless 狀態機使用指南

適用對象:GST 團隊開發人員 前置知識:C# 基礎、MVVM 模式 預計閱讀時間:20 分鐘


1. 概述

1.1 為什麼使用 Stateless

Stateless 是一個輕量級的 .NET 狀態機框架,GST 團隊選擇使用它的原因:

優點說明
清晰的狀態轉換明確定義允許的狀態轉換,防止非法操作
Guard 防護使用 PermitIf 設定條件轉換,確保業務邏輯正確
UI 整合友善CanFire 屬性可直接綁定到 MVVM 的 Command
可視化支援內建 DOT/UML 圖表輸出,方便文件撰寫
輕量無依賴不需要複雜的設定,直接使用

1.2 適用場景

  • 設備連線管理:Disconnected → Connecting → Connected → Error
  • 控制流程:NotInitialized → Ready → Running → Paused → Stopped
  • 資料處理:Idle → Processing → Completed → Error

1.3 套件資訊

<PackageReference Include="Stateless" Version="5.20.0" />

2. 基本模式

2.1 命名規範

元素命名規範範例
State enum{Feature}StateConnectionState, SmbControllerState
Trigger enum{Feature}TriggerConnectionTrigger, SmbTrigger
狀態機類別{Feature}StateMachineDeviceConnectionStateMachine

2.2 State Enum 範例

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionState.cs

namespace Jope.SMB.Core.Devices;

/// <summary>
/// Unified connection state for all device types
/// </summary>
public enum ConnectionState
{
/// <summary>Device is not connected</summary>
Disconnected = 0,

/// <summary>Device is attempting to connect</summary>
Connecting = 1,

/// <summary>Device is connected and ready</summary>
Connected = 2,

/// <summary>Device is attempting to reconnect after connection loss</summary>
Reconnecting = 3,

/// <summary>Device has a connection error</summary>
Error = 4
}

2.3 Trigger Enum 範例

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionTrigger.cs

namespace Jope.SMB.Core.Devices;

/// <summary>
/// Triggers for device connection state machine
/// </summary>
public enum ConnectionTrigger
{
/// <summary>Initiate connection</summary>
Connect,

/// <summary>Connection succeeded</summary>
Success,

/// <summary>Connection failed</summary>
Failure,

/// <summary>Disconnect actively</summary>
Disconnect,

/// <summary>Connection lost unexpectedly</summary>
LostConnection,

/// <summary>Retry connection</summary>
Retry
}

2.4 類別結構

標準的狀態機類別應包含以下結構:

public class {Feature}StateMachine
{
// 1. 私有欄位
private readonly StateMachine<{State}, {Trigger}> _machine;

// 2. 公開屬性 - 狀態
public {State} State => _machine.State;

// 3. 公開屬性 - CanFire
public bool CanXxx => _machine.CanFire({Trigger}.Xxx);

// 4. 事件
public event EventHandler<StateChangedEventArgs>? StateChanged;

// 5. 建構函式
public {Feature}StateMachine()
{
_machine = new StateMachine<{State}, {Trigger}>({State}.Initial);
ConfigureStateMachine();
}

// 6. ConfigureStateMachine 方法
private void ConfigureStateMachine() { ... }

// 7. Fire 方法
public void Fire({Trigger} trigger) { ... }
}

2.5 ConfigureStateMachine() 模式

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs:62-95

private void ConfigureStateMachine()
{
// Disconnected state
_machine.Configure(ConnectionState.Disconnected)
.Permit(ConnectionTrigger.Connect, ConnectionState.Connecting)
.Ignore(ConnectionTrigger.Disconnect);

// Connecting state
_machine.Configure(ConnectionState.Connecting)
.Permit(ConnectionTrigger.Success, ConnectionState.Connected)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected);

// Connected state
_machine.Configure(ConnectionState.Connected)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected)
.Permit(ConnectionTrigger.LostConnection, ConnectionState.Reconnecting)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error);

// Reconnecting state
_machine.Configure(ConnectionState.Reconnecting)
.Permit(ConnectionTrigger.Success, ConnectionState.Connected)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected);

// Error state
_machine.Configure(ConnectionState.Error)
.Permit(ConnectionTrigger.Retry, ConnectionState.Connecting)
.Permit(ConnectionTrigger.Disconnect, ConnectionState.Disconnected)
.OnEntry(() => { /* Error state entered */ });

// Register transition callback
_machine.OnTransitioned(OnStateMachineTransitioned);
}

3. 進階功能

3.1 OnEntry / OnExit

進入或離開狀態時執行動作:

_machine.Configure(ConnectionState.Error)
.Permit(ConnectionTrigger.Retry, ConnectionState.Connecting)
.OnEntry(() => { /* 進入 Error 狀態時執行 */ })
.OnExit(() => { /* 離開 Error 狀態時執行 */ });

3.2 OnEntryAsync(非同步)

非同步操作使用 OnEntryAsync

_machine.Configure(ConnectionState.Connecting)
.OnEntryAsync(async () =>
{
await ConnectToDeviceAsync();
})
.Permit(ConnectionTrigger.Success, ConnectionState.Connected)
.Permit(ConnectionTrigger.Failure, ConnectionState.Error);

注意:使用非同步時,需要使用 FireAsync 觸發:

await _machine.FireAsync(ConnectionTrigger.Connect);

3.3 PermitIf(條件轉換)

設定轉換條件(Guard):

_machine.Configure(SmbControllerState.Ready)
.PermitIf(SmbTrigger.Start, SmbControllerState.Running,
() => _configuration != null,
"Configuration must be set before starting");

3.4 PermitReentry(重入)

允許同一狀態重入:

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs:133-137

// Ready state
_machine.Configure(SmbControllerState.Ready)
.Permit(SmbTrigger.Start, SmbControllerState.Running)
.PermitReentry(SmbTrigger.Configure) // 允許重複設定
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

3.5 Ignore(忽略觸發)

忽略特定觸發(不拋出例外):

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs:65-67

// Disconnected state
_machine.Configure(ConnectionState.Disconnected)
.Permit(ConnectionTrigger.Connect, ConnectionState.Connecting)
.Ignore(ConnectionTrigger.Disconnect); // 已斷線時再斷線不做事

3.6 OnTransitioned(轉換回呼)

註冊全域轉換回呼:

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs:97-106

_machine.OnTransitioned(OnStateMachineTransitioned);

private void OnStateMachineTransitioned(
StateMachine<ConnectionState, ConnectionTrigger>.Transition transition)
{
StateChanged?.Invoke(this, new ConnectionStateChangedEventArgs
{
PreviousState = transition.Source,
CurrentState = transition.Destination,
Trigger = transition.Trigger,
ErrorMessage = transition.Destination == ConnectionState.Error
? _lastErrorMessage : null
});
}

4. UI 整合(WPF MVVM)

4.1 CanFire 屬性設計

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs:57-94

在 Controller/Service 層公開 CanFire 屬性:

#region CanFire Properties

/// <summary>
/// Whether the controller can be configured
/// </summary>
public bool CanConfigure => _machine.CanFire(SmbTrigger.Configure);

/// <summary>
/// Whether the controller can start
/// </summary>
public bool CanStart => _machine.CanFire(SmbTrigger.Start);

/// <summary>
/// Whether the controller can pause
/// </summary>
public bool CanPause => _machine.CanFire(SmbTrigger.Pause);

/// <summary>
/// Whether the controller can resume
/// </summary>
public bool CanResume => _machine.CanFire(SmbTrigger.Resume);

/// <summary>
/// Whether the controller can stop
/// </summary>
public bool CanStop => _machine.CanFire(SmbTrigger.Stop);

/// <summary>
/// Whether the controller can reset
/// </summary>
public bool CanReset => _machine.CanFire(SmbTrigger.Reset);

/// <summary>
/// Whether the controller can acknowledge an error
/// </summary>
public bool CanAcknowledge => _machine.CanFire(SmbTrigger.Acknowledge);

#endregion

4.2 ViewModel 綁定

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs:52-89

在 ViewModel 中轉發 CanFire 屬性:

#region CanFire Properties (from JOP-63 State Machine)

/// <summary>
/// Whether the Start button should be enabled
/// </summary>
public bool CanStart => _controller.CanStart;

/// <summary>
/// Whether the Pause button should be enabled
/// </summary>
public bool CanPause => _controller.CanPause;

/// <summary>
/// Whether the Resume button should be enabled
/// </summary>
public bool CanResume => _controller.CanResume;

/// <summary>
/// Whether the Stop button should be enabled
/// </summary>
public bool CanStop => _controller.CanStop;

/// <summary>
/// Whether the Reset button should be enabled
/// </summary>
public bool CanReset => _controller.CanReset;

/// <summary>
/// Whether the Acknowledge button should be enabled
/// </summary>
public bool CanAcknowledge => _controller.CanAcknowledge;

/// <summary>
/// Whether configuration changes are allowed
/// </summary>
public bool CanConfigure => _controller.CanConfigure;

#endregion

4.3 StateChanged 事件處理

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs:190-210

狀態變更時通知 UI 更新:

private void OnControllerStateChanged(object? sender, StateChangedEventArgs e)
{
// Notify UI of all state-dependent properties
System.Windows.Application.Current?.Dispatcher.Invoke(() =>
{
OnPropertyChanged(nameof(CurrentState));
OnPropertyChanged(nameof(StateDisplayText));
OnPropertyChanged(nameof(StateColor));
OnPropertyChanged(nameof(IsInError));
OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsPaused));

// Notify CanFire properties
OnPropertyChanged(nameof(CanStart));
OnPropertyChanged(nameof(CanPause));
OnPropertyChanged(nameof(CanResume));
OnPropertyChanged(nameof(CanStop));
OnPropertyChanged(nameof(CanReset));
OnPropertyChanged(nameof(CanAcknowledge));
OnPropertyChanged(nameof(CanConfigure));
});
}

4.4 XAML 綁定範例

<!-- 使用 CanFire 控制按鈕 IsEnabled -->
<Button Content="Start"
Command="{Binding StartCommand}"
IsEnabled="{Binding CanStart}" />

<Button Content="Pause"
Command="{Binding PauseCommand}"
IsEnabled="{Binding CanPause}" />

<Button Content="Stop"
Command="{Binding StopCommand}"
IsEnabled="{Binding CanStop}" />

<Button Content="Reset"
Command="{Binding ResetCommand}"
IsEnabled="{Binding CanReset}" />

<!-- 錯誤確認按鈕只在 Error 狀態顯示 -->
<Button Content="Acknowledge Error"
Command="{Binding AcknowledgeCommand}"
Visibility="{Binding CanAcknowledge, Converter={StaticResource BoolToVisibility}}" />

5. 實作範例

5.1 DeviceConnectionStateMachine(完整範例)

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs

此為通用的設備連線狀態機,可供所有設備類型使用:

using Stateless;
using Stateless.Graph;

namespace Jope.SMB.Core.Devices;

/// <summary>
/// State machine for managing device connection states.
/// Provides unified connection management for all device types.
/// </summary>
public class DeviceConnectionStateMachine
{
private readonly StateMachine<ConnectionState, ConnectionTrigger> _machine;
private string? _lastErrorMessage;

public ConnectionState State => _machine.State;
public bool CanConnect => _machine.CanFire(ConnectionTrigger.Connect);
public bool CanDisconnect => _machine.CanFire(ConnectionTrigger.Disconnect);
public bool CanRetry => _machine.CanFire(ConnectionTrigger.Retry);
public string? LastErrorMessage => _lastErrorMessage;

public event EventHandler<ConnectionStateChangedEventArgs>? StateChanged;

public DeviceConnectionStateMachine()
{
_machine = new StateMachine<ConnectionState, ConnectionTrigger>(
ConnectionState.Disconnected);
ConfigureStateMachine();
}

public void Fire(ConnectionTrigger trigger)
{
if (!_machine.CanFire(trigger))
{
throw new InvalidOperationException(
$"Cannot fire trigger '{trigger}' from state '{_machine.State}'");
}
_machine.Fire(trigger);
}

public void Fire(ConnectionTrigger trigger, string? errorMessage)
{
if (trigger == ConnectionTrigger.Failure)
{
_lastErrorMessage = errorMessage;
}
Fire(trigger);
}

public bool TryFire(ConnectionTrigger trigger)
{
if (!_machine.CanFire(trigger))
return false;

_machine.Fire(trigger);
return true;
}

public string GetStateDiagram()
{
return UmlDotGraph.Format(_machine.GetInfo());
}

// ConfigureStateMachine 見前面章節
}

5.2 SmbController 狀態機整合

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs

在 Controller 中整合狀態機:

public class SmbController : ISmbController
{
private readonly StateMachine<SmbControllerState, SmbTrigger> _machine;

public SmbController()
{
// 初始化狀態機,初始狀態為 NotInitialized
_machine = new StateMachine<SmbControllerState, SmbTrigger>(
SmbControllerState.NotInitialized);
ConfigureStateMachine();
}

private void ConfigureStateMachine()
{
// NotInitialized state
_machine.Configure(SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Configure, SmbControllerState.Ready)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Ready state
_machine.Configure(SmbControllerState.Ready)
.Permit(SmbTrigger.Start, SmbControllerState.Running)
.PermitReentry(SmbTrigger.Configure)
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Running state
_machine.Configure(SmbControllerState.Running)
.Permit(SmbTrigger.Pause, SmbControllerState.Paused)
.Permit(SmbTrigger.Stop, SmbControllerState.Stopped)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Paused state
_machine.Configure(SmbControllerState.Paused)
.Permit(SmbTrigger.Resume, SmbControllerState.Running)
.Permit(SmbTrigger.Stop, SmbControllerState.Stopped)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Stopped state
_machine.Configure(SmbControllerState.Stopped)
.Permit(SmbTrigger.Start, SmbControllerState.Running)
.Permit(SmbTrigger.Configure, SmbControllerState.Ready)
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// Error state
_machine.Configure(SmbControllerState.Error)
.Permit(SmbTrigger.Acknowledge, SmbControllerState.Ready)
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized);

// 註冊轉換回呼
_machine.OnTransitioned(OnStateMachineTransitioned);
}

// 操作方法中使用 CanFire 檢查
public void Configure(SmbConfiguration configuration)
{
if (!CanConfigure)
throw new InvalidOperationException(
$"Cannot configure in state: {_machine.State}");

// 執行設定邏輯...
_machine.Fire(SmbTrigger.Configure);
}
}

5.3 狀態圖

SmbController 狀態機的狀態轉換圖:

                         ┌─────────────┐
│NotInitialized│
└──────┬──────┘
│ Configure

┌──────────────────►┌─────┐◄──────────────────┐
│ │Ready│ │
│ └──┬──┘ │
│ │ Start │ Acknowledge
│ ┌─────────────┼─────────────┐ │
│ │ ▼ │ │
│ │ ┌───────┐ │ │
│ Reset │ Pause ◄─┤Running├─► Stop │ │
│ │ └───┬───┘ │ │
│ │ │ │ │
│ ▼ │ Resume ▼ │
│ ┌──────┐ │ ┌───────┐ │
│ │Paused├──────────┘ │Stopped├────┘
│ └──┬───┘ └───┬───┘
│ │ Stop │
│ └────────────────────────────┘
│ │
└────────────────┬───────────────────┘
│ Error (from any state)

┌─────┐
│Error│
└─────┘

6. 最佳實踐

6.1 錯誤處理

每個狀態機都應該有 Error 狀態和對應的恢復機制:

// 1. Error 狀態設計
_machine.Configure(SmbControllerState.Error)
.Permit(SmbTrigger.Acknowledge, SmbControllerState.Ready) // 確認後回到 Ready
.Permit(SmbTrigger.Reset, SmbControllerState.NotInitialized); // 或完全重置

// 2. 任何狀態都可以進入 Error
_machine.Configure(SmbControllerState.Running)
.Permit(SmbTrigger.Error, SmbControllerState.Error);

// 3. 觸發 Error 的方法
private void SetError(string message, Exception? exception = null)
{
_lastError = message;

if (_machine.CanFire(SmbTrigger.Error))
{
_machine.Fire(SmbTrigger.Error);
}

ErrorOccurred?.Invoke(this, new ErrorOccurredEventArgs
{
Message = message,
Exception = exception
});
}

6.2 非同步操作

處理非同步操作的模式:

public async Task StartAsync(CancellationToken cancellationToken = default)
{
// 1. 先檢查 CanFire
if (!CanStart)
{
throw new InvalidOperationException(
$"Cannot start from state: {_machine.State}");
}

// 2. 執行非同步操作(在 Fire 之前)
await InitializeResourcesAsync(cancellationToken);

// 3. 觸發狀態轉換
_machine.Fire(SmbTrigger.Start);

// 4. 狀態轉換後的非同步操作
if (_configuration.ShouldAutoAdvance)
{
StartControlLoop();
}
}

6.3 執行緒安全

如果狀態機會被多執行緒存取,需要加鎖:

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs:34-43

private readonly object _lock = new();

public SmbControllerState ControllerState
{
get
{
lock (_lock)
{
return _machine.State;
}
}
}

public void Configure(SmbConfiguration configuration)
{
lock (_lock)
{
if (!CanConfigure)
throw new InvalidOperationException(...);

// 設定邏輯...
_machine.Fire(SmbTrigger.Configure);
}
}

6.4 測試方式

檔案位置D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\tests\Jope.SMB.Core.Tests\Devices\DeviceConnectionStateMachineTests.cs

狀態轉換測試

[Fact]
public void Fire_Connect_FromDisconnected_ShouldTransitionToConnecting()
{
var machine = new DeviceConnectionStateMachine();

machine.Fire(ConnectionTrigger.Connect);

Assert.Equal(ConnectionState.Connecting, machine.State);
}

[Fact]
public void Fire_Connect_FromConnecting_ShouldThrow()
{
var machine = new DeviceConnectionStateMachine();
machine.Fire(ConnectionTrigger.Connect);

Assert.Throws<InvalidOperationException>(() =>
machine.Fire(ConnectionTrigger.Connect));
}

CanFire 測試

[Fact]
public void InitialState_CanConnect_ShouldBeTrue()
{
var machine = new DeviceConnectionStateMachine();

Assert.True(machine.CanConnect);
}

[Fact]
public void Connected_CanConnect_ShouldBeFalse()
{
var machine = new DeviceConnectionStateMachine();
machine.Fire(ConnectionTrigger.Connect);
machine.Fire(ConnectionTrigger.Success);

Assert.False(machine.CanConnect);
}

事件測試

[Fact]
public void StateChanged_ShouldBeRaisedOnTransition()
{
var machine = new DeviceConnectionStateMachine();
ConnectionStateChangedEventArgs? eventArgs = null;
machine.StateChanged += (s, e) => eventArgs = e;

machine.Fire(ConnectionTrigger.Connect);

Assert.NotNull(eventArgs);
Assert.Equal(ConnectionState.Disconnected, eventArgs.PreviousState);
Assert.Equal(ConnectionState.Connecting, eventArgs.CurrentState);
Assert.Equal(ConnectionTrigger.Connect, eventArgs.Trigger);
}

完整流程測試

[Fact]
public void FullCycle_ConnectFailRetryConnect_ShouldWork()
{
var machine = new DeviceConnectionStateMachine();

machine.Fire(ConnectionTrigger.Connect);
machine.Fire(ConnectionTrigger.Failure, "First attempt failed");
Assert.Equal(ConnectionState.Error, machine.State);

machine.Fire(ConnectionTrigger.Retry);
Assert.Equal(ConnectionState.Connecting, machine.State);

machine.Fire(ConnectionTrigger.Success);
Assert.Equal(ConnectionState.Connected, machine.State);
}

7. 參考資源

7.1 Stateless 官方資源

7.2 相關 Linear Issues

Issue說明
JOP-62導入 Stateless 狀態機框架
JOP-63SmbController 狀態機重構
JOP-64設備連線狀態機抽象化
JOP-65ViewModel CanFire 綁定
JOP-67WalBufferStateMachine(規劃中)

7.3 專案內參考檔案

檔案完整路徑
ConnectionStateD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionState.cs
ConnectionTriggerD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionTrigger.cs
DeviceConnectionStateMachineD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs
SmbTriggerD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbTrigger.cs
SmbControllerStateD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\Models\SmbState.cs
SmbControllerD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs
SmbControlPanelViewModelD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs
TestsD:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\tests\Jope.SMB.Core.Tests\Devices\DeviceConnectionStateMachineTests.cs

版本歷史

版本日期變更
1.02026-01-20初版建立