なか日記

一度きりの人生、楽しく生きよう。

MVVMに触れてみた(4)

前回「MVVMに触れてみた(3) - タイトルは未定」の続きで、今回はコードビハインドに残っているCalcメソッドを呼び出す一連の処理を消してしまいましょう。

ICommandインタフェース

コマンドを定義します。

ICommand インターフェイス (System.Windows.Input)

ViewからViewModelにある処理を呼ぶには、ICommandインタフェースを実装したメンバが必要です。
とりあえず、ICommandインタフェースを実装したCalcCommandクラスを作成してみましょう。

CalcCommand.cs
using System;
using System.Windows.Input;

namespace MVVMSample
{
    public class CalcCommand : ICommand
    {
        public void Execute(object parameter)
        {
            throw new NotImplementedException();
        }

        public bool CanExecute(object parameter)
        {
            throw new NotImplementedException();
        }

        public event EventHandler CanExecuteChanged;
    }
}

Executeには実行する処理をCanExecuteには実行できるかどうか(ボタンの有効、無効)を判定する処理を書きます。
で、このクラスを実装して、BmiViewModelに組み込めばいいような気がしますが、ExecuteコマンドにはBmiViewModelが持っている身長や体重の情報が必要ですし、最終的にはBmiViewModelのBmiプロパティへセットする必要があります。
クラスが違うと当然スコープも違いますから面倒ですね。

そこでラムダ式ですよ

CalcCommandのコンストラクタでExecute、CanExecuteに対応するラムダ式をセットするようにしましょう。

    public class CalcCommand : ICommand
    {
        private Action _exec;
        private Func<bool> _canExec;

        public CalcCommand(Action exec, Func<bool> canExec)
        {
            _exec = exec;
            _canExec = canExec;
        }

        public void Execute(object parameter)
        {
            _exec();
        }

        public bool CanExecute(object parameter)
        {
            return _canExec();
        }

よく見ると、処理内容自体は外部からコンストラクタ経由で設定されるので、CalcCommandという処理内容を表す名前は不適切ですね。
というわけで、名前をCommandに変えてしまいます。

Command.cs

最終的に、Commandクラスはこんな感じになりました。

using System;
using System.Windows.Input;

namespace MVVMSample
{
    public class Command : ICommand
    {
        private Action _exec;
        private Func<bool> _canExec;

        public Command(Action exec, Func<bool> canExec)
        {
            _exec = exec;
            _canExec = canExec;
        }

        public void Execute(object parameter)
        {
            _exec();
        }

        public bool CanExecute(object parameter)
        {
            return _canExec();
        }

        public event EventHandler CanExecuteChanged;
    }
}

BmiViewModelにCommand型のメンバを追加

次にBmiViewModelにCommand型のメンバ_calcCommandを追加します。
そして、そのメンバをCalcCommandプロパティを通して公開しますが、ここではgetされたときにインスタンスが生成されてなければ生成するようにしています。
CanExecuteについては今回は面倒なので常にtrueを返す(いつでもボタンが押せる)ようにしています。

BmiViewModel.cs
using System;
using System.ComponentModel;

namespace MVVMSample
{
    public class BmiViewModel : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;
        private Command _calcCommand;

        public Command CalcCommand
        {
            get
            {
                if (_calcCommand == null)
                {
                    _calcCommand = new Command(
                        () => { Bmi = Math.Truncate((Weight/Math.Pow((Height/100), 2))*100)/100; },
                        () => true);
                }
                return _calcCommand;
            }
        }

        private void RaisePropertyChanged(string propertyName)
        {
            if (PropertyChanged == null) return;
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public double Height { get; set; }

        public double Weight { get; set; }

        private double _bmi;
        public double Bmi
        {
            get { return _bmi; }

            set
            {
                _bmi = value;
                RaisePropertyChanged("Bmi");
            }
        }
    }
}

テストコードの修正

Command型のプロパティでBMIの計算コマンドを公開するようにしたので、それを使用するように変更します。

            viewModel.Calc();

            viewModel.CalcCommand.Execute(null);

に変更しただけですね。

BmiViewModelTest.cs
using System;
using System.ComponentModel;
using MVVMSample;
using NUnit.Framework;

namespace MVVMSampleTest
{
    [TestFixture]
    public class BmiViewModelTest
    {
        [Test]
        public void 正常()
        {
            var viewModel = new BmiViewModel();

            viewModel.Height = 172;
            viewModel.Weight = 66;

            viewModel.CalcCommand.Execute(null);

            Assert.That(viewModel.Bmi, Is.EqualTo(22.30));
        }

        [Test]
        public void プロパティの変更通知確認()
        {
            var viewModel = new BmiViewModel();
            String propertyName = "";

            viewModel.PropertyChanged += new PropertyChangedEventHandler((s, e) => { propertyName = e.PropertyName; });
            viewModel.Height = 172;
            viewModel.Weight = 66;

            Assert.That(propertyName, Is.EqualTo(""));

            viewModel.CalcCommand.Execute(null);

            Assert.That(propertyName, Is.EqualTo("Bmi"));
        }
    }
}

View(MainWindow)の変更

BmiViewModelはテストを実施して壊れてないことが確認できたので、ViewであるMainWindowの変更を行います。

MainWindow.xaml

ポイントは以下のCommandプロパティにDataContextのCalcCommandプロパティをバインドしているところになります。

<Button Content="計算" Grid.Row="3" Name="calcButton" Grid.ColumnSpan="2" Margin="50,10,50,10" Command="{Binding CalcCommand}" />
<Window x:Class="MVVMSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:viewModels="clr-namespace:MVVMSample" Title="MainWindow" Height="237" Width="318">

    <Window.DataContext>
        <viewModels:BmiViewModel />
    </Window.DataContext>

    <Grid>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="71*" />
            <ColumnDefinition Width="225*" />
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition Height="10*" />
            <RowDefinition Height="10*" />
            <RowDefinition Height="10*" />
            <RowDefinition Height="10*" />
        </Grid.RowDefinitions>
        <Label Content="身長" Name="label1" VerticalAlignment="Center" HorizontalAlignment="Center" />
        <Label Content="体重" Grid.Row="1" Name="label2" VerticalAlignment="Center" HorizontalAlignment="Center" />
        <Label Content="BMI" Name="label3" Grid.Row="2" VerticalAlignment="Center" HorizontalAlignment="Center" />
        <TextBox Name="heightText" Text="{Binding Path=Height}" Grid.Column="1" Margin="10" />
        <TextBox Name="weightText" Text="{Binding Path=Weight}" Grid.Row="1" Grid.Column="1" Margin="10" />
        <TextBlock Name="bmiText"  Text="{Binding Path=Bmi}" Grid.Row="2" Grid.Column="1" Margin="10" VerticalAlignment="Center" />
        <Button Content="計算" Grid.Row="3" Name="calcButton" Grid.ColumnSpan="2" Margin="50,10,50,10" Command="{Binding CalcCommand}" />
    </Grid>
</Window>
コードビハインドから不要な処理を削除

XAML側でCalcCommandプロパティをバインドしましたので、コードビハインドでの記述は不要になります。

using System.Windows;

namespace MVVMSample
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
        }
    }
}

まとめ

目標であったコードビハインドをきれいにすることができました。
次はエラー処理を組み込んでみます。

ソース

このエントリで作成したソースはこちら
View(MainWindow)の変更 · 7420464 · nakaji/MVVMSample · GitHub