読者です 読者をやめる 読者になる 読者になる

SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

MVVMな設計でVMからダイアログ表示を行う

MVVM界隈でよく話題になっている、「VMからViewに指示をしたい」という時の定番ネタ。
VMからダイアログを開く方法を添付ビヘイビアで作ってみました。

↓の記事を参考に作ってみました。
トロこんぶ: 「MVVMでダイアログ表示のためにメッセンジャーを使用するのはおかしいのでは?」 -ダイアログ表示を画面状態としてモデル化する-

仕組み

大まかな仕組みは以下の図の通り。
f:id:minami_SC:20140406233117p:plain

  1. 親となるウィンドウのVMが、ダイアログのVMをプロパティとして持つようにする。
  2. 添付ビヘイビアを親ウィンドウに付加
    1. 上記プロパティにダイアログのVMがセットされnullから変化したらダイアログを表示
    2. ダイアログのClosedイベントでは、親ウィンドウのVMに用意したコマンドを呼び出す
    3. ダイアログ画面での操作結果を扱いたい場合は、上記コマンドで処理


参考にした上記記事の実装との違いは主に以下の点でしょうか。
・ダイアログを閉じる際の処理を、デリゲートで渡すのではなくVMのコマンドを渡す
・ウィンドウのClosedイベントで、上記VMのコマンドを実行


一応実装上での工夫点というか、若干トリッキーなとこは以下の部分かな。
添付ビヘイビアで表示したウィンドウのClosedイベントを登録してるところ。
Closedイベントのイベントハンドラからは、添付ビヘイビアのプロパティなどにアクセスする手段がないので、ラムダ式の中で必要な変数をキャプチャして使っています。

コード

この添付ビヘイビアを使った簡単なサンプルです。
f:id:minami_SC:20140406234053p:plain
Openボタンを押すとダイアログを開きます。
で、親ウィンドウのテキストボックスに入力されてた値を、ダイアログ側のテキストボックスにも表示します。
その後、ダイアログ側のテキストボックスを編集して、ダイアログでOKを押したら、ダイアログ側のテキスト変更結果を親ウィンドウにも反映します。

OpenWindowAttachedBehavior.cs
    public class OpenWindowAttachedBehavior
    {
        public static bool GetIsModal(DependencyObject obj)
        {
            return (bool)obj.GetValue(IsModalProperty);
        }
        public static void SetIsModal(DependencyObject obj, bool value)
        {
            obj.SetValue(IsModalProperty, value);
        }
        // Using a DependencyProperty as the backing store for IsModal.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty IsModalProperty =
            DependencyProperty.RegisterAttached("IsModal", typeof(bool), typeof(OpenWindowAttachedBehavior), new PropertyMetadata(true));


        public static bool GetHasOwner(DependencyObject obj)
        {
            return (bool)obj.GetValue(HasOwnerProperty);
        }
        public static void SetHasOwner(DependencyObject obj, bool value)
        {
            obj.SetValue(HasOwnerProperty, value);
        }
        // Using a DependencyProperty as the backing store for HasOwner.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty HasOwnerProperty =
            DependencyProperty.RegisterAttached("HasOwner", typeof(bool), typeof(OpenWindowAttachedBehavior), new PropertyMetadata(false));


        public static ICommand GetCloseCommand(DependencyObject obj)
        {
            return (ICommand)obj.GetValue(CloseCommandProperty);
        }
        public static void SetCloseCommand(DependencyObject obj, ICommand value)
        {
            obj.SetValue(CloseCommandProperty, value);
        }
        // Using a DependencyProperty as the backing store for CloseCommand.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty CloseCommandProperty =
            DependencyProperty.RegisterAttached("CloseCommand", typeof(ICommand), typeof(OpenWindowAttachedBehavior), new PropertyMetadata(null));


        public static DataTemplate GetWindowTemplate(DependencyObject obj)
        {
            return (DataTemplate)obj.GetValue(WindowTemplateProperty);
        }
        public static void SetWindowTemplate(DependencyObject obj, DataTemplate value)
        {
            obj.SetValue(WindowTemplateProperty, value);
        }
        // Using a DependencyProperty as the backing store for WindowTemplate.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty WindowTemplateProperty =
            DependencyProperty.RegisterAttached("WindowTemplate", typeof(DataTemplate), typeof(OpenWindowAttachedBehavior), new PropertyMetadata(null));


        public static object GetWindowViewModel(DependencyObject obj)
        {
            return (object)obj.GetValue(WindowViewModelProperty);
        }
        public static void SetWindowViewModel(DependencyObject obj, object value)
        {
            obj.SetValue(WindowViewModelProperty, value);
        }
        // Using a DependencyProperty as the backing store for WindowViewModel.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty WindowViewModelProperty =
            DependencyProperty.RegisterAttached("WindowViewModel", typeof(object), typeof(OpenWindowAttachedBehavior), new PropertyMetadata(null, OnWindowViewModelChanged));

        private static void OnWindowViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            var element = d as FrameworkElement;
            if (element == null)
                return;

            var template = GetWindowTemplate(d);
            var viewmodel = GetWindowViewModel(d);

            // テンプレートが指定されてないと、ウィンドウは表示できない。
            if (template != null)
            {
                if (viewmodel != null)
                {
                    // VMがセットされたらウィンドウ表示
                    OpenWindow(element);
                }
                else
                {
                    // VMがnullになったら、ウィンドウを閉じる
                    CloseWindow(element);
                }
            }
        }

        private static void OpenWindow(FrameworkElement element)
        {
            var isModal = GetIsModal(element);
            var win = GetWindow(element);
            var cmd = GetCloseCommand(element);
            var template = GetWindowTemplate(element);
            var vm = GetWindowViewModel(element);
            var owner = Window.GetWindow(element);
            var hasOwner = GetHasOwner(element);

            if(win == null)
            {
                win = new Window()
                {
                    ContentTemplate = template,
                    Content = vm,
                    SizeToContent = System.Windows.SizeToContent.WidthAndHeight,
                    Owner = hasOwner ? owner : null,
                };

                // ウィンドウの終了処理追加
                // イベントハンドラの引数からは添付ビヘイビアのプロパティにアクセスできないので、
                // ラムダ式でキャプチャする
                win.Closed += (s, e) =>
                    {
                        if (cmd != null)
                        {
                            // ダイアログのVMを引数にCloseCommand実行
                            if (cmd.CanExecute(vm))
                                cmd.Execute(vm);
                        }
                        SetWindow(element, null);
                    };

                // ウィンドウの表示処理
                SetWindow(element, win);
                if (isModal)
                    win.ShowDialog();
                else
                    win.Show();
            }
            else
            {
                // すでにウィンドウが表示されているので、アクティブ化で前面に出す
                win.Activate();
            }
        }

        private static void CloseWindow(FrameworkElement element)
        {
            var win = GetWindow(element);

            if (win != null)
            {
                win.Close();
                SetWindow(element, null);
            }
        }


        #region 添付ビヘイビアで使用する内部プロパティ
        public static Window GetWindow(DependencyObject obj)
        {
            return (Window)obj.GetValue(WindowProperty);
        }
        public static void SetWindow(DependencyObject obj, Window value)
        {
            obj.SetValue(WindowProperty, value);
        }
        // Using a DependencyProperty as the backing store for Window.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty WindowProperty =
            DependencyProperty.RegisterAttached("Window", typeof(Window), typeof(OpenWindowAttachedBehavior), new PropertyMetadata(null));
        #endregion
    }
MainWindow.xaml
<Window x:Class="AttachedBehaviorTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:AttachedBehaviorTest"
        Title="MainWindow"
        Width="300"
        Height="200">
    <Grid local:OpenWindowAttachedBehavior.CloseCommand="{Binding CloseDialogCommand}"
          local:OpenWindowAttachedBehavior.HasOwner="True"
          local:OpenWindowAttachedBehavior.IsModal="True"
          local:OpenWindowAttachedBehavior.WindowViewModel="{Binding DialogVM}">
        <local:OpenWindowAttachedBehavior.WindowTemplate>
            <DataTemplate>
                <local:DialogView />
            </DataTemplate>
        </local:OpenWindowAttachedBehavior.WindowTemplate>
        <TextBox Width="120"
                 Height="23"
                 Margin="10,10,0,0"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Top"
                 Text="{Binding Message}"
                 TextWrapping="Wrap" />
        <Button Width="75"
                Margin="10,38,0,0"
                HorizontalAlignment="Left"
                VerticalAlignment="Top"
                Command="{Binding OpenDialogCommand}"
                Content="Open" />
    </Grid>
</Window>
MainWindow.xaml.cs
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            this.DataContext = new MainWindowViewModel();
        }
    }
MainWindowViewModel.cs
    class MainWindowViewModel : BindableBase
    {
        private string message;
        public string Message
        {
            get { return message; }
            set { this.SetProperty(ref this.message, value); }
        }


        private DialogViewModel dialogVM;
        /// <summary>
        /// ダイアログ画面のViewModel
        /// </summary>
        /// <remarks>
        /// このプロパティにダイアログのVMをセットすると、ダイアログ画面が表示されます。
        /// nullにセットすると画面を閉じます。
        /// </remarks>
        public DialogViewModel DialogVM
        {
            get { return dialogVM; }
            set { this.SetProperty(ref this.dialogVM, value); }
        }

        #region コマンドの実装
        private RelayCommand openDialogCommand;
        public RelayCommand OpenDialogCommand
        {
            get { return openDialogCommand = openDialogCommand ?? new RelayCommand(OpenDialog); }
        }

        private void OpenDialog()
        {
            this.DialogVM = new DialogViewModel() { Message = this.Message };
        }


        private RelayCommand<DialogViewModel> closeDialogCommand;
        public RelayCommand<DialogViewModel> CloseDialogCommand
        {
            get { return closeDialogCommand = closeDialogCommand ?? new RelayCommand<DialogViewModel>(CloseDialog); }
        }

        private void CloseDialog(DialogViewModel parameter)
        {
            // ダイアログクローズ時の処理
            if (parameter.Result)
            {
                // ダイアログがOKで閉じられた場合の処理
                this.Message = parameter.Message;
            }
        }
        #endregion
    }
DialogView.xaml
<UserControl x:Class="AttachedBehaviorTest.DialogView"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:AttachedBehaviorTest"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             Width="300"
             Height="300"
             d:DesignHeight="300"
             d:DesignWidth="300"
             local:CloseWindowAttachedBehavior.Close="{Binding CloseWindow}"
             mc:Ignorable="d">
    <Grid>
        <TextBox Width="120"
                 Height="23"
                 Margin="10"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Top"
                 Text="{Binding Message}"
                 TextWrapping="Wrap" />
        <Button Width="75"
                Height="19"
                Margin="0,0,90,10"
                HorizontalAlignment="Right"
                VerticalAlignment="Bottom"
                Command="{Binding OkCommand}"
                Content="OK"
                IsDefault="True" />
        <Button Width="75"
                Height="19"
                Margin="0,0,10,10"
                HorizontalAlignment="Right"
                VerticalAlignment="Bottom"
                Content="Cancel"
                IsCancel="True" />
    </Grid>
</UserControl>
DialogViewModel.cs
    class DialogViewModel : BindableBase
    {
        private string message;
        public string Message
        {
            get { return message; }
            set { this.SetProperty(ref this.message, value); }
        }

        private bool closeWindow;
        public bool CloseWindow
        {
            get { return closeWindow; }
            set { this.SetProperty(ref this.closeWindow, value); }
        }

        private bool result;
        public bool Result
        {
            get { return result; }
            set { this.SetProperty(ref this.result, value); }
        }


        #region コマンドの実装
        private RelayCommand okCommand;
        public RelayCommand OkCommand
        {
            get { return okCommand = okCommand ?? new RelayCommand(Ok); }
        }

        private void Ok()
        {
            this.Result = true;
            // ウィンドウを閉じる
            this.CloseWindow = true;
        }
        #endregion
    }