MVVMでメッセンジャーを使わずにダイアログを表示する(MVVM Dialog Behaviorライブラリ提供)


□導入

MVVMでのダイアログ表示では、メッセンジャーを使用することが一般的となっています。しかし、メッセンジャーは理解が難しく、扱い方が煩雑であるために、MVVMを使用すべきでないという理由のひとつにさえなっていました。

私自身、MVVM自体は使える技術だと思っているものの、メッセンジャーを使用してダイアログ表示を行うこと自体については疑問を感じており、 以前、私のブログにて、メッセンジャーを使わずにシンプルにダイアログ表示を行う方法を「MVVMでダイアログ表示のためにメッセンジャーを使用するのはおかしいのでは?」と題してご紹介もしましたが、あくまでこの時点では方向性の提案にとどまるものでした。

しかし、この度「MVVM Dialog Behavior(http://mvvmdialogbehavior.codeplex.com)」と呼ぶ、ダイアログ表示のためのライブラリを作成いたしましたので、今回のブログではその思想と使い方について解説します。

実は、ライブラリとはいってもたった2クラスで、全体でも200行程度しかなく、むしろコードスニペットとでもいうべきものです。このためライセンス自体を完全フリー(放棄)としますので、CodePlexでDLLをダウンロードして使用するなり、コードをコピーペーストして使うなりと自由にお使いいただければと思います。ただし、無保証ですので自己責任での使用をお願い致します。


□MVVM Dialog Behaviorの思想

MVVMではViewModelに画面状態を保持し、ViewModelの状態を変化させることでViewを制御します。

一方、メッセンジャーを使用する方法は、処理の起動であって、あくまでイベントを送るようなものであり、これ自体は画面状態とは相容れない考え方です。また、View を起動してそこで処理を行うので、ViewModelがViewへとダイアログ表示制御の責任を放棄してしまうような面があります。つまり、メッセンジャーによ るダイアログ表示は、ViewModelにて画面状態を保持して画面を制御するというMVVMの基本的な姿勢に反するところがあるのです。

MVVM Dialog Behaviorでは、「ダイアログ表示という状態も画面状態の一部であり、このためViewModelの状態変化によって制御されるべきである。」と考えており、「ダイアログ表示を画面状態としてモデル化する」という思想のもとに作成されました。

何とも分かりづらいかもしれませんが、実は表示するコンテンツをViewModelの状態として保持して制御するのは、そこかしこで当たり前のように行われていることです。例えば、ラベルやボタンのコンテンツも、Metro UIのタイルのコンテンツも、MVVMならViewModelで管理するはずです。それらと同様のプログラミング体験でダイアログ表示を扱えるようにすべきだといっているだけだったりします。

単に、ラベルやボタンやタイルのコンテンツと同様にダイアログも扱うためのビヘイビアをライブラリとして提供したもの、それがMVVM Dialog Behaviorというライブラリです。


□MVVM Dialog Behaviorのメリット

MVVM Dialog Behaviorでは、ダイアログ操作にメッセンジャーではなくダイアログに対応したViewModel自体を使用します。そしてこのダイアログViewModelインスタンスを、親ViewModelのプロパティにセットすることでダイアログ表示を行い、Nullをセットすることでダイアログを閉じます。

またダイアログViewModelのプロパティを通してダイアログへのデータの引き渡しと結果の取得を行うことができます。このようにViewModel主導で、より直感的にダイアログを制御することができます。

そして、ダイアログの定義にViewとViewModelをしますので、メッセンジャー(およびメッセージおよびアクション)を使用するよりも見た目や機能をより簡単かつ柔軟に変更することができます。

MVVM Dialog Behaviorを使用することによるメリットには以下のようなものがあります。
  • ラベルやボタンやMetro UIのタイルのコンテンツ操作と同様のプログラミング体験で、ダイアログ表示を行うことができる。
    • ダイアログViewModelインスタンスを親ViewModelのプロパティにセットするだけでダイアログを表示を行える。また、Nullをセットするだけでダイアログを閉じることができる。
    • ダイアログ用ViewModelのプロパティを通してデータの引き渡しと結果の取得が行える。
  • ViewとViewModelを使用して柔軟にダイアログの見た目や機能を変更することができる。
  • ダイアログの種類を増やすのために、メッセンジャーのように専用のアクションや専用のメッセージを作成する必要がなく、ViewとViewModelの記述だけで完結させることができる。
  • ダイアログ表示状態が明示的にViewModel構造に表現されるため、ダイアログ表示中かどうかをViewModel内で容易に判断できる。またこれを用いて、システムの都合によりダイアログを強制的にクローズすること容易にできる。
  • ダイアログ表示のプレゼンテーションロジックがViewModelに含まれるようになり、自動単体テストコードによる検証が行いやすい。

□MVVM Dialog Behaviorの動作環境およびインストールについて

本ライブラリの動作環境は以下の通りです。

インストールについては以下の通りです。
  • 本ライブラリのライセンスについて
    Code Plexでは、MITライセンスを選択していますが、それ以上ゆるいライセンスが選択できなかっただけであり、ライセンス自体は完全に放棄します。MITライセンスでも本来必要な「著作権表示の保持」なども不要です。ただし、[免責]このソフトウェアを原因として発生した損失や損害については一切の責任を負いません。すべて自己責任でお願い します。
  • 本ライブラリのダウンロードについて


MVVM Dialog Behaviorのサンプルコード

MVVM Dialog Behaviorの概念ばかり説明されてもあまりイメージが湧かないでしょうから、そろそろサンプルコードを見ながら説明していくことにします。このサンプルではViewModelの作成にMVVM Light Toolkit(http://mvvmlight.codeplex.com/)を使用して説明しますので、他のMVVMフレームワークを使用している場合には適宜読み替えてください。

以下のような簡単な画面からログインダイアログが表示されるというシナリオを考えてみましょう。


メインウィンドウ
ログインダイアログ(メインウィンドウのOpenDialogボタン押下時に起動する)

次にViewModelを見てみます。まずはViewModelのクラス図を見てみましょう。

ViewModel構造

メインウィンドウ用としてMainViewModelがあり、ログインダイアログ用としてLoginDialogViewModelがあります。そして、MainViewModelはDialogプロパティによって(ViewModelBaseを通して間接的に)LoginDialogViewModelを保持します。

このDialog プロパティの多重度が「0,1」となっているところがポイントです。これは、ダイアログが表示されているかどうかを、DialogViewModelのイ ンスタンスが存在するか、Nullかによって判断するというモデルになっています。つまり、ダイアログ表示のためには、Dialogプロパティに DialogViewModelのインスタンスをセットし、またダイアログを閉じるにはNullをセットすれば良いことになります。

このように、ViewModelの状態変更によってダイアログ表示を制御できるのは、ダイアログ表示状態そのものがViewModelのクラス構造の中にモデル化されているからです。ここはMVVM Dialog Behaviorの思想がよく表れている部分です。

それではViewModelのコードを見てみましょう。まずは、MainViewModelのソースコードを見てみます。


MainViewModelのソースコード
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GalaSoft.MvvmLight;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;

namespace MvvmDialogBehaviorSample
{
    public class MainViewModel:ViewModelBase
    {
        public MainViewModel()
        {
        }

        private ViewModelBase _Dialog;
        public ViewModelBase Dialog
        {
            get { return _Dialog; }
            set
            {
                if (_Dialog != value)
                {
                    _Dialog = value;
                    RaisePropertyChanged("Dialog");
                }
            }
        }

        private ICommand _OpenDialogCommand;
        public ICommand OpenDialogCommand
        {
            get { 
                if (_OpenDialogCommand == null)
                {
                    _OpenDialogCommand = new RelayCommand(OpenDialog);
                }

                return _OpenDialogCommand;
            }
        }

        private void OpenDialog()
        {
            // Open Dialog
            Dialog = new LoginDialogViewModel(CloseDialog) { UserName="LastLoginUser1" };
        }

        private void CloseDialog()
        {
            // Close Dialog
            Dialog = null;
        }
    }
} 

注目すべきは、ダイアログオープンのために、Dialogプロパティへのインスタンスのセットしか行っていないことです。そして、CloseDialog()メソッドにて、ダイアログクローズのために、DialogプロパティにNullのセットしか行っていません。

また、ダイアログに引き渡すべきユーザ名がViewModelプロパティによる単純な受け渡しで実現できています。
 
ところで、ここで問題になるのが、そもそもどうやってこのCloseDialog()メソッドを呼び出すのかということです。ここでは、ダイアログ側か らのクローズを考慮して、このメソッド自体をLoginDialogViewModelのコンストラクタに渡してコールバック可能にしていることに注意してくださ い。このあたりの実装方法は、他にもいろいろ考えられます(*1)ので、それぞれのプロジェクトに合わせて実装していただければと思います。

*1:ほかの実装方法としては例えば、ダイアログViewModelにクローズイベントを作成して、ダイアログの呼び出し元ViewModelからイベントハンドラとしてをCloseDialog()をあらかじめ登録しておき、ダイアログViewModel側でクローズしたいときにイベントを発火させてクローズさせる方法も考えられます。

次に、LoginDialogViewModelのソースコードを見てみます。


LoginDialogViewModelのソースコード
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using GalaSoft.MvvmLight;
using System.Windows.Input;
using GalaSoft.MvvmLight.Command;
using System.Windows;

namespace MvvmDialogBehaviorSample
{
    public class LoginDialogViewModel:ViewModelBase
    {
        private Action _CloseAction;

        public LoginDialogViewModel(Action closeAction)
        {
            _CloseAction = closeAction;
        }

        private String _UserName;
        public String UserName
        {
            get { return _UserName; }
            set
            {
                if (_UserName != value)
                {
                    _UserName = value;
                    RaisePropertyChanged("UserName");
                }
            }
        }

        private String _Password;
        public String Password
        {
            get { return _Password; }
            set
            {
                if (_Password != value)
                {
                    _Password = value;
                    RaisePropertyChanged("Password");
                }
            }
        }

        private ICommand _LoginCommand;
        public ICommand LoginCommand
        {
            get
            {
                if (_LoginCommand == null)
                {
                    _LoginCommand = new RelayCommand(Login);
                }

                return _LoginCommand;
            }
        }

        private void Login()
        {
            if (DoLogin())
            {
                _CloseAction();
            }
        }

        private ICommand _CancelCommand;
        public ICommand CancelCommand
        {
            get
            {
                if (_CancelCommand == null)
                {
                    _CancelCommand = new RelayCommand(Cancel);
                }

                return _CancelCommand;
            }
        }

        private void Cancel()
        {
            _CloseAction();
        }

        private bool DoLogin()
        {
            // Login Process...
            return true;
        }
    }
} 

ログインダイアログとして、ユーザ名とパスワードのプロパティを持ち、またログインとキャンセルのコマンドを持っています。たいしたコードでもありませんが、先ほど説明したとおり、自身のダイアロ グをクローズするために、MainViewModelから渡されたCloseDialog()メソッドをコールバックしている点はご注意くだ さい。

これで、ViewModel側のソースコードの説明は終了です。次に、View側のソースコードを見てみます。

MainWindowのソースコード
<Window x:Class="MvvmDialogBehaviorSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MvvmDialogBehaviorSample"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
        xmlns:mvvmdialogbehavor="clr-namespace:MvvmDialogBehavior;assembly=MvvmDialogBehavior"
        Title="MainWindow" Height="275" Width="375" Background="Azure">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>

    <i:Interaction.Behaviors>
        <mvvmdialogbehavor:DialogBehavior Content="{Binding Dialog}" >
            
            <mvvmdialogbehavor:DialogBehavior.Resources >
                <DataTemplate DataType="{x:Type local:LoginDialogViewModel}" >
                    <Grid Height="250" Width="350" Background="Ivory">
                        <Label Content="LoginDialogView" Height="29" HorizontalAlignment="Left" Name="label1" VerticalAlignment="Top" />
                        <Label Content="UserName" Height="29" HorizontalAlignment="Left" Margin="22,70,0,0" VerticalAlignment="Top" />
                        <Label Content="Password" Height="29" HorizontalAlignment="Left" Margin="22,112,0,0" VerticalAlignment="Top" />
                        <TextBox Text="{Binding UserName}" Height="24" HorizontalAlignment="Left" Margin="96,72,0,0" VerticalAlignment="Top" Width="120" />
                        <TextBox Text="{Binding Password}" Height="24" HorizontalAlignment="Left" Margin="96,114,0,0" VerticalAlignment="Top" Width="120" />
                        <Button Content="Login" Command="{Binding LoginCommand}" Margin="61,157,189,39" />
                        <Button Content="Cancel" Command="{Binding CancelCommand}" Margin="167,157,83,39" />
                    </Grid>
                </DataTemplate>
            </mvvmdialogbehavor:DialogBehavior.Resources>
            
            <mvvmdialogbehavor:DialogBehavior.Style>
                <Style TargetType="Window">
                    <Setter Property="SizeToContent" Value="WidthAndHeight"/>
                </Style>
            </mvvmdialogbehavor:DialogBehavior.Style>
            
        </mvvmdialogbehavor:DialogBehavior>
    </i:Interaction.Behaviors>
    
    <Grid>
            <Button Content="Open Dialog" Command="{Binding OpenDialogCommand}" Height="49" HorizontalAlignment="Left" Margin="88,83,0,0" Name="button1" VerticalAlignment="Top" Width="160" />
    </Grid>
</Window> 

このMainWindowのコードには、メインウィンドウとログインダイアログのViewの定義の両方が記述されています。また、ログインダイアログの定義では、MVVM Dialog Behaviorが提供するDialogBehaviorクラスが使用されています。

ここで使用されているDialogBehaviorクラスのプロパティである、Content、Resources、Styleは、実は意図的にWindowクラスが持つプロパティと同名となっています。そしてこれらは、ダイアログの実体であるWPFのWindowインスタンスのプロパティにそのまま適用されることになります。

ただし実は、Contentプロパティだけは少し特別な扱いを受けています。通常は、ここにダイアログViewModelをバインディングするようにして使用するのですが、このダイアログのViewModelが存在している間のみダイアログが表示され、Nullの時にはダイアログは非表示となるようになっているのです。

これは、ViewModel側でダイアログが表示されるかどうかを、ダイアログのViewModelのインスタンスが存在するかNullかで表現するとした、ViewModelの定義とうまく適合するようになっています。


さて、先ほども説明したとおり、DialogBehaviorビヘイビアのResourcesとStyleのプロパティは、そのままダイアログのWindowインスタンスのプロパティへと設定されます。それ以上の働きはしませんが、サンプルコードの内容については少し説明しておきましょう。 

今回のサンプルのResourcesプロパティにはDataTemplateが指定されています。このDataTemplateは、ViewModelとViewをマッピングする役割があります。DataTypeプロパティにViewModelの型を指定して、さらにViewを記述するのです。このようにしておくと、ContentプロパティのViewModelの型によって自動的にViewが適用され、またViewとViewModel間のDataContextの設定も自動的に行われます。

今回のサンプルコードでは、LoginDialogViewModelインスタンスがContentプロパティに設定されたときに、Grid以下のコントロールがViewとして適用されることになります。

次に、 Styleプロパティでは、WindowのSizeToContentプロパティにWidthAndHeightを指定しています。このため、内部のViewの縦横の幅に合わせてダイアログのWindowの縦横の幅となります。

以上でサンプルの説明は終了です。シンプルにダイアログ表示が実現できていると思いませんか。


□もっと実践的にMVVM Dialog Behaviorを使う 1 (Viewの共通化)

比較的単純にMVVM Dialog Behaviorを使用するサンプルコードをご説明してきました。しかし、実際のプロジェクトでは、もっとコードの共通化と再利用を推し進めることで、MVVM Dialog Behaviorに指定するコードはさらにシンプルなものになります。

まず、 ログインダイアログのViewを共通化するところから見ていきましょう。

現在は、ログインダイアログのViewがDataTemplate内にインライン記述されています。しかし、複数の場所からログインダイアログを使用したい場合に、それぞれの場所でインラインに記述していては、コードの重複が発生することになるでしょう。

この問題を解決するためにログインダイアログのViewを共通化しましょう。といってもViewの共通化とは、単純にLoginDialogView.xamlファイルを作成して、DataTemplate内に記述していたXAMLコードを移動するだけです。

LoginDialogView.xamlの内容は以下のようになります。

LoginDialogViewのソースコード
<UserControl x:Class="MvvmDialogBehaviorSample.LoginDialogView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
             xmlns:mvvmdialogbehavor="clr-namespace:MvvmDialogBehavior;assembly=MvvmDialogBehavior"
             mc:Ignorable="d" 
             d:DesignHeight="238" d:DesignWidth="338"  >

    <Grid Height="250" Width="350" Background="Ivory">
        <Label Content="LoginDialogView" Height="29" HorizontalAlignment="Left" Name="label1" VerticalAlignment="Top" />
        <Label Content="UserName" Height="29" HorizontalAlignment="Left" Margin="22,70,0,0" VerticalAlignment="Top" />
        <Label Content="Password" Height="29" HorizontalAlignment="Left" Margin="22,112,0,0" VerticalAlignment="Top" />
        <TextBox Text="{Binding UserName}" Height="24" HorizontalAlignment="Left" Margin="96,72,0,0" VerticalAlignment="Top" Width="120" />
        <TextBox Text="{Binding Password}" Height="24" HorizontalAlignment="Left" Margin="96,114,0,0" VerticalAlignment="Top" Width="120" />
        <Button Content="Login" Command="{Binding LoginCommand}" Margin="61,157,189,39" />
        <Button Content="Cancel" Command="{Binding CancelCommand}" Margin="167,157,83,39" />
    </Grid>
</UserControl>  

これに伴って、MainWindowのDataTemplateは以下のような記述に変更することになるでしょう。

MainWindowのソースコードの抜粋(LoginDialogView.xaml適用)
    <i:Interaction.Behaviors>
        <mvvmdialogbehavor:DialogBehavior Content="{Binding Dialog}" >
            
            <mvvmdialogbehavor:DialogBehavior.Resources >
                <DataTemplate DataType="{x:Type local:LoginDialogViewModel}" >
                    <local:LoginDialogView />
                </DataTemplate>
            </mvvmdialogbehavor:DialogBehavior.Resources>
            
            <mvvmdialogbehavor:DialogBehavior.Style>
                <Style TargetType="Window">
                    <Setter Property="SizeToContent" Value="WidthAndHeight"/>
                </Style>
            </mvvmdialogbehavor:DialogBehavior.Style>
            
        </mvvmdialogbehavor:DialogBehavior>
    </i:Interaction.Behaviors> 

これで少しDialogBehavior内の記述が簡単になりました。


□もっと実践的にMVVM Dialog Behaviorを使う 2 (App.xamlへDataTemplate移動)

次に、このDataTemplateによるViewModelとViewのマッピングは、システム全体に対して1つあれば事足りることが多いでしょう。そうなると、このDataTemplateによるマッピングはApp.xamlに記述してしまえば良いことになります。

App.xamlのソースコード
<Application x:Class="MvvmDialogBehaviorSample.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:MvvmDialogBehaviorSample"
             StartupUri="MainWindow.xaml">
    <Application.Resources>
        <DataTemplate DataType="{x:Type local:LoginDialogViewModel}">
            <local:LoginDialogView />
        </DataTemplate>
    </Application.Resources>
</Application> 

これに伴って、MainWindowのDialogBehaviorからResourcesの記述も不要となります。

MainWindowのソースコードの抜粋(App.xamlへDataTemplate移動)
    <i:Interaction.Behaviors>
        <mvvmdialogbehavor:DialogBehavior Content="{Binding Dialog}" >
            
            <mvvmdialogbehavor:DialogBehavior.Style>
                <Style TargetType="Window">
                    <Setter Property="SizeToContent" Value="WidthAndHeight"/>
                </Style>
            </mvvmdialogbehavor:DialogBehavior.Style>
            
        </mvvmdialogbehavor:DialogBehavior>
    </i:Interaction.Behaviors>  

□もっと実践的にMVVM Dialog Behaviorを使う 3 (WindowのStyleの移動)

これでかなりDialogBehaviorも単純になりましたが、よくよく考えるとStyleのプロパティも、それぞれのダイアログ種別に応じて指定するものであるため、MainWindow.xaml内ではなく、LoginDialogView.xaml内に記述できたほうがよさそうです。

こうしたことから、MVVM Dialog Behaviorでは、「WindowStyleBehavior」クラスを提供しています。これを使用すると、LoginDialogView.xamlは以下のようなコードとなります。

LoginDialogViewのソースコード(WindowStyleBehavior適用)
<UserControl x:Class="MvvmDialogBehaviorSample.LoginDialogView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 
             xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
             xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
             xmlns:mvvmdialogbehavor="clr-namespace:MvvmDialogBehavior;assembly=MvvmDialogBehavior"
             mc:Ignorable="d" 
             d:DesignHeight="238" d:DesignWidth="338"  >
    
    <i:Interaction.Behaviors>
        <mvvmdialogbehavor:WindowStyleBehavior>
            <mvvmdialogbehavor:WindowStyleBehavior.Style>
                <Style TargetType="Window">
                    <Setter Property="SizeToContent" Value="WidthAndHeight"/>
                </Style>            
            </mvvmdialogbehavor:WindowStyleBehavior.Style>
        </mvvmdialogbehavor:WindowStyleBehavior>
    </i:Interaction.Behaviors>

    <Grid Height="250" Width="350" Background="Ivory">
        <Label Content="LoginDialogView" Height="29" HorizontalAlignment="Left" Name="label1" VerticalAlignment="Top" />
        <Label Content="UserName" Height="29" HorizontalAlignment="Left" Margin="22,70,0,0" VerticalAlignment="Top" />
        <Label Content="Password" Height="29" HorizontalAlignment="Left" Margin="22,112,0,0" VerticalAlignment="Top" />
        <TextBox Text="{Binding UserName}" Height="24" HorizontalAlignment="Left" Margin="96,72,0,0" VerticalAlignment="Top" Width="120" />
        <TextBox Text="{Binding Password}" Height="24" HorizontalAlignment="Left" Margin="96,114,0,0" VerticalAlignment="Top" Width="120" />
        <Button Content="Login" Command="{Binding LoginCommand}" Margin="61,157,189,39" />
        <Button Content="Cancel" Command="{Binding CancelCommand}" Margin="167,157,83,39" />
    </Grid>
</UserControl>   

WindowStyleBehaviorは、そのStyleプロパティの値で、自身のコントロールを保持するWindowのStyleを上書きするビヘイビアです。このビヘイビアにより、ダイアログのWindowのStyleをViewの中から設定することができます。

これで、DialogBehaviorでのStyleの記述も不要となりますので、結果的にMainWindowのDialogBehaviorにはContentプロパティしか残らないことになります。

MainWindowのソースコードの抜粋(WindowStyleの移動)
    <i:Interaction.Behaviors>
        <mvvmdialogbehavor:DialogBehavior Content="{Binding Dialog}" />
    </i:Interaction.Behaviors> 

Contentプロパティしか存在しないということは、MainWindowにはログインダイアログといった個別のダイアログに関する設定は含まれないということでもあります。これによってMainViewModelのDialogプロパティにセットするViewModelクラスの種類によって、ダイアログ種別を柔軟に切り替えることができるようになるのです。

実際のプロジェクトでは、この方法を全体的に使用していくのが良いでしょう。


□もっと実践的にMVVM Dialog Behaviorを使う 番外編 (WindowのStyleへのデータバインディング)

今までのWindowのStyleは静的に値を与えていましたが、これをダイアログのViewModelのプロパティとのデータバインディングによって動的に制御したい場合があるでしょう。その場合には以下のようにRelativeSourceを使用することで実現できます。

この例では、WindowのTitleプロパティに、ViewModelの WindowTitleプロパティをバインディングしています。

LoginDialogViewのソースコード(WindowのStyleにViewModelのプロパティをバインディング)
        <mvvmdialogbehavor:WindowStyleBehavior>
            <mvvmdialogbehavor:WindowStyleBehavior.Style>
                <Style TargetType="Window">
                    <Setter Property="Title" Value="{Binding Path=Content.WindowTitle, 
                        RelativeSource={RelativeSource Mode=Self}}"/>
                </Style>
            </mvvmdialogbehavor:WindowStyleBehavior.Style>
        </mvvmdialogbehavor:WindowStyleBehavior>  

{RelativeSource Mode=Self}はダイアログのWindowインスタンスを表現します。そして、WindowのContentにはダイアログのViewModelが設定されていることを思い出してください。このため、Pathでは、Contentに続けてダイアログのViewModelのプロパティ名を記述することで、データバインドを行うことができます。

さて、既にもうほとんどのMVVM Dialog Behaviorの機能を使いながら紹介しました。あと2つだけ紹介していない機能があります。次にこれらについて説明していきます。


□その他のMVVM Dialog Behaviorの機能 1 (IsModalプロパティ)

DialogBehaviorのIsModalプロパティは、モーダルにダイアログを表示するかをboolで指定します。デフォルトではモーダル(True)です。

MainWindowのソースコードの抜粋(IsModalを使用)
    <i:Interaction.Behaviors>
        <mvvmdialogbehavor:DialogBehavior Content="{Binding Dialog}" IsModal="False" />
    </i:Interaction.Behaviors> 


□その他のMVVM Dialog Behaviorの機能 2 (ContentTemplateプロパティ)

また、DialogBehaviorのContentTemplateプロパティは、ResourecesにDataTemplateを指定するよりも直接的にダイアログのViewを指定することができます。ContentTemplateプロパティのDataTemplateでは、マッピング対象の型に関わらず適用される型(DataTypeプロパティ)の指定が不要です。このため、ダイアログの見た目(View)を固定で決め打ちする場合に適しています。

MainWindowのソースコードの抜粋(LoginDialogView.xaml適用)
    <i:Interaction.Behaviors>
        <mvvmdialogbehavor:DialogBehavior Content="{Binding Dialog}" >
            
            <mvvmdialogbehavor:DialogBehavior.ContentTemplate>
                <DataTemplate>
                    <local:LoginDialogView />
                </DataTemplate>
            </mvvmdialogbehavor:DialogBehavior.ContentTemplate>

        </mvvmdialogbehavor:DialogBehavior>
    </i:Interaction.Behaviors> 

以上で、MVVM Dialog Behaviorの機能の説明はひととおり終了です。後はいくつか補足をしてMVVM Dialog Behaviorの説明を終了としましょう。


□[補足]ラベルやボタンでのコンテンツの表示と、ダイアログ表示とで、同じプログラミング体験を提供します

DataTemplateにあまりなじみがない方には、DialogBehaviorビヘイビアのこの使用方法はちょっと難しかったかもしれません。しかし、これはラベルやボタン(ContentControlを継承している)のでのコンテンツ表示と同じプログラミング体験を提供するという目的によるものです。

実は、DialogBehaviorビヘイビアをContentControl置き換えて(+さらにほんの少しだけ修正する必要がありますが。)も、そのまま動作します。

この場合、メインウィンドウのOpenDialogボタンを押すと、なんと以下のようにメインウィンドウ内にログインダイアログが出現します。もちろんLoginボタンを押すと元通りに消えるというダイアログらしさもそのままです。


メインウィンドウ(ContentControl版)


このContentControlによるコンテンツの表示は、MVVMではよく知られたテクニックの一つです。(知らなかった方はこの機会に是非慣れ親しんでください。)

このように、MVVM Dialog Behaviorでは、ラベルやボタン(ContentControlを継承している)のコンテンツ表示と、ダイアログ表示とが同様のプログラミング体験となるように、そのインタフェースが設計されているのです。

ちなみに、以下のようにMainWindowのソースコードを修正すると上記ことが実現できます。

MainWindowのソースコード(ContentControl版)
<Window x:Class="MvvmDialogBehaviorSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MvvmDialogBehaviorSample"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
        xmlns:cmd="clr-namespace:GalaSoft.MvvmLight.Command;assembly=GalaSoft.MvvmLight.Extras.WPF4"
        xmlns:mvvmdialogbehavor="clr-namespace:MvvmDialogBehavior;assembly=MvvmDialogBehavior"
        Title="MainWindow" Height="275" Width="375" Background="Azure">
    <Window.DataContext>
        <local:MainViewModel />
    </Window.DataContext>

    <Grid>
        <ContentControl Content="{Binding Dialog}">
            <ContentControl.Resources>
                <DataTemplate DataType="{x:Type local:LoginDialogViewModel}" >
                    <Grid Height="250" Width="350" Background="Ivory">
                        <Label Content="LoginDialogView" Height="29" HorizontalAlignment="Left" Name="label1" VerticalAlignment="Top" />
                        <Label Content="UserName" Height="29" HorizontalAlignment="Left" Margin="22,70,0,0" VerticalAlignment="Top" />
                        <Label Content="Password" Height="29" HorizontalAlignment="Left" Margin="22,112,0,0" VerticalAlignment="Top" />
                        <TextBox Text="{Binding UserName}" Height="24" HorizontalAlignment="Left" Margin="96,72,0,0" VerticalAlignment="Top" Width="120" />
                        <TextBox Text="{Binding Password}" Height="24" HorizontalAlignment="Left" Margin="96,114,0,0" VerticalAlignment="Top" Width="120" />
                        <Button Content="Login" Command="{Binding LoginCommand}" Margin="61,157,189,39" />
                        <Button Content="Cancel" Command="{Binding CancelCommand}" Margin="167,157,83,39" />
                    </Grid>
                </DataTemplate>
            </ContentControl.Resources>
        </ContentControl>

        <Button Content="Open Dialog" Command="{Binding OpenDialogCommand}" Height="49" HorizontalAlignment="Left" Margin="88,83,0,0" Name="button1" VerticalAlignment="Top" Width="160" />
    </Grid>
</Window> 


□[補足]ダイアログの閉じるボタンを押されたらどうなる?

ダイアログには、何も指定しないと右上に閉じるボタンが表示されます。このボタンを押した場合、DialogBehaviorはどのような振る舞いをするのでしょうか?

実は、ダイアログが閉じられるとDialogBehaviorのContentプロパティはNullに設定されます。このContentプロパティにダイアログViewModelのプロパティがバインドされるでしょうから、バインディング機能により、ダイアログViewModelへプロパティも同時にNullに設定されることになります。

実はこの仕組みによって、ダイアログViewModelインスタンスが存在するかNullかでダイアログの表示中かどうかが判断できるように、常にViewModel側の状態に同期されることになるのです。

なお、Alt+F4などのウィンドウを閉じるショートカットキーが押された場合でも同様の動きをします。


□[補足]メッセンジャーによるダイアログ表示との使い分け

メッセンジャーによるダイアログ表示はMVVMに反するところがあると述べましたが、実際のところ、OKやCancelボタンがあるだけのような単純なメッ セージボックスや、ファイル選択のダイアログなどであれば、そもそもViewModelで制御したいとも思わないかもしれません。

そうした場合には、 MVVM Dialog Behaviorを使用する必要はなく、何れかのMVVMフレームワークが提供するメッセージボックスのためのメッセンジャー(およびメッセージやアク ション)を使用するのも良いでしょう。

しかし、ある程度の複雑なプレゼンテーションロジックを持ったダイアログを表示する場合や、ダイアログ表示中かどうかをViewModel内で判断してその表示を制御したいなどの場合、あるいはダイアログのテーマを自由にデザインしたい場合には、このMVVM Dialog Behaviorの使用を検討する価値があります。

プロジェクト要件に応じて使い分けるようにしてください。


□まとめ

これで、MVVMでメッセンジャーなしにダイアログ表示をシンプルに実現させるMVVM Dialog Behaviorライブラリのご紹介は終了です。

メッセンジャーによるダイアログ表示に疑問を持ったらこのライブラリを使ってみることを検討してください。このライブラリが少しでも皆様のMVVMライフの足しになれば幸いです。
 
長文お疲れ様でした。

以上。

人気の投稿