はじめに
恥ずかしながら未だにWinForms案件ばかりでWPF、ましてやMVVMなんて使ったことがない時代遅れなプログラマーです。恥ずかしい・・・
そんな私の元にも遂にWPF案件が舞い降りてきてしまった。
ここ数日「WPFでどうやって作るねん」から調べ始めて、今さらMVVMという存在を知り、「MVVMってなんやねん」をずっと調べているけど・・・
なんだかよく分からない!
「View-ViewModel-Modelに分けて作りましょう」
「テストしやすくなるよ」
とかざっくりとは分かる。
でも具体的なその分け方が調べていても人によって微妙に違って何が正解か分からない。1
そもそも正解なんて無いのかもしれない。
で、考え込みすぎて訳が分からなくなってきたので、自分なりの考えを整理するためにも、MVVMについて書いてみることに。
WinForms脳な人間がMVVMについて数日調べただけの内容なので間違えてる部分が多々あるかもしれません。その時は優しく教えてくれると嬉しいです。優しく・・・
なんでMVVMに分けるのだろう?
V-VM-Mの分け方について調べているだけだと明確な物が見えてこないので、視点を変えて
「なんでMVVMに分けるのだろう?」
という目的から考えてみることにした。
目的
テスタビリティ(テスト容易性)向上
画面とコードがくっついてるとユニットテストがし辛い。
そこで画面(View)とコード(ViewModel/Model)に分離してやるとやりやすくなる。
なるほど、たしかに。
プロダクティヴィティ(生産性)向上
画面(View)とコード(ViewModel/Model)が分離してると、デザイナは画面(View)、プログラマはコード(ViewModel/Model)と明確に分業できる。
もっともウチにはデザイナなんていない。
ポータビリティ(移植性)向上(?)
画面(View)とコード(ViewModel/Model)が分離してると、画面(View)を用意するだけで他のプラットフォームに対応できる・・・かもしれない。
と言うのも、これについて触れられている情報を殆ど見かけなくて、あくまで勝手なイメージ。
ただ理論上は可能なはず・・・2
分け方が曖昧な原因はここにある?
恐らく大まかにこの3つが目的で、特にテスタビリティ向上が重要っぽい。
あれ?これらの目的って、とりあえず画面さえ分離しちゃえば一応は満たせる?
そのせいでV-VM-Mの分け方が曖昧なんだろうか。
理想的なプロジェクト構成を考えてみる
↑に挙げた目的を元に、まずはどんなプロジェクト構成が理想的かを大まかに考えてみる。
こんな感じ?
- MvvmTest (.Net Core/WPF)
- Views
- MainView.xaml
- MainView.xaml.cs
- App.xaml
- App.xaml.cs
- AssemblyInfo.cs
- MvvmTest.MVM (.Net Standard)
- MainViewModel.cs
- MainModel.cs
あくまで最小の基本構成。DIコンテナとか画面遷移とか考え出すと多分これじゃあ足りない。
MvvmTest プロジェクト
Viewとアプリケーションを定義・実装するプロジェクト。
ViewをMvvmTest.Viewsプロジェクトに分けるのも有りかも。意味があるかは謎。
あとこれをMvvmTest.NetCoreにして、XamarinでMvvmTest.iOSとか作れると幸せ。
MvvmTest.MVM プロジェクト
ViewModel と Model を定義・実装するプロジェクト。.Net Standardにしたい!!3
MVM・・・もう少し良い名前はないものか。MvvmTest.ViewModelsとMvvmTest.Modelsに分けるのも有りかも。意味があるかは謎。
ViewModel と Model は同じ階層で近い位置にいた方が便利のような?
MVVMそれぞれの役割を考えてみる
次にView、ViewModel、Modelそれぞれの役割を大まかに考えてみる。
View
画面。それ以上でもそれ以下でもない。ViewModelが提供するプロパティに沿って画面を定義する。
デザインに関すること(XAML)以外は書きたくない。
Model
ロジック。やるべき処理をやる人。ViewModelからパラメータを貰って処理を行う。
どんなデザインの画面かは知らない。4
ViewModel
画面とロジックを結ぶ橋渡し役。
画面からどんな情報が欲しいか定義して、ロジックの求める形で情報を渡して、ロジックから結果を貰って、結果を画面に渡してあげる。
大体どんな画面か知っていて5、画面寄りの処理(入力チェックとか)はこの人がやるべき?(不明瞭)
それぞれ持つべきものを考えてみる
↑の役割をふまえて、View、ViewModel、Modelがそれぞれ持つべきもの(やるべきこと)を大まかに考えてみる。
View
- 画面デザイン
- 対応する
ViewModelのインスタンス(ViewはViewModelに依存、DataContext) - コントロールが
ViewModelのどのプロパティと連動するか(Binding)
ViewModel
- 対応する
Modelのインスタンス(ViewModelはModelに依存) - 画面の入力データを受け取る6
- 入力データが
Modelに渡せるかどうかの入力チェック7 - エラーを
Viewに通知(INotifyDataErrorInfo) - ボタンなどのイベント処理(ICommand)
- 入力データを
Modelの求める形に変換して渡す Modelのプロパティ変更通知を受け取る- プロパティ変更を
Viewに通知(INotifyPropertyChanged)
Model
ViewModelからパラメータを受け取る- パラメータのエラーチェック
- エラーを
ViewModelに通知(INotifyDataErrorInfo) - 実際にやりたい処理
- 結果をプロパティに格納
- プロパティ変更を
Viewに通知(INotifyPropertyChanged)
実際に書いてみる
今まで考えたことをふまえて、実際にコードを書いてみる。
数値を2つを渡して実行すると足し算した結果が返ってくる超単純なアプリをMVVMで作ってみた。
Model(MainModel.cs)
まずは画面は気にせずやるべき処理を書いてみる。
classMainModel:ViewModelBase,IMainModel{privateProperty<int>ans=newProperty<int>();publicintParamA{get;set;}publicintParamB{get;set;}publicintAnswer{get=>this.ans;set=>this.ans.SetValue(value,this);}publicvoidSum()=>this.Answer=this.ParamA+this.ParamB;}2つの数字を受け取って足し算するだけ。ややこしいのでパラメータエラーはなし。
因みにViewModelBaseクラスにプロパティ変更通知が実装されていて、Property<T>のSetValueで良い感じに通知してる。IMainModelは、MainModelの定義そのまま。
ViewModel(MainViewModel.cs)
次に画面を想像しつつModelと繋ぐViewModelを書いてみる。
数字を入力するテキストボックス2個(ParamA,ParamB)と、実行ボタン1個(SumCommand)と、結果を表示するラベルが1個(Answer)かな。
publicclassMainViewModel:ViewModelBase{privateIMainModelModel{get;}privateProperty<string>paramA=newProperty<string>("0",(value)=>{if(value.Length==0)return"未入力エラー";elseif(!int.TryParse(value,outint_))return"フォーマットエラー";elsereturnnull;});privateProperty<string>paramB=newProperty<string>("0",(value)=>{if(value.Length==0)return"未入力エラー";elseif(!int.TryParse(value,outint_))return"フォーマットエラー";elsereturnnull;});privateProperty<int>ans=newProperty<int>();publicstringParamA{get=>this.paramA;set{// 入力エラーがなければModelに設定if(this.paramA.SetValue(value,this))this.Model.ParamA=int.Parse(this.ParamA);}}publicstringParamB{get=>this.paramB;set{// 入力エラーがなければModelに設定if(this.paramB.SetValue(value,this))this.Model.ParamB=int.Parse(this.ParamB);}}publicintAnswer{get=>this.ans;set=>this.ans.SetValue(value,this);}publicICommandSumCommand{get;}publicMainViewModel(){// TODO: 本来ならDIコンテナから取得this.Model=newMainModel();this.Model.PropertyChanged+=Model_PropertyChanged;// Sumボタンthis.SumCommand=newCommand(()=>{// 実行this.Model.Sum();},paramA,paramB);}privatevoidModel_PropertyChanged(objectsender,PropertyChangedEventArgse){if(e.PropertyName==nameof(this.Model.Answer))this.Answer=this.Model.Answer;}}Viewからデータを受け取って、Modelに渡せるデータかチェックして、SumCommandの有効・無効を制御。
SumCommandの処理でModelにパラメータを渡して実行。Modelのプロパティ変更通知で結果を取り出してViewModelに通知。
独自のクラスが多くて分かりにくい・・・
ひとまずProperty<T>には検証機能もあって、Commandクラスには指定したProperty<T>のエラー状態と連動して有効・無効が勝手に切り替わる・・・くらいのイメージで・・・8
View(MainView.xaml)
最後に画面。ViewModelに従って数字を入力するテキストボックス2個と、実行ボタン1個と、結果を表示するラベルが1個。
<Windowx:Class="MvvmTest.Views.MainView"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:MvvmTest.Views"xmlns:vm="clr-namespace:MvvmTest.VM;assembly=MvvmTest.VM"mc:Ignorable="d"Title="Main"SizeToContent="WidthAndHeight"><Window.DataContext><vm:MainViewModel/></Window.DataContext><GridMinWidth="300"MinHeight="200"><Grid.ColumnDefinitions><ColumnDefinitionWidth="100"/><ColumnDefinition/></Grid.ColumnDefinitions><Grid.RowDefinitions><RowDefinition/><RowDefinition/><RowDefinition/><RowDefinition/></Grid.RowDefinitions><!-- ParamA --><TextBlockGrid.Row="0"Grid.Column="0"Margin="0,0,10,0"HorizontalAlignment="Right"VerticalAlignment="Center"Text="ParamA"/><TextBoxGrid.Row="0"Grid.Column="1"MaxHeight="24"Margin="10, 0, 10, 0"VerticalContentAlignment="Center"Text="{Binding ParamA, UpdateSourceTrigger=PropertyChanged}"/><!-- ParamB --><TextBlockGrid.Row="1"Grid.Column="0"Margin="0,0,10,0"HorizontalAlignment="Right"VerticalAlignment="Center"Text="ParamB"/><TextBoxGrid.Row="1"Grid.Column="1"MaxHeight="24"Margin="10, 0, 10, 0"VerticalContentAlignment="Center"Text="{Binding ParamB, UpdateSourceTrigger=PropertyChanged}"/><!-- Button --><ButtonGrid.Row="2"Grid.Column="0"Grid.ColumnSpan="2"Margin="10,10,10,10"Content="Sum"Command="{Binding SumCommand}"/><!-- Answer --><TextBlockGrid.Row="3"Grid.Column="0"Margin="0,0,10,0"HorizontalAlignment="Right"VerticalAlignment="Center"Text="Answer"/><TextBlockGrid.Row="3"Grid.Column="1"HorizontalAlignment="Left"Margin="10, 0, 10, 0"VerticalAlignment="Center"Text="{Binding Ans}"/></Grid></Window>画面のデザインと、ViewModelが誰かと、コントロール毎のバインディングを定義するだけ。
コードビハインド(MainView.xaml.cs)
/// <summary>/// MainView.xaml の相互作用ロジック/// </summary>publicpartialclassMainView:Window{publicMainView(){InitializeComponent();}}生成されたコードそのまま。絶対に触らない(鉄の意志)
ソースコードの全てはGitHubに上げてみた
mitsu-at3/MvvmTest: Mvvm Test Project
仕事でGitLabは使っているけど、GitHubは初めて使った・・・(今さら)
合ってるだろうか?
色々と調べたりMVVMの目的を考えると、この分け方が1番しっくり来るけど、合ってるのか分からない・・・
特にViewModelに定義するViewからデータを受け取るためのプロパティの型を、例えば数値入力でもテキストボックスならstring型みたいに、Viewのコントロールの都合を考慮した型にするのが正しいのかどうかが謎。
調べても明確にそういう考え方してる情報に出会えなかった。
ただViewModelのプロパティをintにしてしまうと、Viewの段階で入力エラーが出てしまって、そのままだとViewModel側でエラーを検知できないから、ViewからViewModelに伝える仕組みが必要になってしまう。
でもViewにはなるべく余計なものは書きたくない。
って考えると、ViewModel側はとりあえずstringで受け入れて、入力チェックはViewModel側でやるのがベター?9
調べてる中で気になったこととか
最後に調べてるときに見かけた情報で「それ合ってるの?」と気になったことなど。
あちこちの情報をひたすら漁ってる中で見かけた情報なのでどこで見たのかは覚えてない・・・10
Modelがただのデータクラス
publicclassPersonModel{publicstringName{get;set;}publicintAge{get;set;}}Modelの意味を勘違いしてるのか、ロジックはこれから実装するつもりだったのか・・・
「ViewModelとModelは同じプロパティが並びます」
必ずしも同じ必要は無いのではなかろうか?
同じだとViewModelが架け橋する意味も薄くなっちゃう。ViewModelとModelの関係が崩れなければ、違っていても良い気がする。
ViewModelが完全にModelへの受け渡しだけ
↑と似ているけど・・・
publicclassPersonModelView{publicstringName{get=>this.Model.Name;set=>this.Model.Name=value;}publicintAge{get=>this.Model.Age;set=>this.Model.Age=value;}// ・・・Modelから通知を受けたり、コマンドを受けてModelを実行したりだけ}入力チェックも何もない画面なら有りなのかもしれないけど、ここまで来るとVMとMを分ける意味がないような・・・
超小規模なプロジェクトならModelなしも有り?
あなたのモデルはどこから?
publicMainViewModel(IMainModelmodel){this.Model=model;}なんやかんやの仕組みがあって自動的に依存性注入で渡ってくるならこれも良いと思う。
でもこれは嫌だ(MainView.xaml.cs)
/// <summary>/// MainView.xaml の相互作用ロジック/// </summary>publicpartialclassMainView:Window{publicMainView(){IMainModelmodel=DIコンテナ的なやつ.GetService(typeof(IMainModel));this.DataContext=newMainViewModel(model);InitializeComponent();}}コードビハインドには書きたくない!!!
ViewModelのコンストラクタで作れば良くない?
publicMainViewModel(){this.Model=DIコンテナ的なやつ.GetService(typeof(IMainModel));}ViewModelがModelに依存するのは間違いじゃないので、素直にこっちの方が良いような。
Modelがシングルトン
これが正しいのかが1番分からない。
今までの考えからすると、View-ViewModel-Modelは1つのセットと見なして、生存期間は同じにしておく方が自然のような気がする。
シングルトンで扱いたい機能は、Modelよりも下に定義して、Modelの中でそのシングルトンクラスを使った方がMVVMの関係性が明確で分かりやすくない?
でも厳密には「ViewとViewModel以外全てがModel」らしいから11、シングルトンでも間違いではないのかな・・・
個人的には少なくともViewModelが参照するModelは、生存期間を同じにしておきたいなあ。
おわりに
長々と書いていて「こう考えるのが正しいのではないか?」というのはところまでは来たけども、まだ確信を持って「この考えで正しい!」とは言えない・・・
しっくりは来てるんだけどなあ。
でも序盤に挙げた「目的」を明確に認識しておくだけでも「その形が間違ってるか?」の判断材料にはなりそう。
少しは最新の開発スタイルに近付けただろうか・・・?
あと画面遷移とかダイアログ表示とかDIコンテナとか調べだすと、Prismとか外部フレームワークに頼りたくなる理由が分かってきた・・・
何となく自力でも実装できそうな雰囲気だけど、かなり面倒そうな印象。
おまけ:DIコンテナって・・・
MVVMについて調べる中で初めて「DIコンテナ」を知ったのだけど(今さら)、解説を読んだときに真っ先にあいつを思い出した・・・
HRESULTCoCreateInstance(REFCLSIDrclsid,LPUNKNOWNpUnkOuter,DWORDdwClsContext,REFIIDriid,LPVOID*ppv);取得したいInterfaceの情報を渡すとインスタンスが返ってくるってこれじゃん。
そう思うと急に親しみが湧いてきたのでした。おしまい。
理解力が足りないだけかもしれない。 ↩
Xamarinを使えば出来たりしないかな?(Xamarin未経験) ↩
.Net Standard大好き ↩
何をする画面かは知ってる。じゃないと何の処理をすれば良いのか分からない。あくまでデザインを知らないだけ。 ↩
このパラメータはテキストボックス的なやつで、このパラメータはチェックボックス的なやつで・・・みたいな。 ↩
Viewがバインディングするところでエラーが出ないようにするべき?(数値入力前提のテキストボックスでも、とりあえずstring型のプロパティで受け取るとか) ↩Modelの処理に依存するエラーは知らない。あくまで「渡せるかどうか」だけ。 ↩INotifyPropertyChangedやINotifyDataErrorInfoはどういう実装が便利だろう?とお試しで作ったクラス。実際にこれで良いかは分からない。(全容はGitHubで・・・) ↩stringにしておけばどんな入力でも受け入れられるテキストボックスだからってだけで、どんな型にしてもエラーが発生しうるコントロールの場合は、
View→ViewModelの通知は避けられない? ↩ただその情報のせいで余計に混乱した ↩
混乱の素 ↩