2017年11月18日土曜日

MVVMとは何か

MVVMとは何か、それはWPFプログラミングを始めた人は誰しもが気になることでしょう。
私はあまりWindows以外のプラットフォームは触らないのですが、最近はAndroidでもMVVMが流行ってるとかなんとか。

そのせいか、MVVM+適当なワードでググるといくらでもQi○taなどの記事がヒットします。ですが、多くは「データバインディングを使えばMVVM」くらいの浅はかな理解で、MVVMとは何かがまるでわかっていないようです。
まあ「初心者」というのは誰もが通る道ですからそういうのをはなから否定する気はないですが、情報汚染されているのは悲しいので私の理解の範囲を記事にしておこうと思います。

まず忘れてほしいこと

MVVMとは何かというのを調べ始めるとまず見つかることですが、それはMVVMの本質ではないというのをいくつか挙げていきます。私の今回の記事を読むうえでまずは次の3点のことを忘れてください。

1. データバインディングをするのがMVVMである

MVVM初学者は真っ先にこれにぶち当たるかもしれませんが、データバインディングはあくまでもViewとViewModelのデータのやり取りの手段に過ぎません。データバインディングをするのがMVVMではありません。
似たようなものとして「ユーザーインターフェースとビジネスロジックの疎結合を実現するのがMVVMである」というのも大嘘です。

確かにWin32APIやWinFormsなどをいじっていた人からしたら、今までUIとビジネスロジックを一体的に記述していたわけですから、それは大きなモチベーションに見えるでしょう。それぞれを分離して設計できれば、設計を分担するのもやりやすいですし、再利用性、可読性も上がります。何しろ手続き型言語であるC言語やC#などでUIを設計するそれらのフレームワークに比べて、XAMLとかいうマークアップ言語でUIが設計でき、ビジネスロジックをC#で書ければそれはたいそうなメリットに見えるでしょう。私も当初はそうでした。

でも考えてください。それならばView-Modelで良いんです。UIとビジネスロジックを分離するって、それってView-Modelですよね。で、ModelとViewの接続にはデータバインディングを使うと。
実際、この手のモチベーションだけでMVVMを書こうとすると、ViewModelが実質Model的な役割になって、結局のところView-Modelになってしまいます。それじゃあMVVMじゃないんです。

この記事の下のほうへ行くと見えてくると思いますが、MVVMは「UIとビジネスロジックの疎結合」なんていうのは当たり前なんです。それだけだと不都合が起こるから、ViewModelが介在しているんです。

2. MVVMはコードビハインドを書かない

これは結果論としては間違っていないとは思いますが、違います。
何も考えずにコードビハインドを書くとたいていそれがビジネスロジックになりますから、UIとビジネスロジックの分離すらできていない状態になります。それは"論外"以前の問題です。
正確には「WPFはコードビハインドを書かなくて済む」なのです。データバインディング、コマンド、ビヘイビアなどの様々な「Viewとやり取りする方法」があるため、結果としてコードビハインドを書く必要が無いケースがほとんどなのです。

ですので、WPF初心者は「コードビハインドを書かない」をテーマにいろいろ調べてみるといいと思います。いろいろな手段が出てきていい勉強になるでしょう。ですが、あくまでもそれが目的ではないのです。

3. Modelはビジネスロジックなので、UIに依存しない

これは見方によっては間違ってはいないのですが、間違った意味にも取れるので書いておきます。そして、多くの人は間違った意味に取るように思います。
「依存しない」の意味を正しく理解しているかがキーです。

「UIに依存しない」というのは「UIのフレームワークで生まれた制約を知らなくていい」というニュアンスの意味なのです。UIのフレームワークの制約を知らなくていいだけなので、ModelはUIの存在を強く意識します

これだけだと何を言っているのかよくわからないので、メール送信ソフトを作っている場面を想像しながら説明します。
例えば送信ボタンを押したとき、ボタンのコマンドが発動します。コマンドはViewModelに記述しますが、そのViewModel内でTextプロパティやToAddressプロパティ、Subjectプロパティなどの値を取得し、
mail.Send(ToAddress, Subject, Text);
などといったメソッドを呼ぶなどといった処理を記述しがちかと思います。なおこのMailクラスは当然Modelにあるものとします。

これは大間違いなのです。

これだとModelは「ライブラリ」です。このスタイルではView-Model-Libraryになってしまいます。Modelとライブラリの関係なんてUIフレームワークの側からなんかは知ったこっちゃないですよね。ですのでこれはView-Modelと何ら変わらないのです。
なぜこういう書き方をしてしまうかというと、MVVM初心者はViewModelが何のためにあるのかがわかっていないのです。そのため、「ViewとModelをつなぐもの」という説明からViewの範囲をUI、Modelの範囲をビジネスロジックライブラリに限定してしまい、上記のような誤った実装をしてしまうのです。

ではViewModelは何のためにあるのか。実は、ViewModelとはView(=UI)の制約を吸収するためだけの層なのです。なので、Viewがあれば必然的にViewModelも出来上がります。そして残りの領域がすべてModelになるわけです。

「ModelはUIに依存しない」によく似た表現として、「ViewとModelがあって、その間を取り持つのがViewModel」 というのがありますが、これも大いに誤解を生む表現です。正しくは「ViewとViewModelがあって、それ以外の領域がModel」なのです。
「ありき」なのはViewとModelではなく、ViewとViewModelなのです。「Modelはビジネスロジック」といったようにModelの範囲を限定している時点でそれはもう間違っているのです。

WPFの制約

ここからは、UIをWPFに限定してお話をしていきます。
さて、先ほど「UIの制約を吸収するのがViewModel」と言いました。では、WPFにおけるUIの制約とは何なのでしょうか。下記に2例示します。

1. UIは単一スレッドでしか操作できない

WPFはUIを操作できるスレッドは1つしかありません。
複数のスレッドからUIをいじれるようにしようとすると排他制御などで逆に効率が落ちるらしく、それならばと単一スレッドでしか使えないという縛りを入れているようです。
なので、UI以外のスレッドからアクセスしようとしたら例外が吐かれます。

これを吸収するのがViewModelの役目です。
ViewModelは、Modelで何かしらのプロパティの値が変更されたら、それをUIのスレッドでViewに変更通知をします。それ以下のこともそれ以上のこともしません。

MVVMライブラリは、ViewModelの基底クラスでこの処理を行っています。PropertyChangedイベントは必ずUIのスレッドで発生するように作ってあります。ですので初心者は「WPFは単一スレッドでしかUIを触れない」という制約すら知らずに済むのです。MVVMが成功している証です。

ここで注意しなければならないのは、コレクションの変更通知はプロパティ変更通知とはまた別ということです。
ObservableCollectionは何もしなければコレクションを更新したスレッドからそのままCollectionChangedイベントが発生しますので、UIスレッド以外から更新すると例外が飛びます。ですので、ObservableCollectionもコレクションとUIを結びつけるものとしてViewModelとしてUIのスレッドでCollectionChangedイベントを発生させるようなものを作ってやる必要があるのです。
//BindingOperations.EnableCollectionSynchronizationメソッドというものがありこれを使えばUIスレッドに転送できますが、なんでこのような取ってつけたような実装にしたのか理解に苦しみます…。

2. リスト系コントロールは表示されている分しか要素のインスタンスを持たない 

これはどういうことかというと、リストの要素を操作しようとしたときに実体(インスタンス)が無い場合があるということです。
理解するために次のようなコードを書いてみます。

public partial class TestUserControl : UserControl
{
    static int counter = 0;

    public TestUserControl()
    {
        Console.WriteLine(counter++);
        InitializeComponent();
    }
}

まずはこんなコントロールを作ってみます。このコントロールのインスタンスが作られた回数を標準出力に書き出すだけのコントロールです。

<Window x:Class="WpfListControlTest.MainWindow"
        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:WpfListControlTest"
        mc:Ignorable="d"
        Title="MainWindow" Height="350" Width="525">
    <Grid>
        <ListBox Name="list">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <local:TestUserControl Height="20" />
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </Grid>
</Window>

こちらがMainWindowです。Listの要素としてTestUserControlを表示しています。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();

        for(int i = 0; i < 100; i++)
            list.Items.Add(new object());
    }
}


めんどいのでMainWindowのコードビハインドでアイテムを100個追加してみます。

直観的に考えたらソフトを起動したときに100個のTestUserControlが生成されそうな気がします。


14個しかインスタンスが生成されていません。
これは表示されている分で、それ以上のインスタンスは生成されていないのです。

何往復かスクロールしてみます。


100個しかアイテムが無いはずなのに269回インスタンスが作られています。
表示外に外れたインスタンスは消去され、再び表示されたらまたインスタンスから作り直しているんですね。
こうすることで、確かにヒープに負荷はかかりますが何万個とアイテムを持ったリストなどの生成を可能としているのです。

もしもこれにViewModelを用意しなかったらModelでViewをいじろうとしても表示外だったらぬるぽが発生するのです。
そこで、ViewModelでワンクッションを置くことでModelからはいつでもすべての表示項目のViewModelをいじれるようにしてあげます。項目が表示されればViewのインスタンスが生成され、同時にViewModelにバインディングすることであたかも隠れていた項目が今表示されたかのようにふるまうことができるのです。

これもUIの都合ですので、それを吸収するのがViewModelの役目です。

WPFの制約まとめ


どうでしょう、案外WPFにはいろいろな制約があることが分かったと思います。ほかにもいろいろあるかもしれませんが、これだけでMVVMの必要性を説明するのには十分だと思います。

Modelにとっては、UIは単一スレッドでしか触れないことや表示されていない項目のコントロールのインスタンスが存在しないことなんて知ったこっちゃありません。それを吸収するのがViewModelの役割なのです。なので、「これとこれを表示するUI(TextBoxやListViewなど)がある」とか「ボタンが押された」などといったUIの存在自体やUIの主体となる動作はModelは意識する必要があるのです。

このような目的から、Modelから見たViewModelはViewのように見えなければなりません。ViewModelはViewの制約をなくすためのラッパーなのですから、当然機能としてはただのViewと同等になるわけです。

MVVMとは

MVVMの根底には確かにUIとビジネスロジックの分離があります。でも、単純にViewとModelに分けようとすると、WPFの場合はViewに様々な制約があり、Modelがその制約に引きずられたものになってしまいます。そうすると、Modelと言っておきながらWPFに深く入り込んだものが出来上がり、分離とは言い難い形になってしまうのです。
そこで、Viewの制約を吸収してあげる層、すなわちViewModelを作って制約なくModelからViewを操作できるようにしてあげようというのがModel-View-ViewModelこと、MVVMなのです。

概念だけ説明されてもわからないよ、という人もいるかと思いますので、次回は実際にソフトを作りながら解説していきます。

2 件のコメント:

  1. MVVM初心者です。
    「メール送信ソフトを作っている場面を想像しながら説明します。
    例えば送信ボタンを押したとき、ボタンのコマンドが発動します。コマンドはViewModelに記述しますが、そのViewModel内でTextプロパティやToAddressプロパティ、Subjectプロパティなどの値を取得し、
    mail.Send(ToAddress, Subject, Text);
    などといったメソッドを呼ぶなどといった処理を記述しがちかと思います。なおこのMailクラスは当然Modelにあるものとします。」

    これが大間違いとの事ですが、ではMVVMライクに書くとどういう記述になるのですか?

    返信削除
    返信
    1. コメントありがとうございます。

      その部分をMVVMで記述すると、ViewModelとModelの両方にTextプロパティやToAddressプロパティ、Subjectプロパティを持たせることになります。例えばメール本文が入力されたら、データバインディングでViewModelのTextプロパティが更新され、そのタイミングでViewModelがModelのTextを同じ文字列で書き換えるような仕掛けを仕込むことになります。
      Sendボタンが押されたら、ViewModelのコマンドでそれを受け取り、そのコマンドからModelにあるSendButtonPushedメソッドを呼びに行きます。このメソッドには引数は何もなく、SendButtonPushedメソッド内でModelのTextプロパティやToAddressプロパティ、Subjectプロパティの値を使ってmail.Send(ToAddress, Subject, Text);というメソッドを呼ぶことになります。
      すなわち、View-ViewModel-Modelの3者間ではすべての要素(≒プロパティ)が一対一で対応しているのです。

      一見冗長に見えるかもしれませんが、これは本ブログ内でも説明している通り、ModelはUIのロジックを記述するためのもの、ViewModelはUIのフレームワーク都合の制約を吸収するためにあるものだからです。同じ目的でUIをデザインするなら、どのようなフレームワークであったとしても、宛先アドレス、件名、メール本文のテキストボックスと送信ボタンを実装することになるため、それらの挙動に関するコードはModelに記述することになります。一方で、例えばWPFでは単一スレッドでしかUIが触れないなどの制約がありますが、別のUIのフレームワークだとそのような制約は無いかもしれないですし、別の制約があるかもしれません。そのようなものをViewModelで吸収しようというコンセプトになります。

      こうすることで、例えばiPhone/Android両対応アプリを作るとしても、iPhoneのUIのフレームワーク特有の制約に従ってViewとModelがやり取りするコードをiPhone向けのViewModelに記述し、AndroidのUIのフレームワーク特有の制約を同様にAndroid向けのViewModelに記述することで、Modelから先は完全に共通のコードにすることができるのです。

      MVVMでTwitterクライアントを実装する例をこちらのブログに掲載していますので、もしよろしければご確認ください。 https://days-of-programming.blogspot.com/2017/11/mvvm-twitterviewer.html

      削除