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

Training for D-Day

ブログの内容は個人の見解であり、所属する企業を代表するものではありません。

WPF/MVVM/C#/Prism5.0 ViewModelを少し便利に-BindableBaseとDelegateCommand-

WPF/MVVM/C#/Prism5.0 ViewModelを少し便利に-BindableBaseとDelegateCommand- - Qiita

*Qiitaにも投稿しています。

WPFでMVVMは難しい

残念なことに、WPFMVVMパターンを適用する際には、.NET標準だけ使うとなると、綺麗でわかりやすく保守が容易なコードが書けません。 書けないような基盤しかないのです。 なので、PrismなどのMVVM基盤ライブラリが必要となります。 https://msdn.microsoft.com/ja-jp/library/gg406140.aspx

他にも様々なライブラリが公開されていますが、MS謹製ということで今回はPrismを利用しようと思います。 Prismを利用することで得られるメリットを公開します。 以下の予定です。

  1. BindableBase/DelegateCommand ~ViewModelの基盤~
  2. ErrorsContaier ~便利なエラー通知~
  3. ViewModelLocationProvider ~ViewとViewModelを自動で関連付け~
  4. Regionってなんなのさ ~Viewの配置をお手軽に~
  5. IModuleとUnity ~UIでDI~
  6. DIPパターンの恩恵 ~MSBuildで並列ビルド~

本稿は上記1の記事になります。

Visual Studio 2013 Community Editionで実験しています。

Prismのインストール

Nuget Package Manager Consoleにて、以下のように打ちます。

Install-Package Prism

そうすると、以下のようにモジュール参照が追加になります。

Image.png

package.configは以下のようになります。

<?xml version=" 1.0" encoding=" utf-8"?>
<packages>
  <package id=" CommonServiceLocator" version=" 1.2" targetFramework=" net45" />
  <package id=" Prism" version=" 5.0.0" targetFramework=" net45" />
  <package id=" Prism.Composition" version=" 5.0.0" targetFramework=" net45" />
  <package id=" Prism.Interactivity" version=" 5.0.0" targetFramework=" net45" />
  <package id=" Prism.Mvvm" version=" 1.0.0" targetFramework=" net45" />
  <package id=" Prism.PubSubEvents" version=" 1.0.0" targetFramework=" net45" />
</packages>

たくさんあって混乱しそうですが、とりあえず今はこのまま! ビルドすると、出力パスにはこんなにたくさんのDLLやフォルダが!! Image.png

Prismのサイズは、多言語Resource含めて約1MBです。 これを多いと思うか否かは環境次第ですが、訳もわからずDLLが増えるのは嫌ですよね。

使ってみる

UIはこんな感じを想定しています。ボタンを押したら下に答えが出るようなアプリです。 (かずきさんのアプリを参考しています。) https://code.msdn.microsoft.com/MVVM-Light-toolkitMessenger-0ec2e5c4

Image.png

XAMLはこんな感じです。

<Window x :Class="KStore.Calc._1.CalcView"
        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:mc ="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local ="clr-namespace:KStore.Calc._1"
        mc:Ignorable ="d"
        Title="Calc" Height ="350" Width="525">
    <Grid >
        <Grid.RowDefinitions>
            <RowDefinition Height ="120*"/>
            <RowDefinition Height ="200*"/>
        </Grid.RowDefinitions>
        <Grid Name ="InputRegion" Grid.Row="0">
            <Grid.RowDefinitions>
                <RowDefinition Height ="30*"/>
                <RowDefinition Height ="30*"/>
                <RowDefinition Height ="30*"/>
                <RowDefinition Height ="30*"/>
            </Grid.RowDefinitions>
            <TextBox Grid.Row ="0" Text="{ Binding LeftValue}" Name="LeftValue" VerticalContentAlignment="Center" TextAlignment="Center" />
            <TextBlock Grid.Row ="1"  Text ="+" HorizontalAlignment="Center" VerticalAlignment="Center"/>
            <TextBox Grid.Row ="2" Text="{ Binding RightValue}" Name="RightValue" VerticalContentAlignment="Center" TextAlignment="Center" />
            <Button Grid.Row ="3" Name="CalcButton" Content="=" Command ="{Binding CalcCommand , Mode=OneWay}" />
        </Grid>
        <Grid Name ="OutputRegion" Grid.Row="1">
            <TextBlock Name ="Answer" Text="{ Binding AnswerValue}" HorizontalAlignment="Center" VerticalAlignment="Center" FontSize="48pt"/>
        </Grid>
    </Grid >
</Window>

ViewModelとViewの紐付けは、今は下記のようになります。今回はベタでいきます。わかりやすいですけどね。

using System.Windows;

namespace KStore.Calc._1
{   
    public partial class CalcView : Window
    {
        public CalcView()
        {
            InitializeComponent();
            this.DataContext = new CalcViewModel(); // これが紐付けって行。
        }
    }
}

ViewModelは以下のようになります。ここでBindableBaseとDelagateCommandを使用しています。

using KStore.Calc._1.Model;
using Microsoft.Practices.Prism.Commands;
using Microsoft.Practices.Prism.Mvvm;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Input;

namespace KStore.Calc._1
{
    public class CalcViewModel : BindableBase
    {
        private string _leftValue;
        public string LeftValue
        {
            get { return _leftValue; }
            set { this .SetProperty(ref this._leftValue, value); }
        }
       
        private string _rightValue;
        public string RightValue
        {
            get { return _rightValue; }
            set { this .SetProperty(ref this._rightValue, value); }
        }

        private string _answerValue;
        public string AnswerValue
        {
            get { return _answerValue; }
            set { this .SetProperty(ref this._answerValue, value); }
        }

        private ICommand calcCommand;

        public ICommand CalcCommand
        {
            get { return this.calcCommand ?? ( this.calcCommand = new DelegateCommand(CalcExecute, CanCalcExecute)); }
        }

        private bool CanCalcExecute()
        {
            return true ;
        }

        private void CalcExecute()
        {        
            AnswerValue = IntToString( Calculation.Sum(StringToInt(LeftValue), StringToInt(RightValue)));
        }

        private int StringToInt(string src)
        {
            int ret = 0;
            if( int .TryParse(src, out ret) )
            {
                return ret;
            }
            throw new ArgumentException( "src" + src);
        }

        private string IntToString(int src)
        {
            return src.ToString();
        }

    }

}

考え方は単純で、

INotifyPropertyChanged

使いたくないんですよね。 使っちゃうと、文字列でプロパティを表現しないといけません。

下記のようなメソッドを用意してあげて、

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

public string RightValue
{
     get { return _rightValue; }
     set
     { 
         this .(ref this._rightValue, value); 
         NotifyPropertyChanged("RightValue"); // <-- これがダルい。
     }
}

プロパティのセッターで、文字列指定が必要になります。。 プロパティ名が変わってもVisual Studioのリファクタ機能で追従してくれない。 実行時にバグがわかると。 これではC#のような静的言語の旨味も半減です。 コンパイルを活用したいんですよね。せっかくコンパイルするんだから。

これを回避してくれるのが、BindableBase。 https://msdn.microsoft.com/en-us/library/microsoft.practices.prism.mvvm.bindablebase%28v=pandp.50%29.aspx 説明少なっ!

BindableBaseを継承すると、上記サンプルのように、

set { this .SetProperty(ref this._leftValue, value); }

これで良いということになります。 これだけでもPrism使う価値がある!と思う。 ちなみに昔は

NotificationObject

っていう名前だったんですよね。 BindableBaseのほうがわかりやすくて良い。

つづいて、DelegateCommandです。 https://msdn.microsoft.com/en-us/library/microsoft.practices.prism.commands.delegatecommand%28v=pandp.50%29.aspx これは、もしかしたら不要かも。

このサンプルレベルだと恩恵を受けますが、ある程度の規模の開発になって、ViewやViewModelが超複雑になる場合、 Commandの処理とViewModelを切り離したくなる時がきます。 DelegateCommandだと、ViewModelに直接delegateメソッドを定義しないとなりません。 ViewやViewModelは、UserControlで切り出して小分けで作るのは、キツイ場合があります。 全部一緒だと何かと都合が良い。UIってそうなんですよね。Class分割しすぎると生産性落とします。 だけどViewModelにロジックを書きたくない。 ただのViewのデータ部分を反映する箱にしておきたい。そんな場合に対応できないです。

そうゆう場合は、ICommandを直接実装してもいいんじゃないかなと考えます。

public class CalcCommand_SeparatedViewModel : ICommand
{
    CalcViewModel _parentViewModel;

    public CalcCommand_SeparatedViewModel(CalcViewModel parentViewModel)
    {
        _parentViewModel = parentViewModel;
    }

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

    public event EventHandler CanExecuteChanged;

    public void Execute(object parameter)
    {
        _parentViewModel.AnswerValue = Util.IntToString(Calculation .Sum(Util.StringToInt(_parentViewModel.LeftValue), Util.StringToInt(_parentViewModel.RightValue)));
    }
}

ViewModelでの定義は以下のようになります。

private ICommand _calcCommand_SeparetedViewModel;

public ICommand CalcCommand_SeparetedViewModel
{
    get { return this._calcCommand_SeparetedViewModel ?? ( this._calcCommand_SeparetedViewModel = new CalcCommand_SeparatedViewModel (this)); }
}

まとめ

という訳で、Prism5.0使うなら BindableBaseは活用しよう! DelegateCommandは用途に応じて活用しよう! ということでした。

続いて、ErrorsContainerも試してみます。