はじめに
Livetを使用する以上、MVVMに沿ったやり方で開発することとなります。Rails使ったらMVCに沿うのと同じノリです。で、MVVMなんですが、ざっくりでこんな感じです。
パート | ざっくりとした役割 |
---|---|
View | UIに関する部分。UIが必要なロジック(描画計算)とかもココ。Window一つ分、Page一ページ分に相当。 |
ViewModel | ViewとModelを対応付け、入力なりなんなりをルーティングする部分。Viewと一対一で対応する事もあれば、一対多、多対多な対応もある。ので、複数のViewを管理することもある。 |
Model | その他。アプリそのもののロジックやら、データやらなんやら。 |
各々の責務はサーバサイドでよくあるMVCの責務とかなり似ています。まあ要するにModelを中心に開発していきます。描画に必要な部分をModelからViewに追い出して、Viewを管理するために(Modelから遠ざけるために)ViewModelが間に入ります。ちなみにViewは描画に関係する部分であり、見た目全般を司るものではありません。見た目の情報をModelが持っても良いです。まあこの辺はちょっとややこしいので後でもうちょっと詳しく述べます。
さて、今回はView<->ViewModel<->Model間の通信について説明します。前回やったデータバインディングはデータのシームレスなやり取りで、まああれも通信なんですが、ちょっと特殊なのでここでの通信には含めません。つまり、ここで言う通信とはもっと能動的なものです。
さて、各々のレイヤー間の通信の意味についてかなりざっくりですが以下のようにまとめてみました。
通信の対象 | 通信が必要なケース |
---|---|
View -> ViewModel | マウスイベントの補足等、データバインディングでは扱えない場合 |
View <- ViewModel | UIオブジェクトのメソッドを呼び出す等、データバインディングでは扱えない場合 |
ViewModel -> Model | Viewでの入力を、ロジックとかを担当するModelに教えてあげる場合 |
ViewModel <- Model | ロジック側からViewに描画して欲しいことがある場合 |
ここで重要なのは、View <-> Model
間の通信が存在しないということです。理由は大きく2つあります。
- 直接的な相互依存だから
- ViewとModelが一対一で対応しない場合があるから
このあたり色々あるんですが、まあざっくり入門なのでこの辺で。とりあえずViewModelを介さないView<->Model間の直接通信はよろしくないですよ、という話です。
具体例
今回はちょっと文字ばっかりでつまんない感じになりそうなので具体例を交えつつ考えていきましょう。というわけでTwitterクライアントを作るとします。作るのです。作りましょう。以後、各通信部分の説明時にTwitterクライアントの開発ではどうなるかを触れます。具体例なのでもちろんコードも登場します。この記事が読み終わる頃にはWPFでTwitterクライアントが作れます。多分。
では改めてTwitterクライアントの作成を前提としてMVVMについて考えてみましょう。Twitterクライアントにおいて、Viewとしてはタイムライン、つまりひたすらツイートが流れてくるウィンドウが考えられます。普通にListViewで流しても良いですし、なんなら独自コントロールとか作って頑張っちゃっても良いです。
次に、Modelはツイートの取得とか、ユーザーの認証とかそういうのです。ざっくり言えば、描画に関係しない部分がModelです。先程も述べたように、描画に関係しないだけです。見た目に関係する情報をModelが持つことは特に問題ありません。例えば、ツイート同士の間隔を何px空けるか、みたいな設定が可能なクライアントの場合、この情報はModelが持つのが普通だと思います。この情報は見た目に関係しますが、描画には直接関係しません。つまり、この情報を持ってして、Modelが直接drawしたりとかはしていません。してはいけません。
最後にViewModelですが、今回の場合は単純な橋渡し役ですね。認証用Modelやらツイート取得用Modelから情報を引き出し、せっせとタイムライン表示用Viewにツイートを流します。重要なのは、ViewModelはそれ以外のことをすべきでない、ということです。これはMVCでControllerが太るFat Controller問題とよく似ています。
ではまとめましょう。今回のTwitterクライアントは以下のクラス群からなります。
クラス | 種類 | 用途 |
---|---|---|
TimeLineWindow | View | タイムラインを表示するウィンドウ |
ConfigWindow | View | 認証とか設定とかのウィンドウ |
TimeLineViewModel | ViewModel | TimeLineWindowとTweet/Authorizerの間でひた走る |
ConfigViewModel | ViewModel | ConfigViewとAuthorizerの間でひた走る |
Authorizer | Model | OAuth認証したり、keyを保存したり |
Model | keyを元にツイートやユーザー情報を引っ張ってくる |
ちなみに、LivetをVSプラグインでインストールしている場合、View, ViewModel, Model作成用テンプレートが用意されています。ソリューションエクスプローラーからフォルダを右クリックし、追加->新しい項目から選べます。
また、ViewModel名ですが、各Viewはデフォルトで「View名 + ViewModel」クラスをViewModelだと認識しています。が、ちょっと冗長なのでこの記事では名前を変えてます。ので、例えばTimeLineWindowの場合だとTimeLineWindow.xaml
を開き
<Window.DataContext>
<vm:TimeLineWindowViewModel/>
</Window.DataContext>
の部分を以下のように書き換えてください。
<Window.DataContext>
<vm:TimeLineViewModel/>
</Window.DataContext>
まあめんどい場合はViewModelをそれぞれTimeLineWindowViewModel、ConfigWindowViewModelで作ると良いと思います。
ViewModel->Model
ViewModelは直接Modelを所有するので、まあメソッドなりなんなり呼べば良いと思います。Viewでの入力をModelに伝える事が主な感じになるかと思います。ここは特に言うことないです。
で、Twitterクライアントの場合だと、まず認証が必要なのでTimeLineViewModelがAuthorizerに認証済みかどうかを聞かなければなりません。これはもう普通に聞くだけです。こんな感じでしょうか。
class TimeLineViewModel : ViewModel
{
Authorizer authorizer = new Authorizer();
public void Initialize()
{
if (!authorize.IsAuthorized)
{
// TODO 認証の必要がある!
}
}
}
ViewModel<-Model
お次は逆パターンです。
これもViewModelがModelを所有しているのでイベントなり何なりでやれば良いだけです。ViewModel-Model間はWPF特有のものではなく、プレーンなC#です。
で、動作を通知したい場合はイベントを使えば良いんですが、データを通知したい場合はより簡単な方法があります。View<->ViewModelの時に使用したものと同じです。lpropn
でTABを2回です。
とりあえずは自身のツイート数とかを定義してみましょう。
class Tweet : Model
{
#region TweetCount変更通知プロパティ
private int _TweetCount;
public int TweetCount
{
get
{ return _TweetCount; }
set
{
if (_TweetCount == value)
return;
_TweetCount = value;
RaisePropertyChanged();
}
}
#endregion
}
このように、ModelからViewModelに通知したい場合も変更通知プロパティを使います。ただし、今回はViewModel側がこの変更を陽に受けるコードを書く必要があります。こんな感じに書きます。あ、今回の説明で必要なケースだけしか書いてないでの、さきほど追加したauthorize.IsAuthorized
部分が消えてますが、ここに載っけてないだけでちゃんとあります。今後も割りとこんな感じです。
using Livet.EventListeners;
class TimeLineViewModel
{
Twitter twitter = new Twitter();
PropertyChangedEventListener listener;
public void Initialize()
{
listener = new PropertyChangedEventListener(twitter) {
() => twitter.TweetCount, (_, __) => RaisePropertyChanged(() => TweetCount)
};
}
public int TweetCount { get { return twitter.TweetCount; } }
}
PropertyChangedEventListener
でModelの変更を受け、そのままViewModelで定義したTweetCount変更通知プロパティに流します。これだけだとなんの意味もない、ただのまどろっこしいコードに見えますね。規模が小さいのでイマイチメリットがないですが、規模を大きくしてもこの考えのままスケール出来るのが利点です。まあとりあえず最初はこんなもんだと思ってください。
えー、Twitter
クラスなのに肝心のツイートを変更通知プロパティにしていないのには訳があります。ツイートは、まあ、タイムラインに流すわけなので、複数必要です。つまりList<string>
辺りで管理します。変更通知プロパティはプロパティへのsetterがトリガーになるだけで、そのリストのAdd
が呼ばれたいかどうかは関与しません。というわけで困りました。こういう時のために、ObservableCollection<T>
があります。これはAdd
されたタイミングで通知されます。というわけで、以下のように書きます。
using System.Collections.ObjectModel;
class Twitter : Model
{
readonly ObservableCollection<string> tweets = new ObservableCollection<string>();
public ObservableCollection<string> Tweets { get { return tweets; } }
}
次にTimeLineViewModel
で以下のように受けます。
using System.Collections.ObjectModel;
class TimeLineViewModel.cs
{
Twitter twitter = new Twitter();
public ObservableCollection<string> Tweets { get { return twitter.Tweets; } }
}
ここまでで一度動かしてみましょう。取り敢えず認証部分はそのままにしておき、Tweets
プロパティにツイートが追加されたらTimeLineWindowにツイートが流れるようにしましょう。
TimeLineWindow.xaml
を開き、ツールボックスからLabel
とListView
を貼っつけます。XAML編集ウィンドウで各々<Label Content="{Binding TweetCount}" ...
、<ListView ItemsSource="{Binding Tweets}" ...
とします。データバインディングが上手く行かない場合はCtrl+B
でビルドしましょう。前回述べたとおり、XAMLは裏でコンパイルしているので、VSが解釈しているコードのタイミングがズレたりする場合があります。よくわかんない感じになったらビルドして、それでもダメならVSを再起動しましょう。WPF道です。
さて、ListViewも貼れたことですし、TimeLineViewModel
のInitialize
メソッドでtwitter.TweetCount = 100;
とかtwitter.Tweets.Add("いぇーい");
とかやってみましょう。どうでしょうか、予想通りな結果になったかと思います。。しかし、ちょっと時代を感じさせられるUIですね。WPF初登場が8年前ですからね。しょうがないですね。
ここまでのコードをGistの方で貼っつけておきました。後は実際に認証し、Tweetsプロパティにツイートを流せば晴れて立派なTwitterクライアントです。そう言い張りましょう。
続き
長くなったのでView<-ViewModelとView->ViewModelは次回です。あ、あと認証部分含めちゃんとTwitterクライアントとして使えるコードを次回か次々回くらいで貼っつけるつもりです。でもあんまりWPF関係ない部分なのでさらっと流すと思います。
正直変更通知プロパティ周りがどうなってるのかいまいち釈然としないかもしれませんが、今は取り敢えずやり方を覚えましょう。後ほど仕組みについても解説を挟むかもしれません。
というかLivetあんまり絡んでないですね。多分次回は活躍します。Blend SDKとかももっと活躍して頂きたい感じですね。まあまったりやっていきます。