がりらぼ

WindowsRuntimeの応援ブログ

MVVM入門してみた

なんとなくMVVMがわかってきた(ような気がする)のでMVVM入門のために足し算するだけのソフトをView-ViewModel-Modelに分割してみました。

こちらのかずきさんのサンプルコードを参考に作りました。

ソースコードはGitHubで公開しておりますので指摘がアレばガンガンお願いします。
SumCalc

View部分のxamlです。

<Window
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity" x:Class="MvvmPractice1.MainWindow"
        Title="SumCalc" Height="249" Width="442">
    <Grid>
        <TextBlock Height="23" Margin="12,43,315,143" Text="たしざんするよ" />
        <TextBlock Height="23" Margin="97,92,305,95" Text="+" />
        <TextBlock Height="23" Margin="202,92,192,95" Text="=" />

        <TextBox Height="35" Width="70"  Margin="12,89,338,86" Text="{Binding Sum1,Mode=TwoWay}"  />
        <TextBox Height="35" Width="70" Margin="126,89,224,86" Text="{Binding Sum2,Mode=TwoWay}" />
        <TextBox Height="35" Width="70" Margin="228,89,122,86" Text="{Binding Result,Mode=TwoWay}" />
        
        <Button Content="Calc!" Width="83" Height="37" Margin="302,150,0,0">
        	<i:Interaction.Triggers>
        		<i:EventTrigger EventName="Click">
        			<i:InvokeCommandAction Command="{Binding sumCommand}"/>
        		</i:EventTrigger>
        	</i:Interaction.Triggers>
        </Button>
    </Grid>
</Window>

足し算する2つの数字を入力するテキストボックスと結果表示するテキストボックスがあり、BindingでViewModelのプロパティに関連付けします。ModeをTwoWayにすることでテキストボックスの変更をViewModelのプロパティにも反映させます。
計算開始するボタンは、Commandプロパティにバインドさせても良かったのですが、今後のことも考えて、ビヘイビアのInvokeCommandActionでクリックとコマンドを関連付けます。

Viewのコードビハインドです。

namespace MvvmPractice1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            //データコンテキストにViewModelをセットする
            this.DataContext = new SampleViewModel();
        }
    }
}

DataContextにViewModelをセットするだけです。
コードビハインドに適切な処理を書くのがMVVMの本質らしいのですがまだわからないのですごく薄いです。
※人によっては極力薄くするみたいです。

ViewModelの親クラスです

namespace MvvmPractice1
{
    /// <summary>
    /// ViewModelの親クラス
    /// プロパティが変更されたことを通知するため、INotifyPropertyChangedインターフェースを実装する
    /// プロパティ変更時NotifyPropertyChangedを呼び出す
    /// </summary>
    class ViewModelBase:INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected void NotifyPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}

INotifyPropertyChangedインターフェースを実装するだけですが、すべてのViewModelで使用するので親クラスにまとめてしまいます。
NotifyPropertyChanged("プロパティ名");でViewModelプロパティの変更をViewに通知します。

ViewModel部分です

namespace MvvmPractice1
{
    /// <summary>
    /// ViewとModelを結合するViewModel
    /// </summary>
    class SampleViewModel:ViewModelBase
    {
        public SumModel sumModel;   //モデル
        public SampleCommand sumCommand { get; set; }   //コマンド
        private string sum1;
        private string sum2;
        private string result;

        public string Sum1
        {
            get { return sum1; }
            set
            {
                this.sum1 = value;
                NotifyPropertyChanged("Sum1");  //変更をViewに知らせる
            }
        }

        public string Sum2
        {
            get { return sum2; }
            set
            {
                this.sum2 = value;
                NotifyPropertyChanged("Sum2");  //変更をViewに知らせる
            }
        }

        public string Result
        {
            get { return result; }
            set
            {
                this.result= value;
                NotifyPropertyChanged("Result");    //変更をViewに知らせる
            }
        }

        /// <summary>
        /// コンストラクタ
        /// </summary>
        public SampleViewModel()
        {
            this.sumCommand = new SampleCommand(new EventDelegate(Sum));
            sumModel = new SumModel();
            this.sum1 = "";
            this.sum2 = "";
            this.result = "";
        }

        /// <summary>
        /// モデルへ渡す
        /// </summary>
        public void Sum()
        {
            this.Result=sumModel.Sum(int.Parse(Sum1),int.Parse(Sum2)).ToString();
        }
    }
}

ViewModelBaseを継承します。
ここにある各プロパティがBindingによってViewと結びつけられます。
プロパティの変更はNotifyPropertyChangedメソッドで通知します。
SumメソッドはModel内の足し算処理を呼び出します。

コマンド部分です。

namespace MvvmPractice1
{
    public delegate void EventDelegate();

    /// <summary>
    /// ICommandインターフェースを実装するコマンド
    /// </summary>
    class SampleCommand:ICommand
    {
        EventDelegate e_delegate;

        /// <summary>
        ///引数で実行したいメソッドを指定する 
        /// </summary>
        /// <param name="ed"></param>
        public SampleCommand(EventDelegate ed)
        {
            e_delegate = ed;
        }

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

        public event EventHandler CanExecuteChanged;

        /// <summary>
        /// コンストラクタで渡されたメソッドを実行する
        /// </summary>
        /// <param name="parameter"></param>
        public void Execute(object parameter)
        {
            e_delegate();
        }  
    }
}

ICommandインターフェースを実装し、delegateによってnewした時に実行したいメソッドを受け取ります。
そしてコマンドが実行(Execute)された時に渡されたメソッドを実行します。
MVVMLightのRelayCommandの代わりになるものを実装したつもりです。
実際にはCalcボタンが押された時にSumCommandを実行し、ViewModel内のSumメソッドが実行される仕組みです。

Model部分です

namespace MvvmPractice1
{
    /// <summary>
    /// 超シンプルなモデルっぽいもの
    /// </summary>
    class SumModel
    {
        public SumModel()
        {
        }

        public int Sum(int a, int b)
        {
            return a + b;
        }
    }
}

今回は単純な足し算だけなのですごく薄いです。
ViewModelから呼ばれ、足し算結果を返します。

イメージとしてはこんな感じ

間違ってる気しかしないですね。
Modelあたりが怪しすぎます。

本当ならMVVMLightなどの外部ライブラリを使って開発するのですが、僕はMVVMビギナーなのでとりあえずロジックの分離をしてみました。
ロジックをView-ViewModel-Modelの3つに分離することでUI部分とロジック部分がお互いに依存しなくなり、再利用性や拡張性が生まれます。
とりあえず今までみたいなコードビハインドにロジックをすべて書くというスパゲッティコードからは卒業できそうです。

ご指摘がアレば@garicchiかコメントでおねがいしますね。

※追伸
コードビハインドには適切な処理を入れる必要があるようです。(まだわかりませんが)
人によっては極限まで薄くするそうです。
Model部分は足し算なので薄くても仕方ないそうな。
イメージ図の②のBindingはあくまでView→ViewModelらしいので「値が変わったぞ!!」が良いらしいです。
(図の元データが吹っ飛んだので修正不可能)