nuits.jp blog

C#, Xamarin, WPFを中心に書いています。Microsoft MVP for Development Technologies。

XAMLアーキテクチャ ViewModel Class(≠Role)不要論

これはXamarin Advent Calendar 2018の22日目の記事…のつもりでしたが一日勘違いして一時間半程間に合いませんでした。埋めていただいた方、ありがとうございました。すいません。

さて内容はちょっとはみ出してWPFやUWPといったXAMLを利用するUIフレームワーク全般で共通な話です。

私は前々からXAMLアーキテクチャの場合、必ずしも(Topレベルの)ViewModelのクラスをViewのクラスから分離する必要はなく、XAMLのコードビハインドをViewModelとして実装しても良いのではないか?ViewModelは役割としては必要でも、クラスとしては必ずしも必要ではないのではないか?と考えていました。

もちろん常にViewModelクラスが不要だという訳ではありません。特定の制約を満たす必要がありますし、その制約を徹底できるだけの組織的な統制力が必要になります。*1

本稿ではその辺りの論拠などを整理してみたいと思います。

何がうれしいのか?

ViewとViewModelを分離していることで、例えば次のような悩みを誰もが経験したことがあるはずです。

  • ListViewのSelectedItemイベントなどをViewModelに通知したいけどCommandを直接設定できない
  • アラートや確認のダイアログを開きたいがViewModelからどう開いて、どう結果を受け取るか?

ViewModelがXAMLのコードビハインドに記述されているなら、これらに悩む必要はないでしょう。もっとも後者はViewModelからダイアログを表示してよいのか?という設計上の疑問はありますが。

ViewModelクラスを破棄してどう実装するか?

まずタイトルにある通り、ViewModelのクラスを破棄するといっても、ViewModelの役割を無くすという意味ではありません。

ViewModelを独立したクラスに分離するのをやめて、XAMLのコードビハインドをViewModelとして利用します。もちろんバインディングは利用しますし、ViewとViewModelの役割は分離します。Viewは.xamlに、ViewModelは.xaml.csに記述し、役割(Role)自体は分けて設計・実装します。

皆さん勿論コードビハインドにはInitializeComponentの呼び出し以外は一切のコードを記述していないでしょうから(冗談です。コードビハインドにあまり記述はないでしょうから)、そこにViewの実装と混じらずにViewModelのコードを記述することができるでしょう。なんならpartialクラスなので、さらにViewModel用に同じクラスの別ファイルを用意しても良いでしょう。

簡単に具体例を示しましょう。まずコードビハインドです。

public partial class MainPage : ContentPage
{
    public string Message { get; } = "Hello, World without ViewModel!";
    public MainPage()
    {
        InitializeComponent();
    }
}

コードビハインドにMessageプロパティが定義されています。これをViewにバインドしましょう。

次がMainPage.xamlのコードです。

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XFApp.MainPage"
             x:Name="This"
             BindingContext="{Binding Source={x:Reference This}}">
    <Label Text="{Binding Message}" 
           HorizontalOptions="Center"
           VerticalOptions="Center" />
</ContentPage>

ポイントは二つあります。

  1. ContentPageのインスタンスにThisという名前をつけている
  2. BindingContextへx:Referenceを利用して自分自身の参照を設定している*2

MainPageクラス自身のインスタンスをBindingContextに設定しているため、ViewModelのクラスを分離している場合と同様の実装が可能です。

必要な制約とは

具体的には以下のとおりです。

  • ViewModelのUnit Testを行わない
  • Viewの役割を.xamlに、ViewModelの役割を.xaml.csのみに記述し不用意に越境しない

です。私のメモには3つの制約が必要と書いてあるのですが、3番目が何だったのかついぞ思い出すことができませんでした。まぁ良いでしょいう(良くない)。多分2つで正しいです。

前者を徹底することは難しくないでしょう。しかし後者は、うっかりすると直ぐに制約を侵害してしまいます。

このためには制約を徹底する統制力がチームに求められます。徹底できないと思うのであれば、素直にViewModelのクラスを分離した方が良いでしょう。

ただ統制を効かせるといっても、ViewとViewModelを統合したときの境界条件を一切超えない、とするならViewModelをViewのコードビハインドに記述するメリットも何もありません。ViewModelクラスを破棄して統合する意味はないでしょう。

適度にチームで決定された範囲で、通常のViewとViewModelの境界を越境しつつも、越境しないと決めた範囲を超えないという統制が必要になります。それができるなら前述のメリットを享受することができるでしょう。

閑話:ViewとViewModelのクラスを統合しない場合

ところで、ここまで読んで統合することないからもう読まないでいいかなって思ったそこの君!ViewとViewModelのクラスを同じアセンブリに含めたりしていませんか?

ViewとViewModelのクラスを分けて役割をわける選択肢を取るなら、アセンブリも分けるべきです。なぜか?同じアセンブリにあれば簡単に越境できてしまうからです。

次のコードを見てください。

public partial class MainPage : ContentPage
{
    public MainPage()
    {
        InitializeComponent();
        BindingContext = new MainPageViewModel { MainPage = this };
    }
}

public class MainPageViewModel
{
    public MainPage MainPage { get; set; }

    public void Action()
    {
        MainPage.Navigation.PushAsync(new SecondPage());
    }
}

迂闊なコードですが、案外やりたくなるものです。私も過去にやりましたし、ViewModelからViewへの通知を解消する方法として、Web上にこの例をまれに見かけます。主にMVVMの理解が進んでいなかった過去の記事ですが。

アラート表示や画面遷移などに必要なオブジェクトはView側にあります。

上の例ではViewModelから画面遷移するために、そのViewModelが所属するViewをプロパティに受け取って操作しています。これをやってしまうなら、ViewとViewModelを分ける意味は失われます。

これを防ぐ方法は二つあります。

  • 統制によって回避する
  • ViewとViewModelのアセンブリを分ける

ViewからViewModel方向への参照のみを張ることで、この実装は完ぺきに防げます。アセンブリを分離しない理由は無いので、ViewとViewModelのアセンブリは分けましょう。

さて、それではもう少し深堀して考察していきましょう。

ViewModelのUnit Testを行わない事は正しいのか?

私にとっては「はい」。

私は自他ともに認めるテスト厨ですが、最近はViewModel単体での品質保証に懐疑的です。コストと時間が無限にあるならやりますけど。

ViewとViewModelは仕組み上疎結合になっていますが、実際にはお互いがお互いを意識していないと実装できません。ViewとViewModelはセットでPresentationを実現するのでその辺りは仕方がないでしょう。同一の関心ごとに対して、異なる領域を実装しているに過ぎません。

このためPresentationへの変更要求が発生したとき、些細な問題でないかぎりはViewもViewModelもいずれも影響を受ける可能性が高いでしょう。

こうした時、ViewModelが単体で動作するということにどれほど価値があるのでしょうか?

テストにはコストがかかります。また作って終わりでなく、メンテナンスし続ける必要があります。高いコストを払ってViewModelの単体テストコードを書いても、Presentationの一部の品質しか保証されません。

Presentationの価値はViewと統合されて初めて発揮されます。ViewModelのUnit Testコストを他に投入するという選択肢は現実的な選択肢だと私は考えています。例えばPresentationの自動テストに取り組むとか。

もっとも、全く同じViewModelが異なるViewに配備されることが実際にあるのであれば、ViewModel単体の自動テストに意味はあるでしょう。

通常コードビハインドに何故コードを書かないのか?

コードビハインドなんて名前してるくせに。もっとも私は必ずしもダメだとは思いませんけど。

XAMLアーキテクチャを用いて実装した場合、Viewの実装はできるだけXAMLに寄せることが一般的なように思います。そしてコードビハインドにはInitializeComponent以外に実装していない例をよく見かけます。

これは別にその方が「イケてる」からではなく明確な理由があります。私はその理由は主に次の二つにあると考えています。他にもあるなら教えてください。

  1. コードビハインドに記述したコードは再利用できない(しない)
  2. コードビハインドに記述したコードはテストできない
  3. ListViewの子UI要素などを手動で生成すると碌な目に合わない

できない訳でもないですが、トップレベルのViewに書かれたコードを、別のViewから呼び出すなんてことは余計あるべき姿ではないでしょう。したければBehaviorやTriggerなどで共通クラスを抽出してそちらに実装を移すはずです。

そしてコードビハインドに記述したコードのテストは、不可能ではありませんが困難です。これは主にUI要素はUIスレッドでの動作のみが想定されているため、単体テストフレームワークとの親和性が低いためです。*3

また例えばListViewの子UI要素などは、全要素分生成されるわけではなく仮想化される前提になっている事があります。Flyweightパターンってやつです。この時、手動で制御しようとすると不具合の元になりやすいです。これを回避するためには、バインディングやDataTemplateの仕組みを正しく使うのが正道です。

結果的にコードビハインドに残るコードはあまりなくなるため、綺麗に書かれたサンプルコードではコードビハインドに実装が含まれていないという事が良く起こります。

逆に言うと再利用しないことが明確で個別に単体テストするほどでもない場合*4、コードビハインドに記述してしまって問題ないはずです。*5

コードビハインドにViewModelを含めるとちょっと不便なこと

一般的なMVVMフレームワークが使いにくくなることがあります。

Dependency Injection(以降DI)パターンを採用することが前提のMVVMフレームワークではViewModelクラスをDIのルートオブジェクトと想定していることがあるからです。

もちろんModelの最上位オブジェクトやViewをDIのルートオブジェクトとして扱ってもよいのですが、画面遷移などでフレームワークが正しく動作する保証はありません。

一般的ではない実装方式をとると、こういった方向から不便を被る可能性があります。

資産が溜まってくるとViewとViewModelのクラスを統合するメリットは薄まる

そもそもViewクラスとViewModelクラスを統合したい動機は次のようなものでした。

  • ListViewのSelectedItemなどをViewModelに通知したいけどCommandを直接設定できない
  • アラートや確認のダイアログを開きたいがViewModelからどう開いて、どう結果を受け取るか?

これらのちょっと不便だと思うところは、MVVMパターンを実装するための資産が溜まってくれば不便に思うことも減り、徐々に統合するための動機も薄まります。

特にXamarin.Forms向けには多くの不満を解消してくれるXamarin.Forms.BehaviorsPackという素晴らしいライブラリが私からリリースされています(えっへん。ドキュメントに書いていないことに今更きがつきましたが、Xamarin.Formsの標準UIオブジェクトの全てのイベントにCommandをバインドできる仕組みも提供しています。

とはいえ、ふとした時に不便に思うことが完全になくなる訳でもないでしょう。でもそれは必要なコストなのかも知れません。

まとめ

つぎの二つの制約を受け入れられるなら、ViewModelのクラスを無くしてViewとクラス統合し、ViewはXAMLにViewModelはコードビハインドに実装するという選択肢は、現実的な選択肢の一つではないでしょうか?

  • ViewModelのUnit Testを行わない
  • Viewの役割を.xamlに、ViewModelの役割を.xaml.csのみに記述し不用意に越境しない

ただし、後者は組織的にそれを徹底する統制力を必要とします。そしてそれは非常に困難な問題であると思います。

ViewModelクラスをViewクラスに統合することで、次のようは不便に感じる問題を解決することが容易になります。

  • ListViewのSelectedItemなどをViewModelに通知したいけどCommandを直接設定できない
  • アラートや確認のダイアログを開きたいがViewModelからどう開いて、どう結果を受け取るか?

とはいえ、これらはMVVMで実装していき資産が溜まってくれば、概ね解決できる問題でもあると思います。ただし初期は不便ですし、世の中にそれらをすべて解決してくれるものが揃っているとは限りません。

逆にViewとViewModelのクラスを分ける選択をした場合は、それらのアセンブリ(つまりプロジェクト)は分割しましょう。十分な統制が図れていないと、ViewとViewModelの役割を越境して双方向参照の状態に陥りかねません。

結局、ViewModelのUnit Testに価値を見出せず、ViewとViewModelが疎結合なアーキテクチャを取っていても論理的には双方を理解していなくてはならない。そんな背景がViewとViewModelのクラスを分離しておくことに疑問を抱かせているように思います。

みなさんはどう選択しますか?思考ゲームとして再考してみる価値はあるかもしれません。

以上です。

*1:ちなみに私自身は統制を徹底できる気がしないので、結局ViewModelのクラスは分離しています

*2:ElementNameはXamarinで未対応なのでx:Referenceをここでは利用しています

*3:WPFなどはそもそもオブジェクトを生成することから困難ですし、プロパティに触ることもできません

*4:ViewModel単体のテストに効果が低いように、再利用されないView単体のテストも効果は低いでしょう

*5:一見コードの見通しに問題があるようにも思えますが、BehaviorやTriggerに切り出したコードがXAMLから読めるわけではないため同じことです