ソフトウェア

C# WPFアプリ XAMLのトリガー→アクションを理解する

はじめに

今回はC# WPFアプリケーションのXAMLのトリガー→アクションについて理解しましょう。
WPFアプリケーション開発を行う上で必要不可欠なものです。
中にはガーベージコレクションがあるからメモリの解放漏れは心配しなくていいという人もいますが、実際は、C#で作ったアプリでも作り方を誤るとメモリリークは発生します。
そしてこの確実なメモリの解放を行うためにも、トリガーとアクションの関係の理解は必要不可欠です。
このトリガー→アクションを理解することで、XAML自体の理解もさらに深まります。
私はトリガー→アクションの理解ができた後は、XAMLのみならず、WPFアプリケーションの理解が急激に深まった感覚を覚えました。

概念図

イベントが発生して、何かの処理をする。
アプリケーションというよりもあらゆるソフトウェアでよく使う方法です。
この仕組みを図で理解することで、WPFアプリの理解が深まります。

まず、上の図で伝えたいことですが、
ViewでViewまたはViewModelからのトリガーを受信・発火後、
アクション・バインドされたViewModelのメソッドなどを実行する

です。
感覚をつかむために図は効果的です。

イベント・アクションの種類

イベント・アクションの種類についてまとめました。
今回は、.Netに加えて、WPFアプリ開発でポピュラーなLivetを使った場合を想定してまとめてみました。

【イベントの種類】

ビヘイビア・メッセージ・イベントトリガー
トリガーの種類(タグ名 / クラス名)
※名前空間
トリガーの発火場所 トリガー名称例(イベント名、メッセージ名等)
<i:Interaction.Behaviors>
※System.Windows.Interactivity
View トリガ特性を含むアクション
****Behavior
WindowCloseCancelBehavior
PasswordBoxSetStateToControlBehavior
・・・
<l:InteractionMessageTrigger … ">
※Livet.Behaviors.Messaging
ViewModel MessageKeyで指定
<i:EventTrigger EventName="… ">
※System.Windows.Interactivity
View マウスイベント
MouseDoubleClick
MouseDown
MouseUp
MouseEnter
MouseLeave
MouseMove

【アクションの種類】
Livet関連です。

Livet関連のアクション
トリガーの種類(タグ名 / クラス名) アクションの種類(名前空間) アクション例
<i:Interaction.Behaviors> Livet.Behaviors WindowCloseCancelBehavior他
<l:InteractionMessageTrigger … "> Livet.Behaviors.Messaging TransitionInteractionMessageAction他
<i:EventTrigger EventName="… "> Livet.Behaviors LivetCallMethodAction他
DataContextDisposeAction他

.Net関連です。

.Net関連のアクション
トリガーの種類(タグ名 / クラス名) アクションの種類(名前空間) アクション例
<i:Interaction.Behaviors> なし なし
<l:InteractionMessageTrigger … "> System.Windows.Interactivity InvokeCommandActionのみ
<i:EventTrigger EventName="… "> System.Windows.Interactivity InvokeCommandActionのみ

主なイベントとアクションの種類は上のとおりです。

ここでイベントの種類のところでビヘイビアという聞きなれない単語が出てきましたが、これもイベントです。
イベントトリガーとの違いは
ビヘイビアはある特定のイベントのみに対応する処理を書くもの
イベントトリガーはイベントの種類に関係のない処理を書くもの
もう少し、わかりやすく表現するとビヘイビアはビヘイビア名がイベント内容と1対1で対応しています。
対して、イベントトリガーはイベント名を切り替えることで、ターゲットのイベントの種類を切り替えることができます。

サンプルリストで具体的にビヘイビアとイベントトリガーの違いをみていきましょう。

サンプルリスト

ご紹介するサンプルリスト

  • ビヘイビアの場合
    • Livetライブラリを利用する場合
      ①画面を閉じる時にバインドするViewModel側で定義したイベント関数が呼ばれるようにする
      ②ViewModelからViewへメッセージを送信して画面を表示する
  • イベントトリガーの場合
    • .Netライブラリ及びLivetライブラリを利用する場合
      ③画面の表示が完了する時にバインドするViewModel側で定義した画面表示完了イベント関数及びオリジナルのイベント関数が呼ばれるようにする
    • Livetライブラリを利用する場合
      ④画面を閉じるときにViewModel側のメモリ解放用メソッド、Disposeが呼ばれるようにする

では順番にご紹介していきます。
とはいいつつ、説明はマニュアル的で、もの足りないかもしれません。
理由は細かいイベント名など覚えてもあまり意味がないからです。
意味がない理由は本節の最後にお話しします。
XAMLで書かれたView側のコードとC#で書かれたViewModel側のコードの関係性、連携がなんとなくイメージしていただければ十分です。

①Livetライブラリで画面を閉じる時にバインドするViewModel側で定義したイベント関数が呼ばれるようにする

View(XAMLファイル)側です。

<Window x:Class="TestClient.Views.MainWindow"
    …
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    …
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
    …>
    <i:Interaction.Behaviors>
    <l:WindowCloseCancelBehavior CanClose="{Binding CanClose, Mode=OneWay}"	
        CloseCanceledCallbackMethodTarget="{Binding}"
        CloseCanceledCallbackMethodName="CloseCanceledCallback" />
    </i:Interaction.Behaviors>

【クラス名】Interaction.Behaviors
【名前空間】System.Windows.Interactivity
【意味】このプロパティは、パブリックの Behavior がアタッチされたプロパティ用の内部バッキング ストアとして使用されます

【クラス名】WindowCloseCancelBehavior
【名前空間】Livet.Behaviors
【意味】WindowのClose処理をキャンセルし、キャンセルした事をCallback通知可能なビヘイビアです。

CloseCanceledCallbackMethodTarget="{Binding}"のBindingには現在、バインド中のViewModelが設定されます。

ViewModel(csファイル)側です。

namespace TestClient.ViewModels	
{
    …
    public class MainWindowViewModel : BaseViewModel, IDisposable
    {
        …
        public void CloseCanceledCallback()
        {
            …
        }
            …
    }
}

View側で定義したCloseCanceledCallbackの定義を行っています。

②LivetライブラリでViewModelからViewへメッセージを送信して画面を表示する
View(XAMLファイル)側です。

<Window x:Class="TestClient.Views.MainWindow"
    …
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    …
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"…>	
    <l:InteractionMessageTrigger MessageKey="Open" Messenger="{Binding Messenger}">
    <l:TransitionInteractionMessageAction InvokeActionOnlyWhenWindowIsActive="false"/>
    </l:InteractionMessageTrigger>
    ...
</Window>

【クラス名】InteractionMessageTrigger
【名前空間】Livet.Behaviors.Messaging
【意味】ViewModelからの相互作用メッセージを受信し、アクションを実行します。

【クラス名】TransitionInteractionMessageAction
【名前空間】Livet.Behaviors.Messaging
【意味】画面遷移(Window)を行うアクションです。TransitionMessageに対応します。

ViewModel(csファイル)側です。

namespace TestClient.ViewModels
{
    …
    public class MainWindowViewModel : BaseViewModel, IDisposable
    {
        …
        public void Testing(string parameter)
        {
            var testingVM = new TestWindowViewModel(int.Parse(parameter));
            var message = new TransitionMessage(typeof(Views.TestWindow), testtingVM, TransitionMode.Modal, "Open");
            Messenger.Raise(message);
            …
        }
        …
    }
}

View側に対して、TransitionMessageを送信しています。
Messenger.Raiseをコールしているところです。

③.Netライブラリ及びLivetライブラリで画面の表示が完了する時にバインドするViewModel側で定義した画面表示完了イベント関数及びオリジナルのイベント関数が呼ばれるようにする

View(XAMLファイル)側です。

<Window x:Class="TestClient.Views.MainWindow"
    …
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    …
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
    …>
    …
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="ContentRendered">
            <i:InvokeCommandAction Command="{Binding LoadingCommand}" CommandParameter="{Binding Mode=OneTime, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type Window}}}"/>
            <l:LivetCallMethodAction MethodTarget="{Binding}" MethodName="Initialize"/>	
        </i:EventTrigger>
        …
    </i:Interaction.Triggers>
</Window>

【クラス名】Interaction.Triggers
【名前空間】System.Windows.Interactivity
【意味】このプロパティは、パブリックの Trigger がアタッチされたプロパティ用の内部バッキング ストアとして使用されます。

【クラス名】EventTrigger
【名前空間】System.Windows.Interactivity
【意味】ソース上の指定イベントをリッスンし、そのイベントが起動されると発生するトリガーです。

【クラス名】InvokeCommandAction
【名前空間】System.Windows.Interactivity
【意味】呼び出されると、指定の ICommand を実行します。

【クラス名】LivetCallMethodAction
【名前空間】Livet.Behaviors
【意味】引数を一つだけ持つメソッドに対応したCallMethodActionです。

上のInvokeCommandActionとLivetCallMethodActionは順番に実行されます。
非同期ではありません

ViewModel(csファイル)側です。

namespace TestClient.ViewModels
{
    …
    public class MainWindowViewModel : BaseViewModel, IDisposable
    {
        …
        #region LoadingCommand
        private ListenerCommand _LoadingCommand;
		
        public ListenerCommand LoadingCommand {
            get {
                if (_LoadingCommand == null) {
                    _LoadingCommand = new ListenerCommand(Loading);
                }
                return _LoadingCommand;
            }
        }
		
        public void Loading(Window parameter)
        {
            …
        }
        #endregion
        …
        public void Initialize()
        {
            …
        }
        …
    }
}

表示が完了、ContentRenderedイベントが発生したときに実行する関数LoadingCommandとInitializeの定義を行っています。

④Livetライブラリで画面を閉じるときにViewModel側のメモリ解放用メソッド、Disposeが呼ばれるようにする

View(XAMLファイル)側です。

<Window x:Class="TestClient.Views.MainWindow"
    …
    xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
    …
    xmlns:l="http://schemas.livet-mvvm.net/2011/wpf"
    …>
    …
    <i:EventTrigger EventName="Closed">
        <l:DataContextDisposeAction/>
    </i:EventTrigge>
    ...
</Window>

【クラス名】DataContextDisposeAction
【名前空間】Livet.Behaviors
【意味】アタッチしたオブジェクトのDataContextがIDisposableである場合、Disposeします。

ViewModel(csファイル)側です。

namespace TestClient.ViewModels
{
    …
    public class MainWindowViewModel : BaseViewModel, IDisposable
    {
        …
        new public void Dispose()
        {
            // 基本クラスのDispose()でCompositeDisposableに登録されたイベントを解放する。
            base.Dispose();
		
            Dispose( true );
        }
        …
    }
    …
}

画面を閉じるときにのDispose関数を定義しています。

ここでのポイントは画面を閉じるときにDiposeメソッドを呼ばれるようにするには
ViewModel側でIDisposableを実装するだけではダメです。
上のリストのようにView側でClosedイベントを受けた時にDataContextDisposeActionを実行するよう宣言する必要があります。

以上、トリガー・アクションのサンプルリストをご紹介しました。
XAMLで書かれたView側のコードとC#で書かれたViewModel側のコードの関係性がなんとなくイメージしていただければ十分です。
というのも、Visual Studioには入力補完機能があるので細かいイベント名など覚える必要はないからです。
また、こういった名称は仕様変更などで変わる可能性もありますしね。

最後に

XAMLで書かれたView側のコードとC#で書かれたViewModel側のコードの関係性についてイベントとアクションに注目し、ご紹介しました。
ViewとViewModelが紐づいているんだなとイメージしていただけたら、本当のそれで十分です。
ViewとViewModelがどう関係しているのかわからないままプログラミングしていて、途中で行き詰る人も少なくないので。

-ソフトウェア