かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

MVVMパターンでコマンドの実行前に確認ダイアログとか出したくない?

前にも同じようなタイトルでBlogを書いてますが、今回は別の方法です。

前のBlogの記事から大分時間が経って、今やるならという感じで書いてみます。

コマンド実行前に確認ダイアログを出す

これは、ButtonなどのようにCommandをサポートするクラスでやるのは結構骨が折れます。というかやり方教えてください。ということでButtonでもCommandプロパティを使うやり方は今回は諦めました。

TriggerBaseクラス

さて、Commandプロパティを使う方法以外にもう一つCommandを実行させる方法があります。Blend SDKに入っているInvokeCommandActionがそれになります。こいつはEventTriggerなどの各種Trigger系のクラスと組み合わせて使います。ButtonのCommandプロパティと同じように使おうと思ったらButtonにEventTriggerを仕掛けてClickイベントに反応するようにします。そして、EventTriggerの下にInvokeCommandActionを置いて、ViewModelのCommandとバインドします。

そして、このTriggerBaseクラスには、PreviewInvoke イベントというイベントがあってTriggerがActionを実行する前の処理をこのイベントで行うことが出来るようになっています。このイベント引数のPreviewInvokeEventArgs クラスのCancellingプロパティをtrueにすることで、TriggerがActionを実行することをキャンセルさせることが出来るようになっています。

Blend SDKで提供されているクラス

このPreviewInvokeイベントどうやってイベントハンドラを登録するの?という疑問が出てくるのですが、こいつはTriggerにPreviewInvokeイベントを購読して処理をするBehaviorを設定することで実現します。Expression Blendを使ってると、条件式を宣言的に定義できるBehaviorがサポートされています。Blendの画面上では条件という名前で登場しています。

XAML上では以下のように定義されています。

<Button Content="Button" HorizontalAlignment="Left" Margin="64,32,0,0" VerticalAlignment="Top" Width="75">
    <i:Interaction.Triggers>
        <i:EventTrigger EventName="Click">
            <i:Interaction.Behaviors>
                <!-- このConditionBehaviorがポイント!! -->
                <ei:ConditionBehavior>
                    <ei:ConditionalExpression>
                        <ei:ComparisonCondition LeftOperand="1" Operator="NotEqual" RightOperand="{Binding Test, Source={StaticResource DataStore}}"/>
                    </ei:ConditionalExpression>
                </ei:ConditionBehavior>
            </i:Interaction.Behaviors>
            <ei:CallMethodAction/>
        </i:EventTrigger>
    </i:Interaction.Triggers>
</Button>

個人的に試した感じだとBlendでは、ConditionBehaviorしかサポートしてない?と思われるので、ここに独自のBehaviorを差し込む場合にはXAMLを手書きしないといけません。まぁ個人的には許容範囲です。ということで、確認ダイアログを出してOKかCancelかをユーザに選択させるようなBehaviorを作ればこの問題は解決です。

コマンド実行後にView側で処理をする

これは今更説明の必要もないですね。InteractionRequestクラスを使えばOKです。View側でInteractionRequestTriggerでメッセージを受信して任意のActionを実行すればOKです。
詳細は以下のエントリを参照してください。

実装してみよう

ということで先日公開したKinkumaFrameworkでは確認ダイアログを出すためのBehaviorを提供しています。コードとしてはシンプルでイベント購読して確認ダイアログ出してるだけです。

namespace Okazuki.MVVM.PrismSupport.Interactivity
{
    using System.Windows;
    using System.Windows.Interactivity;

    /// <summary>
    /// TriggerBaseがアクションを実行する前に確認ダイアログで実行可否を選択可能なようにするBehaviorです。
    /// </summary>
    public class ConfirmPreviewActionBehavior : Behavior<System.Windows.Interactivity.TriggerBase>
    {
        public static readonly DependencyProperty TitleProperty =
            DependencyProperty.Register(
                "Title",
                typeof(string),
                typeof(ConfirmPreviewActionBehavior),
                new UIPropertyMetadata("確認"));

        public static readonly DependencyProperty MessageProperty =
            DependencyProperty.Register(
                "Message", 
                typeof(string), 
                typeof(ConfirmPreviewActionBehavior), 
                new UIPropertyMetadata(string.Empty));

        /// <summary>
        /// 確認メッセージ
        /// </summary>
        public string Message
        {
            get { return (string)GetValue(MessageProperty); }
            set { SetValue(MessageProperty, value); }
        }

        /// <summary>
        /// 確認ダイアログのタイトル
        /// </summary>
        public string Title
        {
            get { return (string)GetValue(TitleProperty); }
            set { SetValue(TitleProperty, value); }
        }

        protected override void OnAttached()
        {
            base.OnAttached();
            this.AssociatedObject.PreviewInvoke += PreviewInvoke;
        }

        protected override void OnDetaching()
        {
            this.AssociatedObject.PreviewInvoke -= PreviewInvoke;
            base.OnDetaching();
        }
 
        private void PreviewInvoke(object sender, PreviewInvokeEventArgs e)
        {
            e.Cancelling = MessageBox.Show(
                this.Message,
                this.Title,
                MessageBoxButton.OKCancel) == MessageBoxResult.Cancel;
        }

    }
}

ちょっと長いですが、DependencyPropertyの定義のせいで実質のコードはそんなに長くありません。OnAttachedでPreviewInvokeイベントを購読して、イベントハンドラでMessageBoxを出してOKかCancelかでイベント引数のCancellingプロパティの値を設定しています。

お試しプログラム

ということでお試しプログラムを書いていきます。お試しプログラムはボタンが1つあって、ボタンを押すと処理を実行するかどうかを確認するダイアログが出てきます。ダイアログでOKを押すと処理が実行されて、処理の実行後に、またダイアログが出てきます。Cancelボタンを押すと処理は実行されません。

ということでWPFアプリケーションプロジェクトを作ってKinkumaFrameworkをNuGetから参照に追加します。Blend使うまでもないので、ついでにNuGetからOkazuki.BehaviorSupportのWPF版も参照に追加しておきました。これでプロジェクトの設定は終わりです。

ViewModelの作成

ViewModelの作成は手抜きをしたいのでKinkumaFrameworkのアイテムテンプレートをつかいます。アイテムテンプレートの入れ方等は、KinkumaFrameworkのプロジェクトページのDocumentationsからドキュメント(書きかけ)をダウンロードして確認してください。

MVVMParts.ttincludeファイルを作成して、MainWindowViewModel.csとMainWindowViewModel.ttを作成します。

そして、MainWindowViewModel.ttにCommandの定義を記述します。

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".generated.cs" #>
// <generated />
namespace PreviewActionSample
{
	using System;

	[System.ComponentModel.TypeDescriptionProvider(typeof(MainWindowViewModelAssociatedMetadataTypeTypeDescriptionProvider))]
	public partial class MainWindowViewModel
	{
		<#
		Command("Alert");
		#>
		<# AssociatedMetadataTypeTypeDescriptionProvider("MainWindowViewModel"); #>
	}
		
}
<#@ include file="MVVMParts.ttinclude" #>

次に、MainWindowViewModel.csにコマンドの処理を記述していきます。

namespace PreviewActionSample
{
    using System.ComponentModel.DataAnnotations;
    using Microsoft.Practices.Prism.Interactivity.InteractionRequest;
    using Okazuki.MVVM.PrismSupport.Utils;
    using Okazuki.MVVM.PrismSupport.ViewModels;

    [MetadataType(typeof(Metadata))]
    public partial class MainWindowViewModel : ViewModelBase
    {
        // Viewにメッセージを投げるためのRequest
        [AutoInit]
        public InteractionRequest<Notification> AlertRequest { get; private set; }

        partial void AlertExecute()
        {
            // ここで何か主処理をやる

            // Viewへメッセージを投げる
            this.AlertRequest.Raise(
                new Notification
                {
                    Title = "確認",
                    Content = "後処理は任せた"
                });
        }

        class Metadata
        {
        }
    }
}

今回は、簡単なサンプルということでコマンドの処理は、ViewModelからViewにメッセージを投げてるだけです。以上でViewModelは完了です。

Viewの作成

ViewModelが出来たのでViewを作成します。Viewは、単純にボタンを1つ置いただけのシンプルなものです。

<Window x:Class="PreviewActionSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:PreviewActionSample"
        xmlns:kinkuma="clr-namespace:Okazuki.MVVM.PrismSupport.Interactivity;assembly=Okazuki.MVVM.PrismSupport"
        Title="MainWindow" xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" 
        xmlns:my="http://www.codeplex.com/prism">
    <!-- ViewModelの設定 -->
    <Window.DataContext>
        <l:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <i:Interaction.Triggers>
            <!-- ViewModelのAlertRequestからのメッセージ受信 -->
            <my:InteractionRequestTrigger SourceObject="{Binding Path=AlertRequest}">
                <!-- メッセージボックスを出す -->
                <kinkuma:ShowMessageBoxAction />
            </my:InteractionRequestTrigger>
        </i:Interaction.Triggers>
        <Button Content="Button" Name="button1">
            <i:Interaction.Triggers>
                <!-- CommandプロパティじゃなくてEventTriggerからCommandを呼ぶ -->
                <i:EventTrigger EventName="Click">
                    <i:Interaction.Behaviors>
                        <!-- PreviewInvokeイベントでMessageBoxを出すアクション -->
                        <kinkuma:ConfirmPreviewActionBehavior
                            Title="確認"
                            Message="本当に実行してもいいですか?" />
                    </i:Interaction.Behaviors>
                    <!-- ConfirmPreviewActionBehaviorでOKボタンが押されたら呼ばれるアクション -->
                    <i:InvokeCommandAction Command="{Binding Path=AlertCommand}" />
                </i:EventTrigger>
            </i:Interaction.Triggers>
        </Button>
    </Grid>
</Window>

画面はシンプルなのですが、XAMLで色々やってるのでそれなりの量があります。まず、最初のポイントはButtonに設定しているEventTriggerです。

<!-- CommandプロパティじゃなくてEventTriggerからCommandを呼ぶ -->
<i:EventTrigger EventName="Click">
    <i:Interaction.Behaviors>
        <!-- PreviewInvokeイベントでMessageBoxを出すアクション -->
        <kinkuma:ConfirmPreviewActionBehavior
            Title="確認"
            Message="本当に実行してもいいですか?" />
    </i:Interaction.Behaviors>
    <!-- ConfirmPreviewActionBehaviorでOKボタンが押されたら呼ばれるアクション -->
    <i:InvokeCommandAction Command="{Binding Path=AlertCommand}" />
</i:EventTrigger>

コメントにもあるように、EventTriggerのPreviewInvokeイベントを購読してMessageBoxを出す先ほどコードを示したアクションをBehaviorとして登録しています。こいつが働くことでコマンド実行前に確認ダイアログを出す処理を実行できるようになります。

後は、ViewModelのAlertRequestからのメッセージを応じてMessageBoxを出すように設定してます。

<i:Interaction.Triggers>
    <!-- ViewModelのAlertRequestからのメッセージ受信 -->
    <my:InteractionRequestTrigger SourceObject="{Binding Path=AlertRequest}">
        <!-- メッセージボックスを出す -->
        <kinkuma:ShowMessageBoxAction />
    </my:InteractionRequestTrigger>
</i:Interaction.Triggers>

あと、当然ですがViewModelもきちんと設定してます。

<!-- ViewModelの設定 -->
<Window.DataContext>
    <l:MainWindowViewModel />
</Window.DataContext>

以上で完成です。

動作確認

では実行して動作確認をします。実行すると以下のように味気ない画面が表示されます。

ボタンをクリックすると、実行していいかを尋ねるダイアログが表示されます。

ここで表示されているメッセージはXAMLで指定したものに出来るので任意のメッセージが指定できるようになっています。OKボタンをクリックすると以下のように、ViewModel側に処理がうつったあとにViewModelからメッセージが投げられてMessageBoxがViewに表示されます。

ここではやりませんが、キャンセルボタンを押すとViewModel側に処理はいきません。

以上!

サンプルのダウンロード

ソースコードは以下からダウンロードできます。