14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[C#/WPF/MVVM]今さらMVVMについて調べた

Last updated at Posted at 2020-09-12

はじめに

恥ずかしながら未だに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.ViewModelsMvvmTest.Modelsに分けるのも有りかも。意味があるかは謎。
ViewModel と Model は同じ階層で近い位置にいた方が便利のような?

MVVMそれぞれの役割を考えてみる

次にViewViewModelModelそれぞれの役割を大まかに考えてみる。

View

画面。それ以上でもそれ以下でもない。
ViewModelが提供するプロパティに沿って画面を定義する。
デザインに関すること(XAML)以外は書きたくない。

Model

ロジック。やるべき処理をやる人。
ViewModelからパラメータを貰って処理を行う。
どんなデザインの画面かは知らない。4

ViewModel

画面とロジックを結ぶ橋渡し役。
画面からどんな情報が欲しいか定義して、ロジックの求める形で情報を渡して、ロジックから結果を貰って、結果を画面に渡してあげる。
大体どんな画面か知っていて5、画面寄りの処理(入力チェックとか)はこの人がやるべき?(不明瞭)

それぞれ持つべきものを考えてみる

↑の役割をふまえて、ViewViewModelModelがそれぞれ持つべきもの(やるべきこと)を大まかに考えてみる。

View

  • 画面デザイン
  • 対応するViewModelのインスタンス(ViewViewModelに依存、DataContext)
  • コントロールがViewModelのどのプロパティと連動するか(Binding)

ViewModel

  • 対応するModelのインスタンス(ViewModelModelに依存)
  • 画面の入力データを受け取る6
  • 入力データがModelに渡せるかどうかの入力チェック7
  • エラーをViewに通知(INotifyDataErrorInfo)
  • ボタンなどのイベント処理(ICommand)
  • 入力データをModelの求める形に変換して渡す
  • Modelのプロパティ変更通知を受け取る
  • プロパティ変更をViewに通知(INotifyPropertyChanged)

Model

  • ViewModelからパラメータを受け取る
  • パラメータのエラーチェック
  • エラーをViewModelに通知(INotifyDataErrorInfo)
  • 実際にやりたい処理
  • 結果をプロパティに格納
  • プロパティ変更をViewに通知(INotifyPropertyChanged)

実際に書いてみる

今まで考えたことをふまえて、実際にコードを書いてみる。
数値を2つを渡して実行すると足し算した結果が返ってくる超単純なアプリをMVVMで作ってみた。

Model(MainModel.cs)

まずは画面は気にせずやるべき処理を書いてみる。

class MainModel : ViewModelBase, IMainModel
{
	private Property<int> ans = new Property<int>();

	public int ParamA { get; set; }
	public int ParamB { get; set; }

	public int Answer
	{
		get => this.ans;
		set => this.ans.SetValue(value, this);
	}

	public void Sum()
		=> 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)かな。

public class MainViewModel : ViewModelBase
{
	private IMainModel Model { get; }

	private Property<string> paramA = new Property<string>("0",
		(value) =>
		{
			if (value.Length == 0)
				return "未入力エラー";
			else if (!int.TryParse(value, out int _))
				return "フォーマットエラー";
			else
				return null;
		});

	private Property<string> paramB = new Property<string>("0",
		(value) =>
		{
			if (value.Length == 0)
				return "未入力エラー";
			else if (!int.TryParse(value, out int _))
				return "フォーマットエラー";
			else
				return null;
		});

	private Property<int> ans = new Property<int>();

	public string ParamA
	{
		get => this.paramA;
		set
		{
			// 入力エラーがなければModelに設定
			if (this.paramA.SetValue(value, this))
				this.Model.ParamA = int.Parse(this.ParamA);
		}
	}

	public string ParamB
	{
		get => this.paramB;
		set
		{
			// 入力エラーがなければModelに設定
			if (this.paramB.SetValue(value, this))
				this.Model.ParamB = int.Parse(this.ParamB);
		}
	}

	public int Answer
	{
		get => this.ans;
		set => this.ans.SetValue(value, this);
	}

	public ICommand SumCommand { get; }

	public MainViewModel()
	{
		// TODO: 本来ならDIコンテナから取得
		this.Model = new MainModel();
		this.Model.PropertyChanged += Model_PropertyChanged;

		// Sumボタン
		this.SumCommand = new Command(() =>
		{
			// 実行
			this.Model.Sum();
		}, paramA, paramB);
	}

	private void Model_PropertyChanged(object sender, PropertyChangedEventArgs e)
	{
		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個。

<Window x: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>
	
	<Grid MinWidth="300" MinHeight="200">
		<Grid.ColumnDefinitions>
			<ColumnDefinition Width="100" />
			<ColumnDefinition/>
		</Grid.ColumnDefinitions>
		<Grid.RowDefinitions>
			<RowDefinition/>
			<RowDefinition/>
			<RowDefinition/>
			<RowDefinition/>
		</Grid.RowDefinitions>

		<!-- ParamA -->
		<TextBlock Grid.Row="0" Grid.Column="0" Margin="0,0,10,0"  HorizontalAlignment="Right" VerticalAlignment="Center" Text="ParamA"/>
		<TextBox Grid.Row="0" Grid.Column="1" MaxHeight="24" Margin="10, 0, 10, 0" VerticalContentAlignment="Center" Text="{Binding ParamA, UpdateSourceTrigger=PropertyChanged}" />
		<!-- ParamB -->
		<TextBlock Grid.Row="1" Grid.Column="0" Margin="0,0,10,0" HorizontalAlignment="Right" VerticalAlignment="Center" Text="ParamB"/>
		<TextBox Grid.Row="1" Grid.Column="1" MaxHeight="24" Margin="10, 0, 10, 0" VerticalContentAlignment="Center" Text="{Binding ParamB, UpdateSourceTrigger=PropertyChanged}" />
		<!-- Button -->
		<Button Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Margin="10,10,10,10" Content="Sum" Command="{Binding SumCommand}"/>
		<!-- Answer  -->
		<TextBlock Grid.Row="3" Grid.Column="0" Margin="0,0,10,0" HorizontalAlignment="Right" VerticalAlignment="Center" Text="Answer"/>
		<TextBlock Grid.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>
public partial class MainView : Window
{
	public MainView()
	{
		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がただのデータクラス

public class PersonModel
{
	public string Name { get; set; }
	public int Age { get; set; }
}

Modelの意味を勘違いしてるのか、ロジックはこれから実装するつもりだったのか・・・

「ViewModelとModelは同じプロパティが並びます」

必ずしも同じ必要は無いのではなかろうか?
同じだとViewModelが架け橋する意味も薄くなっちゃう。
ViewModelModelの関係が崩れなければ、違っていても良い気がする。

ViewModelが完全にModelへの受け渡しだけ

↑と似ているけど・・・

public class PersonModelView
{
	public string Name
	{
		get => this.Model.Name;
		set => this.Model.Name = value;
	}
	public int Age
	{
		get => this.Model.Age;
		set => this.Model.Age= value;
	}

	// ・・・Modelから通知を受けたり、コマンドを受けてModelを実行したりだけ
}

入力チェックも何もない画面なら有りなのかもしれないけど、ここまで来るとVMとMを分ける意味がないような・・・
超小規模なプロジェクトならModelなしも有り?

あなたのモデルはどこから?

	public MainViewModel(IMainModel model)
	{
		this.Model = model;
	}

なんやかんやの仕組みがあって自動的に依存性注入で渡ってくるならこれも良いと思う。

でもこれは嫌だ(MainView.xaml.cs)

/// <summary>
/// MainView.xaml の相互作用ロジック
/// </summary>
public partial class MainView : Window
{
	public MainView()
	{
		IMainModel model = DIコンテナ的なやつ.GetService(typeof(IMainModel));
		this.DataContext = new MainViewModel(model);
		InitializeComponent();
	}
}

コードビハインドには書きたくない!!!

ViewModelのコンストラクタで作れば良くない?

	public MainViewModel()
	{
		this.Model = DIコンテナ的なやつ.GetService(typeof(IMainModel));
	}

ViewModelModelに依存するのは間違いじゃないので、素直にこっちの方が良いような。

Modelがシングルトン

これが正しいのかが1番分からない。
今までの考えからすると、View-ViewModel-Modelは1つのセットと見なして、生存期間は同じにしておく方が自然のような気がする。
シングルトンで扱いたい機能は、Modelよりも下に定義して、Modelの中でそのシングルトンクラスを使った方がMVVMの関係性が明確で分かりやすくない?

でも厳密には「ViewViewModel以外全てがModel」らしいから11、シングルトンでも間違いではないのかな・・・
個人的には少なくともViewModelが参照するModelは、生存期間を同じにしておきたいなあ。

おわりに

長々と書いていて「こう考えるのが正しいのではないか?」というのはところまでは来たけども、まだ確信を持って「この考えで正しい!」とは言えない・・・
しっくりは来てるんだけどなあ。
でも序盤に挙げた「目的」を明確に認識しておくだけでも「その形が間違ってるか?」の判断材料にはなりそう。
少しは最新の開発スタイルに近付けただろうか・・・?

あと画面遷移とかダイアログ表示とかDIコンテナとか調べだすと、Prismとか外部フレームワークに頼りたくなる理由が分かってきた・・・
何となく自力でも実装できそうな雰囲気だけど、かなり面倒そうな印象。

おまけ:DIコンテナって・・・

MVVMについて調べる中で初めて「DIコンテナ」を知ったのだけど(今さら)、解説を読んだときに真っ先にあいつを思い出した・・・

HRESULT CoCreateInstance(
  REFCLSID  rclsid,
  LPUNKNOWN pUnkOuter,
  DWORD     dwClsContext,
  REFIID    riid,
  LPVOID    *ppv
);

取得したいInterfaceの情報を渡すとインスタンスが返ってくるってこれじゃん。
そう思うと急に親しみが湧いてきたのでした。おしまい。

  1. 理解力が足りないだけかもしれない。

  2. Xamarinを使えば出来たりしないかな?(Xamarin未経験)

  3. .Net Standard大好き

  4. 何をする画面かは知ってる。じゃないと何の処理をすれば良いのか分からない。あくまでデザインを知らないだけ。

  5. このパラメータはテキストボックス的なやつで、このパラメータはチェックボックス的なやつで・・・みたいな。

  6. Viewがバインディングするところでエラーが出ないようにするべき?(数値入力前提のテキストボックスでも、とりあえずstring型のプロパティで受け取るとか)

  7. Modelの処理に依存するエラーは知らない。あくまで「渡せるかどうか」だけ。

  8. INotifyPropertyChangedINotifyDataErrorInfoはどういう実装が便利だろう?とお試しで作ったクラス。実際にこれで良いかは分からない。(全容はGitHubで・・・)

  9. stringにしておけばどんな入力でも受け入れられるテキストボックスだからってだけで、どんな型にしてもエラーが発生しうるコントロールの場合は、ViewViewModelの通知は避けられない?

  10. ただその情報のせいで余計に混乱した

  11. 混乱の素

14
16
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?