WPF/MVVM/C#/Prism5.0 ViewModelを少し便利に-BindableBaseとDelegateCommand-
WPF/MVVM/C#/Prism5.0 ViewModelを少し便利に-BindableBaseとDelegateCommand- - Qiita
*Qiitaにも投稿しています。
WPFでMVVMは難しい
残念なことに、WPFでMVVMパターンを適用する際には、.NET標準だけ使うとなると、綺麗でわかりやすく保守が容易なコードが書けません。 書けないような基盤しかないのです。 なので、PrismなどのMVVM基盤ライブラリが必要となります。 https://msdn.microsoft.com/ja-jp/library/gg406140.aspx
他にも様々なライブラリが公開されていますが、MS謹製ということで今回はPrismを利用しようと思います。 Prismを利用することで得られるメリットを公開します。 以下の予定です。
- BindableBase/DelegateCommand ~ViewModelの基盤~
- ErrorsContaier ~便利なエラー通知~
- ViewModelLocationProvider ~ViewとViewModelを自動で関連付け~
- Regionってなんなのさ ~Viewの配置をお手軽に~
- IModuleとUnity ~UIでDI~
- DIPパターンの恩恵 ~MSBuildで並列ビルド~
本稿は上記1の記事になります。
※Visual Studio 2013 Community Editionで実験しています。
Prismのインストール
Nuget Package Manager Consoleにて、以下のように打ちます。
Install-Package Prism
そうすると、以下のようにモジュール参照が追加になります。
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やフォルダが!!
Prismのサイズは、多言語Resource含めて約1MBです。 これを多いと思うか否かは環境次第ですが、訳もわからずDLLが増えるのは嫌ですよね。
使ってみる
UIはこんな感じを想定しています。ボタンを押したら下に答えが出るようなアプリです。 (かずきさんのアプリを参考しています。) https://code.msdn.microsoft.com/MVVM-Light-toolkitMessenger-0ec2e5c4
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も試してみます。