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}State | ConnectionState, SmbControllerState |
| Trigger enum | {Feature}Trigger | ConnectionTrigger, SmbTrigger |
| 狀態機類別 | {Feature}StateMachine | DeviceConnectionStateMachine |
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 官方資源
- GitHub: https://github.com/dotnet-state-machine/stateless
- NuGet: https://www.nuget.org/packages/Stateless
7.2 相關 Linear Issues
| Issue | 說明 |
|---|---|
| JOP-62 | 導入 Stateless 狀態機框架 |
| JOP-63 | SmbController 狀態機重構 |
| JOP-64 | 設備連線狀態機抽象化 |
| JOP-65 | ViewModel CanFire 綁定 |
| JOP-67 | WalBufferStateMachine(規劃中) |
7.3 專案內參考檔案
| 檔案 | 完整路徑 |
|---|---|
| ConnectionState | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionState.cs |
| ConnectionTrigger | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\ConnectionTrigger.cs |
| DeviceConnectionStateMachine | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Devices\DeviceConnectionStateMachine.cs |
| SmbTrigger | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbTrigger.cs |
| SmbControllerState | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\Models\SmbState.cs |
| SmbController | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.Core\Smb\SmbController.cs |
| SmbControlPanelViewModel | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\src\Jope.SMB.WPF\ViewModels\Control\SmbControlPanelViewModel.cs |
| Tests | D:\WorkSpace\GatherTech\喬璞科技\Jope.SMB.App\tests\Jope.SMB.Core.Tests\Devices\DeviceConnectionStateMachineTests.cs |
版本歷史
| 版本 | 日期 | 變更 |
|---|---|---|
| 1.0 | 2026-01-20 | 初版建立 |