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

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つに添付ビヘイビアを適用できます。ユーザーの入力をまとめて検証し、エラーになったコントロールにフォーカスを設定する、なんてことが比較的楽に実現できそうです。