「MVVMでダイアログ表示のためにメッセンジャーを使用するのはおかしいのでは?」 -ダイアログ表示を画面状態としてモデル化する-


[2012/01/05 補足] 
本記事を記述した後、ダイアログ表示のためにライブラリを作成しました。このライブラリの紹介と解説のために「MVVMでメッセンジャーを使わずにダイアログを表示する(MVVM Dialog Behaviorライブラリ提供)」と題したブログを改めて記述しましたので、必要に応じてこちらをご参照ください。

MVVMでダイアログ表示をするためにメッセンジャーを使うのは、現在では最も主流の方法でしょう。しかし以前、MVVMの説明を行ってすぐの技術者に「MVVMでダイアログ表示のためにメッセンジャーを使用するのはおかしいのでは?」と聞かれたとき、説明を続けられずに途中で窮してしまったことがあります。

MVVMパターンはPresentation Modelパターンの一種であり、ViewModelに抽象化された画面状態を保持します。一方、メッセンジャーを使用する方法は、処理の起動であって、あくまでイベントを送るようなものであり、画面状態とは相容れない考え方です。

イメージボタンのようなボタンの中のコンテンツや、MetroUIのようなタイルの中のコンテンツが画面状態と認められる一方で、どうしてダイアログ表示は画面の状態とは認められないのでしょうか?疑問に思い始め、今ではダイアログ表示は画面の状態であるという 考え方を支持しています。

それでは、メッセジャーを使わずにダイアログ表示を実現するにはどうすればよいのでしょうか。最近になって考えている方法をご紹介します。なお残念ながら、この方法は実プロジェクトで実績を積んでいません。あくまで考え方とサンプルとしてお楽しみください。

メッセンジャーを使わずに、ダイアログを画面状態として扱うためには、改めてPresentation Modelパターンに立ち戻る必要があります。

Presentation Modelパターンは、画面の抽象的な状態を表現し、その抽象レベルのまま画面を管理するための方法です。ViewではなくPresentation Modelが主体となるため、スタートとしてダイアログ表示を抽象的にとらえるとどのようなPresentaion Modelとなるのかをまず考える必要があります。

MVVMパターンでは、Presentation ModelはView Modelのことですので、つまるところ、ダイアログ表示をViewModelとして設計するとどのようになるのかということから考える必要があるということです。

ダイアログ表示を 比較的単純にとらえた場合、モデルとしては以下のようになります。

MainViewModel  ◆------ 0,1  DialogViewModel

MainViewModelがダイアログ呼び出し元のViewModelとして存在します。

そして、ダイアログが表示されているかどうかは、MainViewModelが持つDialogViewModel型の属性に、インスタンスが存在するかNullかで判断するというモデルです。これは、上記の「0,1」という多重度に現れています。つまりダイアログ表示を行うためには、MainViewModelが持つDialogViewModel型の属性にDialogViewModelのインスタンスをセットし、ダイアログクローズのためにはNullにセットしなおせばよいことになります。

まずは、MainViewModelのコード例です。

public class MainViewModel
    {
        private DialogViewModel _DialogViewModel;

        public DialogViewModel DialogViewModel
        {
            get
            {
                return _DialogViewModel;
            }
            set
            {
                if (_DialogViewModel != value)
                {
                    _DialogViewModel = value;
                    OnPropertyChanged("DialogViewModel");
                }
            }
        }

        private InvokeCommand _OpenDialogCommand;
        public InvokeCommand OpenDialogCommand
        {
            get
            {
                if (_OpenDialogCommand == null)
                {
                    _OpenDialogCommand = new InvokeCommand(OpenDialog);
                }
                return _OpenDialogCommand;
            }
        }

        public void OpenDialog()
        {
            // ダイアログオープン
            DialogViewModel = new DialogViewModel(CloseDialogAction) { UserName = "ユーザ名だよ", Password = "パスワードだよ" };
        }

        public void CloseDialogAction()
        {
            // ダイアログクローズ
            DialogViewModel = null;
        }
    }

まず注目すべきは、ダイアログオープンのために、 DialogViewMode属性へのインスタンスのセットしか行っていない点です。そして、ダイアログに引き渡すべき、ユーザ名やパスワードがViewModelのプロパティによる単純な受け渡しで実現できており、メッセンジャーのように値の引き渡しや値の返り値を意識したコーディングではなくなっています。このように、ダイアログでの間の連携がしやすいのは、ダイアログを画面状態とした際のメリットです。

また、CloseDialogAction()メソッドでは、ダイアログクローズのためには、DialogViewMode属性にNullを設定しています。なお、ここで問題になるのが、どこからこのCloseDialogAction()メソッドを呼び出すのかということです。ここでは、ダイアログ側からのクローズを考慮して、このメソッド自体をDialogViewModelのコンストラクタに渡して、呼び出せるようにしていることに注意してください。このあたりの実装方法は、いろいろ考えられそうですので、それぞれのプロジェクトに合わせて実装すればよいと思います。

次に、DialogViewModelのコード例です。

public class DialogViewModel
    {
        public Action CloseAction;

        public DialogViewModel(Action closeAction)
        {
            CloseAction = closeAction;
        }

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

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

        private InvokeCommand _LoginCommand;
        public InvokeCommand LoginCommand
        {
            get
            {
                if (_LoginCommand == null)
                {
                    _LoginCommand = new InvokeCommand(Login);
                }
                return _LoginCommand;
            }
        }

        private InvokeCommand _CancelCommand;
        public InvokeCommand CancelCommand
        {
            get
            {
                if (_CancelCommand == null)
                {
                    _CancelCommand = new InvokeCommand(Cancel);
                }
                return _CancelCommand;
            }
        }

        public void Login()
        {
            if (DoLogin())
            {
                CloseAction();
            }
        }

        public void Cancel()
        {
            CloseAction();
        }

        public bool DoLogin()
        {
            // 本来はログイン処理を記述する
            return true;
        }
 

ログインダイアログをイメージして、ユーザ名とパスワードの属性を用意しています。大したコードでもありませんが、先ほど説明したとおり、自身のダイアロ グをクローズするために、MainViewModelから渡されたCloseDialogAction()メソッドをコールバックしている点はご注意くだ さい。

このDialogViewModelのインスタンスのセットに合わせて、画面側でダイアログオープンと、クローズを同期するには、 添付ビヘイビアを使用する必要があります(なお、この添付ビヘイビアは、一度作成すれば他のダイアログでも使いまわすことが可能です)。まずは、この添付ビヘイビアをXamlで使用した場合の例から見てみます。

 <Window x:Class="Local.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:Local"
        Title="MainWindow" Height="350" Width="525" x:Name="This" 
        local:DialogBehavior.Content="{Binding DialogViewModel}">
    <Window.DataContext>
        <local:MainViewModel/>
    </Window.DataContext>
    <local:DialogBehavior.ContentTemplate>
        <DataTemplate>
            <StackPanel Margin="30" Width="300" Height="200">
                <TextBox Text="{Binding UserName}" />
                <TextBox Text="{Binding Password}" />
                <StackPanel Orientation="Horizontal">
                    <Button Content="ログイン" Command="{Binding LoginCommand}"/>
                    <Button Content="キャンセル" Command="{Binding CancelCommand}"/>
                </StackPanel>
            </StackPanel>
        </DataTemplate>
    </local:DialogBehavior.ContentTemplate>
    <StackPanel>
        <Button Content="Login Button" Height="50" Width="150" Command="{Binding OpenDialogCommand}" />
    </StackPanel>
</Window>


添付ビヘイビアは、local:DialogBehavior.Contentlocal:DialogBehavior.ContentTemplateWindowに対して指定されています。

 Contentの添付プロパティはダイアログを表示する際に使用されるViewModelを指定します。このプロパティにViewModelがセットされている間ダイアログが表示されます。Nullになった場合にはダイアログがクローズします。

 ContentTemplate添付プロパティは、ダイアログ表示のためのデータテンプレートです。そのDataContextには、自動的に先ほどのContentのViewModelが設定されるものとします。

なお、この画面は以下のように表示されます。

 □メインウィンドウ

□ダイアログ(メインウィンドウの「Login Button」で起動される)



そして、この添付ビヘイビアの実装は以下の通りです。

     public class DialogBehavior:DependencyObject
    {
        public static readonly DependencyProperty ContentTemplateProperty =
        DependencyProperty.RegisterAttached(
        "ContentTemplate",
        typeof(DataTemplate),
        typeof(DialogBehavior),
        new UIPropertyMetadata(null, (depObj, e) =>
         {
             var self = depObj as FrameworkElement;

             OnPropertyChanged(self, e);
         }));

        public static readonly DependencyProperty ContentProperty =
        DependencyProperty.RegisterAttached(
        "Content",
        typeof(Object),
        typeof(DialogBehavior),
        new UIPropertyMetadata(null, (depObj, e) =>
         {
             var self = depObj as FrameworkElement;

             OnPropertyChanged(self, e);
         }));

        public static readonly DependencyProperty DialogProperty =
        DependencyProperty.RegisterAttached(
        "Dialog",
        typeof(Window),
        typeof(DialogBehavior));

        public static Object GetContent(FrameworkElement element)
        {
            return (Object)element.GetValue(ContentProperty);
        }

        public static void SetContent(FrameworkElement element, DataTemplate val)
        {
            element.SetValue(ContentProperty, val);
        }

        public static DataTemplate GetContentTemplate(FrameworkElement element)
        {
            return (DataTemplate)element.GetValue(ContentTemplateProperty);
        }

        public static void SetContentTemplate(FrameworkElement element, DataTemplate val)
        {
            element.SetValue(ContentTemplateProperty, val);
        }

        private static Window GetDialog(FrameworkElement element)
        {
            return (Window)element.GetValue(DialogProperty);
        }

        private static void SetDialog(FrameworkElement element, Window val)
        {
            element.SetValue(DialogProperty, val);
        }

        public static void OnPropertyChanged(FrameworkElement self, DependencyPropertyChangedEventArgs args)
        {
            if (GetContentTemplate(self) != null)
            {
                if (GetDialog(self) != null)
                {
                    CloseDialog(self);
                }

                if (GetContent(self) != null)
                {
                    OpenDialog(self);
                }
            }
        }

        private static void OpenDialog(FrameworkElement self)
        {
            var dialog = new Window();
            dialog.WindowStyle = WindowStyle.None;
            dialog.ContentTemplate = GetContentTemplate(self);
            dialog.Content = GetContent(self);
            dialog.SizeToContent = SizeToContent.WidthAndHeight;

            SetDialog(self, dialog);
            dialog.ShowDialog();
        }

        private static void CloseDialog(FrameworkElement self)
        {
            var dialog = GetDialog(self);
            dialog.Close();
            SetDialog(self, null);
        }
    }


定型コードが多いのですが、大したことはやっていません。注意点としてあえて言うなら、表示させているダイアログのインスタンスを内部的に保持するためにプライベートの添付プロパティとして「Dialog」を使用していることぐらいでしょうか。

さて、ダイアログ表示を画面状態として扱う方法を見てきました。私の個人的な意見ですが、ダイアログを画面状態として明示的に扱う方法のほうが、ダイアログ表示を十分に抽象化していて連携を行いやすく、Presentation Modelパターンにより準拠していると思われます。今後はメッセンジャーを使用しなくなっていくのではないでしょうか。

今回のサンプルは、まだ実プロジェクトで使用していないため、十分な実績を積んだ方法としてすぐにお勧めすることはできません。あくまで方向性のご提案ですので、その点ご理解いただければ幸いです。

以上。

人気の投稿