ささいなことですが。

Windowsアプリテスト自動化ライブラリFriendly開発者の日記です。

ViewModelをシンプルに書きたい! - ⑦MVVMを考える編 - 本編 -

ViewModelとはViewをモデル化したものである

ある人に教えてもらったのですが、その瞬間に色々なことがスッキリしました。ViewModelの役割って様々な主張があるけど、骨子はこれですね。

モデル化とは・・・

検索したら++C++がかかりました。さすが岩永さんは色々書いてますねー。
http://ufcpp.net/study/dsl/mdd.html

  • モデル化とは、 現実の問題から、問題解決に必要な部分だけを抜き出して簡単化・抽象化することです。
  • 「よいモデル化」というのは、 問題の要件を必要十分に、過不足なく表せるモデルを作ることです。 情報は多すぎても少なすぎてもダメ。

このViewをモデル化してみます。この画像は出来上がったもののキャプチャですが、こんな画面仕様書が来たと思ってください。コンボボックスの選択を変えたら、そのミュージシャンのアルバム一覧が表示されるというものです。
(※リリース日は年はあってますが、月日はわからなかったので嘘ものです)
f:id:ishikawa-tatsuya:20160612140052p:plain
この画面は具体的ですね。ComboBoxとかDataGridViewとか。
これを心の目で見るのですw
f:id:ishikawa-tatsuya:20160613090909p:plain
見えましたか?GUIの詳細はフィルタされてます。それから固定値のTextBlockもフィルタしました。そしたら、これをコードに落としましょう。

public class MainWindowVM
{
    //ミュージシャン
    public ObservableCollection<Musician> Musicians { get; } = new ObservableCollection<Musician>();

    //現在選択されているミュージシャン
    public ReactiveProperty<Musician> SelectedMusician { get; } = new ReactiveProperty<Musician>();

    //アルバム
    public ObservableCollection<Album> Albums { get; } = new ObservableCollection<Album>();
}

次に、これを操作します。操作は・・・、WinFomrsでコードビハインドに実装しているイメージでいいんじゃないですかね。操作対象が具体的なGUIコントロールではなく抽象化されたオブジェクトを操作するように変わっただけです。

using Reactive.Bindings;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Linq;
using System;

namespace WpfApp
{
    public class MainWindowVM
    {
        MusicInfo _info = new MusicInfo();

        //ミュージシャン
        public ObservableCollection<Musician> Musicians { get; } = new ObservableCollection<Musician>();

        //現在選択されているミュージシャン
        public ReactiveProperty<Musician> SelectedMusician { get; } = new ReactiveProperty<Musician>();

        //アルバム
        public ObservableCollection<Album> Albums { get; } = new ObservableCollection<Album>();

        public MainWindowVM()
        {
            //初期化
            _info.GetMusicians().ToList().ForEach(e => Musicians.Add(e));

            //選択ミュージシャン変更イベント
            SelectedMusician.Subscribe(_AppDomain => MusicianChanged());

            //最初は0番目を選択
            SelectedMusician.Value = Musicians[0];
        }

        public void MusicianChanged()
        {
            //変わったらそのミュージシャンのアルバム一覧を取得
            Albums.Clear();
            _info.GetAlbums(SelectedMusician.Value.Id).ToList().ForEach(e => Albums.Add(e));
        }
    }
}

*1
過不足なく抽象化されたViewModelのオブジェクトを操作するのであれば、ハマりポイントの多いGUIコントロールと違って素直に実装できます。しかもテストコードで確認しながら実装できるのです。(理論的には。みんなやってるんですよねw)
ポイントは、あくまでViewをモデル化したものなので、View起因で設計されるものです。決してモデル起因ではありません。

ViewMdoelは薄く

抽象化かかってるとはいえ、あくまでViewをモデル化しただけです。そこにロジックをモリモリ載せるのではなくモデル層でしっかり設計、実装したロジックを呼び出すだけにしましょう。でも、Viewとは違って抽象化かかってるから、コントローラを挟まず直接モデルを呼び出してもOKという判断なのでしょう。

View

Viewは極力Xamlで書くのがいいようですね。とは言え、Viewのお仕事はGUIコントロールの操作なのでそこから外れなければコードビハインドもOKではないでしょうか。

Model

Modelは、まあMVCでやる場合とあんまり変わらない印象です。前回でも書きましたが、MVナントカ関係なくしっかり設計しましょう。

MVVM怖くない

こう考えるとMVVMは怖くなくなるのではないでしょうか。

  • Modelはあんまり変わらない
  • ViewModelはコードビハインドに書くより簡単
  • Viewは・・・、Xamlが怖いかw

チームで導入する場合、デザイナとプログラマで仕事を分けるとかありますけど、現実的には先行でWPFの知識を持った人とその他のプログラマで分けたらいいかもしれないですね。そしたら他の人が慣れてくるまで、その人にひたすらXaml書いてもらえばよいのではないでしょうかw

短期集中連載終わり

2日間で7本は自分の中では記録ですね。また何か便利な書き方学んだり、書きたいことが出てきたらアウトプットしていきます。

連載で使ったMarkup拡張のコードはこちらにおいてます。

github.com

おまけ

サンプルのViewとModelのコードです。

<Window x:Class="WpfApp.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:l="clr-namespace:WpfApp"
        xmlns:c="clr-namespace:VVMConnection;assembly=VVMConnection"
        Title="MainWindow" Height="350" Width="525">
    <Window.DataContext>
        <l:MainWindowVM/>
    </Window.DataContext>

    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="auto"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="auto"/>
                <ColumnDefinition Width="*"/>
            </Grid.ColumnDefinitions>
            <TextBlock Text="ミュージシャン" Grid.Column="0" Margin="10,0,0,0"/>
            <ComboBox ItemsSource="{Binding Musicians}" SelectedItem="{Binding SelectedMusician.Value}" Margin="10,0,10,0" Grid.Column="1" DisplayMemberPath="Name"/>
        </Grid>
        <TextBlock Text="アルバム一覧" Margin="10,30,0,0" Grid.Row="1"/>
        <DataGrid ItemsSource="{Binding Albums}" AutoGenerateColumns="False" CanUserAddRows="False" Grid.Row="2">
            <DataGrid.Columns>
                <DataGridTextColumn Header="リリース日" Binding="{Binding ReleaseDate, StringFormat={}{0:yyyy年MM月dd日}}" Width="150" IsReadOnly="True"/>
                <DataGridTextColumn Header="アルバムタイトル" Binding="{Binding Title}" Width="*" IsReadOnly="True"/>
            </DataGrid.Columns>
        </DataGrid>
    </Grid>
</Window>
using System;

namespace WpfApp
{
    public class Musician
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    public class Album
    {
        public string Title { get; set; }
        public DateTime ReleaseDate { get; set; }
    }

    public class MusicInfo
    {
        public Musician[] GetMusicians()
        {
            return new Musician[]
            {
                new Musician() { Name = "Kenny Burrell", Id = 0},
                new Musician() { Name = "Aerosmith", Id = 1},
                new Musician() { Name = "BLANKEY JET CITY", Id = 2},
            };
        }

        public Album[] GetAlbums(int musicianId)
        {
            switch (musicianId)
            {
                case 0:
                    return new Album[]
                    {
                        new Album() { Title = "Introducing", ReleaseDate = new DateTime(1956, 1, 1)},
                        new Album() { Title = "Kenny Burrell",  ReleaseDate = new DateTime(1957, 2, 2)},
                        new Album() { Title = "Blue Lights Vol.1", ReleaseDate =  new DateTime(1958, 3, 3)}
                    };
                case 1:
                    return new Album[]
                    {
                        new Album() { Title = "Aerosmit", ReleaseDate = new DateTime(1973, 4, 4)},
                        new Album() { Title = "Get Your Wings",  ReleaseDate = new DateTime(1974, 5, 5)},
                        new Album() { Title = "Toys in the Attic", ReleaseDate =  new DateTime(1975, 6, 6)}
                    };
                case 2:
                    return new Album[]
                    {
                        new Album() { Title = "Red Guitar And The Truth", ReleaseDate = new DateTime(1991, 4, 12)},
                        new Album() { Title = "Bang!",  ReleaseDate = new DateTime(1992, 1, 22)},
                        new Album() { Title = "C.B.Jim", ReleaseDate =  new DateTime(1993, 2, 24)}
                    };
            }
            return new Album[0];
        }
    }
}

追記

@yone64さんに、「こう書いたらもっとReactiveだよ。」って教えていただきました。ありがとうございます!

using Reactive.Bindings;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Linq;

namespace WpfApp
{
    public class MainWindowVM
    {
        MusicInfo _info = new MusicInfo();

        //ミュージシャン
        public ObservableCollection<Musician> Musicians { get; } = new ObservableCollection<Musician>();

        //現在選択されているミュージシャン
        public ReactiveProperty<Musician> SelectedMusician { get; } = new ReactiveProperty<Musician>();

        //アルバム
        public ReactiveProperty<ObservableCollection<Album>> Albums { get; }

        public MainWindowVM()
        {
            //初期化
            _info.GetMusicians().ToList().ForEach(e => Musicians.Add(e));

            //最初は0番目を選択
            SelectedMusician.Value = Musicians[0];

            //選択ミュージシャンとAlbumsをReactiveな感じでつなぐ
            Albums = SelectedMusician.Select(e => new ObservableCollection<Album>(_info.GetAlbums(SelectedMusician.Value.Id))).ToReactiveProperty();
        }
    }
}

*1:こっちも@yone64さんにReactiveなイベントのつなぎ方を教えていただいたのでちょっと修正