Ktouth Brand. on Web

け〜くんこと K.Ktouth のだらだらした日常と突発的に作るプログラムや読み物とかの雑多サイト



[2012年09月25日]

Windows.Closing イベントを MVVM で実装してみた

2012年09月26日 08:17更新 筆者:K.Ktouth

昨日悩んだ「Windows.Closing イベントに MVVM で対応する」方法ですが、オブジェクトブラウザで Livet の各クラスをチェックしていたところあっさりわかりました(ぉぃ きちんと WindowCloseCancelBehavior というビヘイビアが準備されているのでコレとイベントトリガを組み合わせるといいみたいですね。
同時に、昨日のコードビヘイビアだと肝心なことが上手く出来ないことに後で気づきました。「イベントのルーティング中に次のイベント、具体的には PropertyChanged が起きるわけがない」ってことです。要するにクローズ処理の「直前に確認を取り」「(画面処理を伴う)時間のかかる処理を行い」「終わった後にウィンドウを閉じる」という方法は、そもそも3ステップに分けて実装する必要があったと言うことです。

で、結局は終了処理という一連の処理の実行状態を示すプロパティを公開し、先ほどのビヘイビアがそれをみながらクローズ処理を制御、実際の処理は「確認ダイアログを出す」「終了処理を行う」「改めてクローズする」と行う形になりました。

(以下、実装)

まずは下準備

まず実行状態を示す列挙値と値コンバータの実装。以下は抜粋。

public enum TaskExecuteState { Before, Into, After };

public sealed class TaskExecuteStateToBooleanConverter : IValueConverter
{
  public TaskExecuteStateToBooleanConverter()
  {
    DefaultValue = false;
    TargetState = TaskExecuteState.After;
    TargetToBoolean = true;
  }

  public bool DefaultValue { get; set; }
  public TaskExecuteState TargetState { get; set; }
  public bool TargetToBoolean { get; set; }

  public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
  {
    return (value is TaskExecuteState) && ((TaskExecuteState)value == TargetState) ? TargetToBoolean : DefaultValue;
  }
}

値コンバータでは対象を変更できるようにしていますが、要するに「TaskExecuteState.After の時だけ true に変換する」というだけです。3値なのでコレで十分対応できるかと。

ViewModel 側

#region 終了処理に関するコマンドおよびプロパティ

public TaskExecuteState AppFinalizationState
{
  get { return _AppFinalizationState; }
  private set { SetProperty(ref _AppFinalizationState, value); }
}
private TaskExecuteState _AppFinalizationState = TaskExecuteState.Before;

public void AppFinalize()
{
  if (AppFinalizationState == TaskExecuteState.Before)
  {
    var confirm = new ConfirmationMessage(
        "アプリケーションを終了します。設定を保存しますか?\n(いいえを選ぶと設定を保存せずに終了します)",
        "終了の確認", System.Windows.MessageBoxImage.Question, System.Windows.MessageBoxButton.YesNoCancel,
        "ConfirmAppFinalize");

    var result = Messenger.GetResponse<ConfirmationMessage>(confirm);
    if (result.Response != null)
    {
      AppFinalizationState = TaskExecuteState.Into;
      Task.Run(async () =>
      {
        await DoApplicationFinalizeAsync(result.Response == true);
        AppFinalizationState = TaskExecuteState.After;
        Messenger.Raise(new WindowActionMessage("CompleteAppFinalize", WindowAction.Close));
      });
    }
  }
}

#endregion

AppFinalizationState プロパティが実行状態を示すプロパティ。INotifyPropertyChanged 対応です。で、このコードに苦心したんですが、AppFinalize が終了処理の実態。C# 5.0 + Livet だと3ステップが綺麗に一つのメソッドに埋め込めるので非常に楽ですね!(笑)
具体的には……

  1. AppFinalizationState が Before、つまり実行前のみ反応する。
  2. Messenger.GetResponse メソッドで同期的に確認を行う。
  3. 返値が Cancel 以外の時に、2ステップから先を非同期で実行する
    クローズ処理はいったんここで抜けることになり、件の WindowCloseCancelBehavior がクローズ処理をキャンセルしてくれる。
  4. 実行状況を進行させ、DoApplicationFinalizeAsync 非同期メソッドに情報を渡して実際の終了処理を行う。
  5. 実行状況を進行させ、Messenger.Raise メソッドで改めてクローズ処理を発行する。
    実行状況が変化しているので WindowCloseCancelBehavior もクローズ処理を許可し、ウィンドウが閉じる。

こんな感じ。

View 側

んで View はリソース・ビヘイビア・トリガーの登録です。

<Window.Resources>
  <vc:TaskExecuteStateToBooleanConverter x:Key="WindowClosingConverter" />
</Window.Resources>
<i:Interaction.Behaviors>
  <l:WindowCloseCancelBehavior CanClose="{Binding AppFinalizationState, Converter={StaticResource WindowClosingConverter}, Mode=OneWay}" />
</i:Interaction.Behaviors>
<i:Interaction.Triggers>
  <i:EventTrigger EventName="Closing">
    <l:LivetCallMethodAction MethodName="AppFinalize" MethodTarget="{Binding Mode=OneWay}" />
  </i:EventTrigger>
  <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="ConfirmAppFinalize">
    <l:ConfirmationDialogInteractionMessageAction />
  </l:InteractionMessageTrigger>
  <l:InteractionMessageTrigger Messenger="{Binding Messenger}" MessageKey="CompleteAppFinalize">
    <l:WindowInteractionMessageAction />
  </l:InteractionMessageTrigger>
</i:Interaction.Triggers>

こっちはシンプルですね。コンバータの生成とビヘイビアの登録。トリガーはクローズ処理の開始、確認ダイアログの表示、最後のウィンドウのクローズの3つです。
ちょっと不満なのが確認ダイアログの部分のアクション。確認ダイアログに表示するテキスト他のレイアウト情報がこのコードでは ViewModel 側で指定してあります。がテキストなどはいわゆるリソースであり、出来れば View 側の仕事にしたいんですけどねぇ。実際、サンプルコードなどでは DirectIntaractionMessage 要素を作って View 側で指定しているのですが……現状の Livet では XAML 上で Message 要素を記述すると、Messenger.Response / ResponseAsync メソッドで適切な返値が設定されないという仕様になっていて、上記コードが上手く動作しません。
GitHub にあるソースコードを読んだ限りでは「ViewModel 側の Message より XAML 側のものが優先される」「XAML 生成の Message から ViewModel 側に Response は伝播しない」のは間違いない模様。最後の if ブロックの最後に伝播させるコードが必要だと思うんだけど、単純な抜けなのかな……? 後で作者に連絡してみようっと。

コードビハインドなら数行で済むものが、MVVMだと3倍くらいに膨らみましたが……でも非同期に出来たし応用効きそう(状況に応じたエフェクトがつけられるとか)だし、いい勉強になりました♪

本日のリンク元
その他のリンク元
検索