trapemiyaの日記

hatenablogが新しくなったんで新規一転また2019年1月からちょこちょこ書いてます。C#中心のプログラミングに関するお話です。

MVVMでViewModelからViewを操作する(Blend付属アセンブリ使用版)

先日、尾上さんの以下の記事をきっかけとして、twitter上で約3時間ほどつぶやき合いました。尾上さんから多くのことを教えていただき、大変有意義な時間でした。改めてお礼申し上げます。

MVVMパターンでViewModelからViewを操作したい
http://ugaya40.net/wpf/mvvm_viewmodel_to_vew.html

この長いつぶやきの始まりは、私が上記の記事で疑問に思ったことのつぶやきからでした。

@ugaya40 ブログの方にも書き込んでおきましたが、Viewからのトリガーが起点となるのではなく、VMから任意のタイミングでViewを操作したいということなのです。その前提を書いた方がよかったですね。すみません。
9:39 AM Sep 28th Tweenから ugaya40宛
http://twitter.com/trapemiya/status/25738173561

つまり、Viewが一連の動作の起点になる場合にViewModelからViewを操作するのではなく、ViewModelが一連の動作の起点になる場合にViewModelからViewを操作する場合はどうするのか?ということでした。ViewModelが動作の起点になる場合というのは少々説明しづらいのですが、ViewModelが任意のタイミングでいつでもViewを操作できるという意味です。例えばViewであるボタンを押してViewModelで処理が行なわれ、その最中で例外が発生したとします。ViewModelはViewに対して、MessageBoxでその例外を表示するように指示したいのです。保存ボタンを押した時に本当に実行するかという確認メッセージを表示するような場合と違い、このような例外時にViewModelがViewに対してMessageBoxを表示するように指示することは、いつのタイミングで発生するかわからず、したがってViewModelがViewにMessageBoxを表示するように指示を出すことに関しては、ViewModelが完全に起点になります。

しかし、この疑問は尾上さんの指摘ですぐに氷解しました。ViewModelからViewにバインドしているプロパティを変化させ、それをViewが感知して動作に入ればいいのだと。つまり、この時点でViewが起点になってViewModelからViewを操作することと同じになるわけです。(#補足 プロパティの値は実際に変化していなくても、変更があったということをOnPropertyChangedで通知すれば良い)

しかし考察はそこで終わらず、Viewに出された指示がダイアログの表示であれば、そのダイアログでユーザーが選択した値をどうViewModelへ戻すのか?ということでした。この解決として私はCallBackを使うことにしました。

以下、MessageBoxを表示するサンプルコードをご紹介します。尾上さんに教えていただかなかったら以下のコードも生まれなかったでしょう。

コードの動作条件として、System.Windows.Interactivity.dllとMicrosoft.Expression.Interactions.dllが必要になります。Blendをインストールされていれば一緒に入っているので問題ありませんが、無ければ以下のかずきさんの記事を参考にして手に入れて下さい。

Visual Studioから使うExpression BlendのBehavior達
http://d.hatena.ne.jp/okazuki/20100817/1282044737

コードの流れは以下のようになります。

Viewでトリガーとアクションを用意します。アクションとはトリガーをきっかけとして動作するものです。今回はトリガーとしてPropertyChangedTriggerを使います。これはプロパティが変化した際に発火するトリガーです。このトリガーを受けて動作するアクションはTriggerActionを継承して作成します。このアクションはMessageBoxに表示するメッセージやキャプション、それをメッセージとして表示するのかダイアログとして表示するのかというパラメーターを受け取ります。これらのパラメーターはそれぞれ作成しても良いのですが、扱いにくいので1つのクラスにラップして1つのパラメーターとして扱います。さらにこのアクションはダイアログの結果をViewModel返すためのCallBackメソッドも受け取ります。以上を実現したアクションを以下に示します。このアクションに結びつけられたトリガーが発火した時にInvokeメソッドが呼び出されます。

using System;
using System.Windows;
using System.Windows.Interactivity;

namespace WpfDialogTestApplication
{
    /// <summary>
    /// MessageBoxを表示するアクション
    /// </summary>
    class MessageDialogAction : TriggerAction<FrameworkElement>
    {

        //メッセージボックスやダイアログを出すために必要となる情報を受け取る        
        public static readonly DependencyProperty ParameterProperty =
            DependencyProperty.Register("Parameter", typeof(MessageDialogActionParameter), typeof(MessageDialogAction), new UIPropertyMetadata());

        public MessageDialogActionParameter Parameter
        {
            get { return (MessageDialogActionParameter)GetValue(ParameterProperty); }
            set { SetValue(ParameterProperty, value); }
        }

        //ダイアログでの選択結果をViewModelに通知するコールバックメソッド
        public static readonly DependencyProperty ActionCallBackProperty =
            DependencyProperty.Register("ActionCallBack", typeof(Action<object>), typeof(MessageDialogAction), new UIPropertyMetadata(null));

        public Action<object> ActionCallBack
        {
            get { return (Action<object>)GetValue(ActionCallBackProperty); }
            set { SetValue(ActionCallBackProperty, value); }
        }

        protected override void Invoke(object obj)
        {
            if (Parameter.IsDialog)
                ActionCallBack(MessageBox.Show(Parameter.Message, Parameter.Caption, MessageBoxButton.YesNo));
            else
                MessageBox.Show(Parameter.Message, Parameter.Caption);
        }
    }
}

このアクションに渡すパラメータクラスは次の通りです。

    /// <summary>
    /// MessageDialogActionへ渡すパラメーター
    /// </summary>
    public class MessageDialogActionParameter
    {
        public string Message { get; protected set; }    //MessageBoxに表示するメッセージ
        public string Caption { get; protected set; }    //MessageBoxのCaption
        public bool IsDialog { get; protected set; }    //true:ダイアログ、false:メッセージ

        public MessageDialogActionParameter(string Message, string Caption, bool IsDialog)
        {
            this.Message = Message;
            this.Caption = Caption;
            this.IsDialog = IsDialog;
        }
    }

続いてViewModelです。特に難しいところはないと思います。RelayCommandを知らない方は、以下にそのコードが載っています。

Model-View-ViewModel デザイン パターンによる WPF アプリケーション
http://msdn.microsoft.com/ja-jp/magazine/dd419663.aspx
using System;
using System.Windows;
using System.Windows.Input;
using System.ComponentModel;

namespace WpfDialogTestApplication
{
    public class MainWindowViewModel : INotifyPropertyChanged
    {
        public MainWindowViewModel()
        {
            // オブジェクト作成に必要なコードをこの下に挿入します。

            InvokeCommand = new RelayCommand(InvokeMessageDialog);
        }

        public ICommand InvokeCommand { get; private set; }

        /// <summary>
        /// MessageDialogActionに渡すパラメーター
        /// </summary>
        public MessageDialogActionParameter MessageDialogActionParam { get; protected set; }

        /// <summary>
        /// MessageDialogActionの実行後に呼ばれるCallBack
        /// </summary>
        public Action<object> MessageDialogActionCallback { get; protected set; }

        /// <summary>
        /// ダイアログを表示し、その結果をCallBackで受け取るように設定する。
        /// </summary>
        /// <param name="obj"></param>
        private void InvokeMessageDialog(object obj)
        {
            MessageDialogActionParam = new MessageDialogActionParameter("別にいいんだけど、どっちかボタン押してみる?", "別に押さなくてもいいんだけど", true);
            MessageDialogActionCallback = p => MessageBox.Show("押したのはこれでしょ?→ " + ((MessageBoxResult)p).ToString());
            OnPropertyChanged("MessageDialogActionParam");
        }
        
        #region INotifyPropertyChanged メンバ

        public event PropertyChangedEventHandler PropertyChanged;

        protected virtual void OnPropertyChanged(string propertyName)
        {
            var h = PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion
    }
}

最後にWindowのXAMLです。Actionを起動するトリガーとしてPropertyChangedTriggerを指定しています。

<Window
	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:local="clr-namespace:WpfDialogTestApplication"
	x:Class="WpfDialogTestApplication.MainWindow"
	Title="MainWindow"
	Width="640" Height="480">

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

	<i:Interaction.Triggers>
		<ei:PropertyChangedTrigger Binding="{Binding MessageDialogActionParam, Mode=OneWay}">
			<local:MessageDialogAction Parameter="{Binding MessageDialogActionParam, Mode=OneWay}"
                                       ActionCallBack="{Binding MessageDialogActionCallback, Mode=OneWay}"/>
		</ei:PropertyChangedTrigger>
	</i:Interaction.Triggers>

	<Grid x:Name="LayoutRoot">
		<Button Content="Button" Command="{Binding InvokeCommand, Mode=OneWay}" HorizontalAlignment="Left"
                Height="29" Margin="50,115,0,0" VerticalAlignment="Top" Width="69" />
	</Grid>
</Window>

意外に簡単なコードで驚かれたのではないでしょうか? 今までMVVMを実現するいろいろなライブラリーを見てきましたが、それらの基本はViewとViewModelの間に両方知っているクラスを作成し、ViewとViewModelの間を取り持つというものでした。アクションもその間を取り持つクラスの一種であると考えられます。アクションはViewを知っていますし、アクションにViewのDataContextを渡せばViewModelも知っていることになります。つまり、アクションはこのようなMVVMを実現するライブラリーと本質的に同じ考え方で動いているのです。
Blendからの素敵な贈り物ですね。いずれVisual StudioにBlendの機能が含有されることを望みます。