An Embedded Engineer’s Blog

とある組み込みエンジニアの備忘録的なブログです。

UMLのステートマシン図を実装する for C# - その7

まえがき

ステートマシンをGUI(WPF)で動作させるアプリケーションを実装していきます。

f:id:an-embedded-engineer:20190414174321p:plain
エアコンステートマシン


事前準備

今回は、WPFMVVMリアクティブプログラミングGUIアプリケーションを実装しようと思います。


MVVMのフレームワークにはLivetを使用します。 こちらからプロジェクトテンプレート(Visual Studio 2017用)をダウンロードして、インストールしてください。


インストールしたLivetプロジェクトテンプレートを使用してWPFアプリケーション用プロジェクトを作成します。


また、リアクティブプログラミングを実現するために、NuGetからReactive Extension(System.Reactive)ReactivePropertyWPFプロジェクトにインストールします。


NotificationObject クラス

MVVMではプロパティ(=データ)が変更されたことをUIに通知するためにINotifyPropertyChanged インタフェースを実装する必要があります。
そのため、INotifyPropertyChanged インタフェースを実装したベースクラスとして、NotificationObject クラスを実装します。

using System.ComponentModel;

public class NotificationObject : INotifyPropertyChanged
{
    // プロパティ変更イベントハンドラ
    public event PropertyChangedEventHandler PropertyChanged;

    // プロパティ変更イベント通知
    protected void RaisePropertyChanged(string name)
    {
        // プロパティ変更イベントハンドラ呼び出し(登録されていたら)
        this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
    }
}


プロパティ変更通知処理

エアコンステートマシンおよびエアコンモデルにおいて、GUIに表示するために、値の変更通知が必要なプロパティを変更します。 値の変更を通知できるようにするために、上記で実装したNotificationObjectを継承します。

StateMachine クラス

StateMachine クラスでは現在の状態を示すCurrentStateプロパティに、プロパティ変更イベント通知処理を組み込みます。

public abstract class StateMachine : NotificationObject


// 現在状態(データ本体)
private State _CurrentState { get; set; }

// 現在状態(プロパティ変更イベント通知用)
public State CurrentState
{
    get { return this._CurrentState; }
    set
    {
        // 現在の状態と異なる値が代入された
        if (this._CurrentState != value)
        {
            // 現在の状態を更新
            this._CurrentState = value;
            // プロパティ値の変更を通知
            this.RaisePropertyChanged(nameof(this.CurrentState));
        }
    }
}


AirConditioner クラス

同様に、AirConditioner クラスにおいても値の変更通知が必要なプロパティに、、プロパティ変更イベント通知処理を組み込みます。

public class AirConditioner : NotificationObject


// 現在温度(本体)
private int _Temperature { get; set; }

// 現在温度(プロパティ変更イベント通知用)
public int Temperature
{
    get { return this._Temperature; }
    set
    { 
        if (this._Temperature != value)
        {
            this._Temperature = value;
            this.RaisePropertyChanged(nameof(this.Temperature));
        }
    }
}

// 目標温度(本体)
private int _TargetTemperature { get; set; }

// 目標温度(プロパティ変更イベント通知用)
public int TargetTemperature
{
    get { return this._TargetTemperature; }
    set
    {
        if (this._TargetTemperature != value)
        {
            this._TargetTemperature = value;
            this.RaisePropertyChanged(nameof(this.TargetTemperature));
        }
    }
}

// 現在湿度(本体)
private int _Humidity { get; set; }

// 現在湿度(プロパティ変更イベント通知用)
public int Humidity
{
    get { return this._Humidity; }
    set
    {
        if (this._Humidity != value)
        {
            this._Humidity = value;
            this.RaisePropertyChanged(nameof(this.Humidity));
        }
    }
}


MainWindow クラス(Xaml)

プロジェクト生成時に自動的に生成されたMainWindow.xamlを編集してGUIの画面設計をしていきます。

<Window x:Class="StateMachineSample.WPF.Views.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
        xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
        xmlns:v="clr-namespace:StateMachineSample.WPF.Views"
        xmlns:vm="clr-namespace:StateMachineSample.WPF.ViewModels"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="Button">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="TextBlock">
            <Setter Property="FontSize" Value="20"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
            <Setter Property="Margin" Value="5"/>
        </Style>
        <Style TargetType="ListBoxItem">
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
        </Style>
        <Style TargetType="StatusBarItem">
            <Setter Property="FontSize" Value="15"/>
            <Setter Property="FontFamily" Value="メイリオ"/>
        </Style>
    </Window.Resources>

    <Window.DataContext>
        <vm:MainWindowViewModel/>
    </Window.DataContext>

    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Closed">
            <l:DataContextDisposeAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/> <!-- Command Buttons -->
            <RowDefinition Height="Auto"/> <!-- Target Temp Slider -->
            <RowDefinition Height="Auto"/> <!-- Latest Message -->
            <RowDefinition Height="*"/> <!-- Message Logs -->
            <RowDefinition Height="Auto"/> <!-- Status -->
        </Grid.RowDefinitions>

        <Grid Grid.Row="0">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <!-- ストップボタン -->
            <Button Grid.Column="0" Content="Stop" Command="{Binding StopCommand}" />
            <!-- スタートボタン -->
            <Button Grid.Column="1" Content="Start" Command="{Binding StartCommand}" />
            <!-- 冷房ボタン -->
            <Button Grid.Column="2" Content="Cool" Command="{Binding CoolCommand}" />
            <!-- 暖房ボタン -->
            <Button Grid.Column="3" Content="Heat" Command="{Binding HeatCommand}" />
            <!-- 除湿ボタン -->
            <Button Grid.Column="4" Content="Dry" Command="{Binding DryCommand}" />
            <!-- クリーニングボタン -->
            <Button Grid.Column="5" Content="Clean" Command="{Binding CleanCommand}" />
        </Grid>
        <Grid Grid.Row="1">
            <StackPanel Orientation="Horizontal">
                <!-- 目標温度変更スライドバー -->
                <TextBlock Text="Target Temperature : " />
                <Slider Value="{Binding TargetTemperature.Value}" Width="100"
                        VerticalAlignment="Center"
                        Minimum="{Binding MinTargetTemperature}"
                        Maximum="{Binding MaxTargetTemperature}"/>
                <!-- 目標温度アップボタン -->
                <Button Content="Up" Command="{Binding UpCommand}" Width="80" />
                <!-- 目標温度ダウンボタン -->
                <Button Content="Down" Command="{Binding DownCommand}" Width="80" />
            </StackPanel>
        </Grid>
        <Grid Grid.Row="2">
            <StackPanel Orientation="Horizontal">
                <!-- 最新受信メッセージ -->
                <TextBlock Text="Message : " />
                <TextBlock Text="{Binding Message.Value}" />
            </StackPanel>
        </Grid>
        <Grid Grid.Row="3">
            <!-- 受信メッセージログ -->
            <ListBox ItemsSource="{Binding MessageLog}" Margin="5"
                     ScrollViewer.HorizontalScrollBarVisibility="Auto" 
                     ScrollViewer.VerticalScrollBarVisibility="Visible"/>
        </Grid>
        <Grid Grid.Row="4">
            <StatusBar>
                <!-- 現在のステートマシン状態表示 -->
                <StatusBarItem Content="状態:" />
                <StatusBarItem Content="{Binding Status.Value}" />
                <Separator/>
                <!-- 目標温度表示 -->
                <StatusBarItem Content="目標温度:" />
                <StatusBarItem Content="{Binding TargetTemp.Value}" />
                <Separator/>
                <!-- 現在の温度表示 -->
                <StatusBarItem Content="温度:" />
                <StatusBarItem Content="{Binding Temperature.Value}" />
                <Separator/>
                <!-- 現在の湿度表示 -->
                <StatusBarItem Content="湿度:" />
                <StatusBarItem Content="{Binding Humidity.Value}" />
            </StatusBar>
        </Grid>
    </Grid>
</Window>


上記のようにxamlファイルを編集すると、以下のようなデザインの画面になります。

f:id:an-embedded-engineer:20190502232943p:plain:w500
GUIサンプル


MainWindowViewModel クラス

最後に、ViewModelの実装を行っていきます。
ViewModelはView(MainWindow)に描画するための状態やデータの保持と、Viewから受け取った入力(ボタン押下、テキスト入力など)を適切な形式に変換し、Model(エアコンモデル、ステートマシン)に伝達する役割を持ちます。


今回は、ViewとModelの仲介方法として、Reactive ExtensionおよびReactivePropertyを使用したリアクティブプログラミングを使用します。
リアクティブプログラミングを用いる事によって、以下のような処理の扱いが非常に簡単になります。

  • GUIによる入出力
  • 時間経過で状態が変換するもの
  • 非同期の通信処理


Model

MVVMで言うところのModelとは、アプリケーショのドメイン(問題領域)を解決するために、そのアプリケーションが扱う領域のデータと手続き(ビジネスロジック)を表現する要素のことを言います。
今回のサンプルでは、エアコンモデル(AirConditioner)やエアコンステートマシンが該当します。

// エアコンモデル
public AirConditioner Model { get; }

// エアコンステートマシン
public ModelStateMachine StateMachine { get; }


データバインディングプロパティ

WPFのデータバインディングによってGUIに表示するデータを保持するプロパティを定義します。
リアルタイムで値が変化するプロパティはReactivePropertyを使用します。
ReactivePropertyを使用することによって、データの値が変化したときにその変更がUIに通知され、Viewで表示している値に反映されます。

// 受信メッセージ
public ReactiveProperty<string> Message { get; }

// 受信メッセージログ
public ReadOnlyReactiveCollection<string> MessageLog { get; }

// 現在の状態
public ReactiveProperty<State> CurrentState { get; }

// 最大目標温度
public int MaxTargetTemperature { get; }

// 最小目標温度
public int MinTargetTemperature { get; }

// 目標温度
public ReactiveProperty<int> TargetTemperature { get; }

// 現在の状態(ステータスバー表示用)
public ReactiveProperty<string> Status { get; }

// 目標温度(ステータスバー表示用)
public ReactiveProperty<string> TargetTemp { get; }

// 現在の温度(ステータスバー表示用)
public ReactiveProperty<string> Temperature { get; }

// 現在の湿度(ステータスバー表示用)
public ReactiveProperty<string> Humidity { get; }


コマンド

ユーザからの入力を変換してModelに伝達するためのコマンドを定義します。
コマンドはReactiveCommandを使用します。
ReactiveCommandを使用することで、ReactivePropertyで変化したデータの値に応じてコマンドの使用可否(ボタンの有効/無効)などを自動的に切り替えることができます。

// ストップコマンド
public ReactiveCommand StopCommand { get; }

// スタートコマンド
public ReactiveCommand StartCommand { get; }

// 冷房コマンド
public ReactiveCommand CoolCommand { get; }

// 暖房コマンド
public ReactiveCommand HeatCommand { get; }

// 除湿コマンド
public ReactiveCommand DryCommand { get; }

// クリーニングコマンド
public ReactiveCommand CleanCommand { get; }

// 目標温度アップコマンド
public ReactiveCommand UpCommand { get; }

// 目標温度ダウンコマンド
public ReactiveCommand DownCommand { get; }


コンストラク

今回のサンプルは、すべての処理の定義がコンストラクタ内で完結します。 ReactivePropertyやReactiveCommandを使用することで、データの変化に対する処理やユーザからの入力に対する処理を「宣言的」に記述することができます。

// コンストラクタ
public MainWindowViewModel()
{
}


固定値設定

時間的に変化しない固定値プロパティをセットします。
今回は、目標温度のリミッタに使用する最大/最小値をセットします。

// 最大目標温度
this.MaxTargetTemperature = AirConditioner.MaxTargetTemperature;

// 最小目標温度
this.MinTargetTemperature = AirConditioner.MinTargetTemperature;


ステータス情報初期化

ステータスバーに表示する各種情報(状態、温度、湿度など)を空の文字列を初期値とし、string型を保持するReactivePropertyとして宣言します。

this.Status = new ReactiveProperty<string>("");

this.TargetTemp = new ReactiveProperty<string>("");

this.Temperature = new ReactiveProperty<string>("");

this.Humidity = new ReactiveProperty<string>("");


受信メッセージロギング

ステートマシンから送信されるメッセージをロギングする処理を記述します。


最新受信メッセージは初期値空(null)のstring型を保持するReactivePropertyとして宣言します。
初期値をnullとして宣言する理由は、空文字("")にしてしまうと、受信メッセージログに変換した際に、空行が含まれてしまうためです。

// 最新受信メッセージ初期化
this.Message = new ReactiveProperty<string>();


受信メッセージログは、最新受信メッセージを、受け取った順番に保持するReactiveCollectionに変換して使用します。

// 最新受信メッセージをReactive Collectionに変換し、受信メッセージログにする
this.MessageLog = this.Message.ToReadOnlyReactiveCollection();


Messengerの受信イベントハンドラには、受信したメッセージ文字列を最新受信メッセージプロパティにセットする処理を記述します。
こうすることで、ステートマシンからメッセージが送信されると、最新受信メッセージプロパティに受信メッセージがセットされ、受信したメッセージがログとしてリスト化されるようになります。

using StmMessenger = StateMachineSample.Lib.Messenger;

// メッセージ受信イベントハンドラ登録
StmMessenger.OnMessageReceived += (message) =>
{
    // 最新受信メッセージに受信したメッセージ文字列をセット
    this.Message.Value = message;
};


Model初期化

今回のModelにあたるエアコンモデル(AirConditioner)とエアコンステートマシンのインスタンスを生成します。

// エアコンモデルインスタンス生成
this.Model = new AirConditioner();

// エアコンステートマシンインスタンス生成
this.StateMachine = new ModelStateMachine(this.Model);


Model - Viewデータ接続

ReactivePropertyを用いて、Modelの状態を監視し、ModelとViewのデータ接続を行います。


エアコンステートマシンの現在の状態を監視し、ReactivePropertyに変換します。
これによって、ステートマシンの状態が変化するたびにViewModelのCurrentStateが更新されます。
CurrentStateの値は、各種コマンドの実行可否(ボタンの有効/無効)制御に使用します。

// エアコンステートマシンの現在状態を監視し、ReactivePropertyに変換(Model -> View単方向)
this.CurrentState = this.StateMachine.ObserveProperty(stm => stm.CurrentState).ToReactiveProperty();


エアコンモデルの目標温度を監視し、ReactivePropertyに変換します。 目標温度はModelからViewへの変更通知(プログラム上から目標温度変更)とViewからModelへの変更通知(ユーザ入力から目標温度変更)が必要となるため、双方向の監視が必要になります。

// エアコンモデルの目標温度を監視し、ReactivePropertyに変換(Model <-> View双方向)
this.TargetTemperature = this.Model.ToReactivePropertyAsSynchronized(model => model.TargetTemperature);


コマンド宣言

ユーザからの入力を受け付けるコマンドを宣言します。
ReactiveCommandは、宣言時に実行可否を切り替える条件を指定することができます。
今回の場合は、実行可能な場合は、対応するボタンが押下可能な状態になり、実行不可能な状態になっている場合は、ボタンが押下不可能になることで、コマンドを実行できない状態になります。

// 現在の状態がRunning状態またはClean状態の時に実行可能なストップコマンドを宣言
this.StopCommand = this.CurrentState.Select(s => s is RunningState || s is CleanState).ToReactiveCommand();

// 現在の状態がStop状態の時に実行可能なスタートコマンドを宣言
this.StartCommand = this.CurrentState.Select(s => s is StopState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な冷房コマンドを宣言
this.CoolCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な暖房コマンドを宣言
this.HeatCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能な除湿コマンドを宣言
this.DryCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の状態がRunning状態の時に実行可能なクリーニングコマンドを宣言
this.CleanCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

// 現在の目標温度が最大目標温度より小さい場合に実行可能な目標温度アップコマンドを宣言
this.UpCommand = this.TargetTemperature.Select(v => v < this.MaxTargetTemperature).ToReactiveCommand();

// 現在の目標温度が最小目標温度より大きい場合に実行可能な目標温度ダウンコマンドを宣言
this.DownCommand = this.TargetTemperature.Select(v => v > this.MinTargetTemperature).ToReactiveCommand();


コマンド処理定義

コマンドが実行された時の処理を定義します。
今回は、ステートマシンのトリガ送信や、エアコンモデルの目標温度変更を実行するようにします。

// ストップコマンドが実行されたら、エアコンステートマシンにSwitchStopトリガを送信
this.StopCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStopTrigger.Instance));

// スタートコマンドが実行されたら、エアコンステートマシンにSwitchStartトリガを送信
this.StartCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStartTrigger.Instance));

// 冷房コマンドが実行されたら、エアコンステートマシンにSwitchCoolトリガを送信
this.CoolCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCoolTrigger.Instance));

// 暖房コマンドが実行されたら、エアコンステートマシンにSwitchHeatトリガを送信
this.HeatCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchHeatTrigger.Instance));

// 除湿コマンドが実行されたら、エアコンステートマシンにSwitchDryトリガを送信
this.DryCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchDryTrigger.Instance));

// クリーニングコマンドが実行されたら、エアコンステートマシンにSwitchCleanトリガを送信
this.CleanCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCleanTrigger.Instance));

// 目標温度アップコマンドが実行されたら、エアコンモデルに目標温度アップを要求
this.UpCommand.Subscribe(_ => this.Model.Up());

// 目標温度ダウンコマンドが実行されたら、エアコンモデルに目標温度ダウンプを要求
this.DownCommand.Subscribe(_ => this.Model.Down());


周期処理

周期的にModelの状態を監視し、Viewに反映させる処理を実装します。
今回はReactive Extensionを使用して100msごとに呼び出されるインターバルタイマを作成し、ステータスバーに表示する各種状態値を更新と、ステートマシンの定常処理を実行するようにします。
Modelで保持されている状態値をユーザが見やす形式に変換する(単位をつける、いくつかの情報を結合するなど)のもViewModelの役割です。

// 100ms間隔のインターバルタイマ生成
var interval = Observable.Interval(TimeSpan.FromMilliseconds(100));

// タイマで周期実行する処理の定義
var timer_sub = interval.Subscribe(
    i =>
    {
        // ステートマシンの現在の状態がRunning状態
        if (this.StateMachine.CurrentState is RunningState running_state)
        {
            // サブステートマシンを取得
            var sub = running_state.SubContext;

            // 現在の状態(メイン状態 - サブ状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
        }
        // ステートマシンの現在の状態がClean状態
        else if (this.StateMachine.CurrentState is CleanState clean_state)
        {
            // サブステートマシンを取得
            var sub = clean_state.SubContext;

            // 現在の状態(メイン状態 - サブ状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
        }
        else
        {
            // 現在の状態(メイン状態)を文字列に変換してセット
            this.Status.Value = $"{this.StateMachine.CurrentState}";
        }

        // 目標温度を文字列に変換してセット
        this.TargetTemp.Value = $"{this.Model.TargetTemperature}[℃]";
        // 現在の温度を文字列に変換してセット
        this.Temperature.Value = $"{this.Model.Temperature}[℃]";
        // 現在の温度を文字列に変換してセット
        this.Humidity.Value = $"{this.Model.Humidity}[%]";

        // ステートマシンの定常処理実行
        this.StateMachine.Update();
    });


全体

ソース全体を示します。

public class MainWindowViewModel : ViewModel
{
    // エアコンモデル
    public AirConditioner Model { get; }

    // エアコンステートマシン
    public ModelStateMachine StateMachine { get; }

    // 受信メッセージ
    public ReactiveProperty<string> Message { get; }

    // 受信メッセージログ
    public ReadOnlyReactiveCollection<string> MessageLog { get; }

    // 現在の状態
    public ReactiveProperty<State> CurrentState { get; }

    // 最大目標温度
    public int MaxTargetTemperature { get; }

    // 最小目標温度
    public int MinTargetTemperature { get; }

    // 目標温度
    public ReactiveProperty<int> TargetTemperature { get; }

    // 現在の状態(ステータスバー表示用)
    public ReactiveProperty<string> Status { get; }

    // 目標温度(ステータスバー表示用)
    public ReactiveProperty<string> TargetTemp { get; }

    // 現在の温度(ステータスバー表示用)
    public ReactiveProperty<string> Temperature { get; }

    // 現在の湿度(ステータスバー表示用)
    public ReactiveProperty<string> Humidity { get; }

    // ストップコマンド
    public ReactiveCommand StopCommand { get; }

    // スタートコマンド
    public ReactiveCommand StartCommand { get; }

    // 冷房コマンド
    public ReactiveCommand CoolCommand { get; }

    // 暖房コマンド
    public ReactiveCommand HeatCommand { get; }

    // 除湿コマンド
    public ReactiveCommand DryCommand { get; }

    // クリーニングコマンド
    public ReactiveCommand CleanCommand { get; }

    // 目標温度アップコマンド
    public ReactiveCommand UpCommand { get; }

    // 目標温度ダウンコマンド
    public ReactiveCommand DownCommand { get; }

    // コンストラクタ
    public MainWindowViewModel()
    {
        this.MaxTargetTemperature = AirConditioner.MaxTargetTemperature;

        this.MinTargetTemperature = AirConditioner.MinTargetTemperature;

        this.Status = new ReactiveProperty<string>("");

        this.TargetTemp = new ReactiveProperty<string>("");

        this.Temperature = new ReactiveProperty<string>("");

        this.Humidity = new ReactiveProperty<string>("");

        this.Message = new ReactiveProperty<string>();

        this.MessageLog = this.Message.ToReadOnlyReactiveCollection();

        StmMessenger.OnMessageReceived += (message) =>
        {
            this.Message.Value = message;
        };

        this.Model = new AirConditioner();

        this.StateMachine = new ModelStateMachine(this.Model);

        this.CurrentState = this.StateMachine.ObserveProperty(stm => stm.CurrentState).ToReactiveProperty();

        this.TargetTemperature = this.Model.ToReactivePropertyAsSynchronized(model => model.TargetTemperature);

        this.StopCommand = this.CurrentState.Select(s => s is RunningState || s is CleanState).ToReactiveCommand();

        this.StartCommand = this.CurrentState.Select(s => s is StopState).ToReactiveCommand();

        this.CoolCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.HeatCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.DryCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.CleanCommand = this.CurrentState.Select(s => s is RunningState).ToReactiveCommand();

        this.UpCommand = this.TargetTemperature.Select(v => v < this.MaxTargetTemperature).ToReactiveCommand();

        this.DownCommand = this.TargetTemperature.Select(v => v > this.MinTargetTemperature).ToReactiveCommand();


        this.StopCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStopTrigger.Instance));

        this.StartCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchStartTrigger.Instance));

        this.CoolCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCoolTrigger.Instance));

        this.HeatCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchHeatTrigger.Instance));

        this.DryCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchDryTrigger.Instance));

        this.CleanCommand.Subscribe(_ => this.StateMachine.SendTrigger(SwitchCleanTrigger.Instance));

        this.UpCommand.Subscribe(_ => this.Model.Up());

        this.DownCommand.Subscribe(_ => this.Model.Down());


        var interval = Observable.Interval(TimeSpan.FromMilliseconds(100));

        var timer_sub = interval.Subscribe(
            i =>
            {
                if (this.StateMachine.CurrentState is RunningState running_state)
                {
                    var sub = running_state.SubContext;

                    this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
                }
                else if (this.StateMachine.CurrentState is CleanState clean_state)
                {
                    var sub = clean_state.SubContext;

                    this.Status.Value = $"{this.StateMachine.CurrentState} - {sub.CurrentState}";
                }
                else
                {
                    this.Status.Value = $"{this.StateMachine.CurrentState}";
                }

                this.TargetTemp.Value = $"{this.Model.TargetTemperature}[℃]";
                this.Temperature.Value = $"{this.Model.Temperature}[℃]";
                this.Humidity.Value = $"{this.Model.Humidity}[%]";

                this.StateMachine.Update();
            });
    }
}


まとめ

今回で、「C#UMLのステートマシン図の実装」は完成となります。


ソースコード一式はGitHubにアップしています。
https://github.com/an-embedded-engineer/StateMachineSample