MVVM パターンで ViewModel から Viewを操作する方法(2)

えむナウさんがまたやってくれました

添付ビヘイビアーの利点は View のコードビハインドを書かなくてすむ、一度作成すると再利用が容易、使用するのにXAMLだけで書ける、

Blend でサポートされている、といろいろある。

MVVM パターンで VM から VIEW を操作するには、添付ビヘイビアーを使用することも推奨する。

MVVM パターンで VM から VIEW を操作したい その2

この発想も無かったです。
添付ビヘイビアはひさしく使ってないから、頭の片隅に追いやっていました。。。

私は文章だけだと理解できないので実践してみます

Expression Blend は持っていないので、Visual C# 2010 Express Edition で。
添付ビヘイビアはコードで書きます。
前回の依存関係プロパティを使ったサンプルを、添付ビヘイビアを使ったものに書き変えていきます。

添付ビヘイビアを実装します

using System.Windows;
using System.Windows.Input;
using System.Windows.Controls;

namespace MvvmSample
{
    public static class ViewBehavior
    {
        private static readonly DependencyProperty HasShowMessageCommandProperty = DependencyProperty.RegisterAttached(
            "HasShowMessageCommand",
            typeof(bool),
            typeof(ViewBehavior),
            new PropertyMetadata(false));

        public static readonly DependencyProperty ShowMessageCommandProperty = DependencyProperty.RegisterAttached(
            "ShowMessageCommand",
            typeof(ICommand),
            typeof(ViewBehavior),
            new PropertyMetadata(null, ShowMessageCommandChanged, CoerceShowMessageCommand));

        public static ICommand GetShowMessageCommand(DependencyObject textBox)
        {
            return textBox.GetValue(ShowMessageCommandProperty) as ICommand;
        }

        public static void SetShowMessageCommand(DependencyObject textBox, ICommand command)
        {
            textBox.SetValue(ShowMessageCommandProperty, command);
        }

        private static void ShowMessageCommandChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
        {
            // 今のところ何もしない
        }

        private static object CoerceShowMessageCommand(DependencyObject sender, object value)
        {
            var hasCommand = (bool)sender.GetValue(HasShowMessageCommandProperty);
            if (hasCommand == false)
            {
                var command = new DelegateCommand(
                    ShowMessageCommandExecute,
                    CanShowMessageCommandExecute);
                sender.SetValue(HasShowMessageCommandProperty, true);
                sender.SetValue(ShowMessageCommandProperty, command);
            }
            return GetShowMessageCommand(sender);
        }

        private static void ShowMessageCommandExecute(object param)
        {
            MessageBox.Show(param.ToString());
        }

        private static bool CanShowMessageCommandExecute(object param)
        {
            return param != null;
        }
    }
}

OneWayToSource でバインドするのが前提なので、ViewModel に渡すための Command を内部で作成しています。

CoerceShowMessageCommand 内で GetShowMessageCommand(sender) を使って Command が設定済みかどうか判定するとスタックオーバーフローになってしまうため、HasShowMessageCommandProperty を使って回避しています。

ViewModel を実装します

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Windows.Input;

namespace MvvmSample
{
    public class GreetingViewModel : INotifyPropertyChanged
    {
        private string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                if (_name != value)
                {
                    _name = value;
                    OnPropertyChanged("Name");
                }
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void OnPropertyChanged(string propertyName)
        {
            var h = PropertyChanged;
            if (h != null)
            {
                h(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        // あいさつをメッセージボックスで表示するコマンド
        private ICommand _greetCommand;
        public ICommand GreetCommand
        {
            get
            {
                return _greetCommand = _greetCommand ??
                    new DelegateCommand(_ =>
                    {
                        // View の機能を呼び出す
                        if (ShowMessageCommand.CanExecute(Name))
                        {
                            ShowMessageCommand.Execute(string.Format("Hello, {0}.", Name));
                        }
                    });
            }
        }

        // View が公開するコマンドがこのプロパティにバインドされる
        public ICommand ShowMessageCommand { get; set; }
    }
}

依存関係プロパティを利用した前回のサンプルと、まったく同じです。

View を実装します

<UserControl x:Class="MvvmSample.GreetingView"
             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" 
             mc:Ignorable="d" 
             d:DesignHeight="300" d:DesignWidth="300">
    <StackPanel>
        <TextBox Text="{Binding Path=Name}"/>
        <Button Content="Greet"
                Command="{Binding Path=GreetCommand}"/>
    </StackPanel>
</UserControl>

XAML は前回と同じです。

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace MvvmSample
{
    public partial class GreetingView : UserControl
    {
        public GreetingView()
        {
            InitializeComponent();
        }
    }
}

ViewModel の Command を View にバインドするための依存関係プロパティは不要なので、コードビハインドは何も追加していません。

添付ビヘイビアを使ってCommandをバインドします

<Window x:Class="MvvmSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:MvvmSample"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <!--添付ビヘイビアを使ってViewModelのCommandをバインド-->
        <local:GreetingView
            local:ViewBehavior.ShowMessageCommand="{Binding Path=ShowMessageCommand, Mode=OneWayToSource}">
            <local:GreetingView.DataContext>
                <local:GreetingViewModel/>
            </local:GreetingView.DataContext>
        </local:GreetingView>
    </Grid>
</Window>

OneWayToSource を指定して、添付ビヘイビアが内部で作成する Command を ViewModel に渡します。

再利用できるからスマートですね!

ViewModel に Command のプロパティを用意するのは変わらないですけど、View に依存関係プロパティを用意する手間が省ける分、スマートですね。再利用もできるし。

例えば、「コントロールにフォーカスを設定するコマンド」を添付ビヘイビアにしておけば、Viewの中で使っているコントロール1つ1つに添付ビヘイビアを適用できます。ユーザーの入力をまとめて検証し、エラーになったコントロールにフォーカスを設定する、なんてことが比較的楽に実現できそうです。