前回MVVMパターンと、それを実装するためのライブラリのMVVM Light Toolkitを紹介した。今回はソースコードを交えつつ解説したいと思う。
今回使用するソースコードは次の場所に公開している。
実行後の画面は下図の通りだ。
リストがあって、詳細ボタンをクリックすると詳細が表示されるといういたって単純な構成にしてある。
プロジェクトの構成は下図の通り。
MainView.xaml
MainView.xamlにFrameが定義してあり、状況によってViewsフォルダ配下のDetailsPage.xamlとListPage.xamlを表示する。MainPage.xamlで注目してほしい箇所は2箇所ある。まずUserControlタグのDataContext属性にキー名LocatorというオブジェクトのMainというプロパティがBindingされている。LocatorはViewModelのインスタンスを管理するViewModelLocatorクラスで、App.xamlでインスタンス化されている。
ViewModelLocator.csは下記の通り。
public class ViewModelLocator{
public static MainViewModel MainStatic
{
get
{
if (_main == null)
{
CreateMain();
}
return _main;
}
}
public MainViewModel Main
{
get
{
return MainStatic;
}
}
public static void ClearMain()
{
_main.Cleanup();
_main = null;
}
public static void CreateMain()
{
if (_main == null)
{
_main = new MainViewModel();
}
}
// 以下、他のViewModel用のPropertyが続く
}
ViewModelごとに上記の記述が連なっていく。
ついで、MainPage.xamlで定義したFrameのSourceにCurrentPageがBindingされている。CurrentPageプロパティが定義されているのはMainViewModelクラスになる。MainViewModel.csのコードは下記になる。
public class MainViewModel : ViewModelBase
{
public MainViewModel()
{
CurrentPage = new Uri("/Views/ListPage.xaml", UriKind.Relative);
Messenger.Default.Register(this, x =>
{
switch(x.Notification)
{
case Notifications.ToList:
CurrentPage = new Uri("/Views/ListPage.xaml", UriKind.Relative);
break;
case Notifications.ToDetails:
CurrentPage = new Uri("/Views/DetailsPage.xaml", UriKind.Relative);
break;
}
});
}
Uri _currentPage;
public Uri CurrentPage {
get { return _currentPage; }
set
{
_currentPage = value;
base.RaisePropertyChanged("CurrentPage");
}
}
}
MainViewModelはCurrentPageを変更することによりページの遷移の制御を行っている。MessengerはMVVM Light Toolkitが提供してくれる通信用のクラスだ。Messengerはデフォルトで1つのインスタンスを提供してくれるので、ViewModelのBroadCastのテスト用途などで指定のMessengerを使いたい場合など以外はMessenger.Defaultプロパティを使えば良いだろう。Register関数でライブラリ標準のNotificationMessageを受け取るように指定している。今回はNotificationMessageに遷移先を渡すようにしているので、Registerを呼び出した際のコールバック内でNotificationを参照してCurrentPageを変更する。
NotificationMessage.Notificationは文字列だが、NotificationMessageにはSenderとTargetも渡せるようになっているので、Registerハンドラーの中でNotificationの文字列比較だけでなく、Sender, Targetも使用してどこに対してのメッセージなのかを識別するようにしたほうが良いだろう。というのも、MessengerのSendとRegisterはGenericを使用してNotificationMessage以外にも任意の型を指定して簡単にメッセージを発信、受信することも可能だが、実際、データ型を指定してしまうとコードからどこの誰に対するメッセージなのかまったく識別できなくなってしまうし、また、メッセージ毎にメッセージ用のクラスを用意すると、それはそれで作りすぎるとかなりカオスな状態を引き起こしてしまう。そのため、Messengerの利用はプロジェクトの規模が大きくなればなるほど計画的に使用するべきだろう。
標準のメッセージクラスは以下の通り。
MessageBase
GenericMessage<T>
NotificationMessage
NotificationMessage<T>
NotificationMessageAction
NotificationMessageAction<T>
DialogMessage
PropertyChangedMessage<T>
ListPage.xaml
ListPage.xamlもMainPage.xamlと同様にDataContextへViewModelLocatorを介してListPageViewModelクラスのBindingを行っている。リストの表示に選択機能は必要ないのでItemsControlでListPageViewModelのBushiListプロパティを単純に表示している。
ListPage.xamlは下記の通り。
<ItemsControl ItemsSource="{Binding BushiList}" >
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal"
Margin="10">
<Button Content="詳細"
Margin="0 0 10 0"
CommandParameter="{Binding}">
<local:BindingHelper.Binding>
<local:RelativeSourceBinding Path="ShowDetailsCommand" TargetProperty="Command" />
</local:BindingHelper.Binding>
</Button>
<TextBlock Text="{Binding Name}"
VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
ListPage.xaml.csは下記の通り。
public class ListPageViewModel : ViewModelBase
{
public ListPageViewModel()
{
if (IsInDesignMode)
{
BushiList = new ObservableCollection<Bushi> {
new Bushi{ Name = "Sample1", Age = 10, Address = "Tokyo" },
new Bushi{ Name = "Sample2", Age = 20, Address = "Kyoto" },
new Bushi{ Name = "Sample3", Age = 30, Address = "Osaka" }
};
}
else
{
BushiList = new ObservableCollection<Bushi> {
new Bushi{ Name = "織田 信長", Age = 49, Address = "Kyo-" },
new Bushi{ Name = "羽柴 秀吉", Age = 46, Address = "Himeji" },
new Bushi{ Name = "徳川 家康", Age = 40, Address = "Mikawa" },
new Bushi{ Name = "明智 光秀", Age = 55, Address = "Tanpa" }
};
}
ShowDetailsCommand = new RelayCommand<Bushi>(x =>
{
Messenger.Default.Send<Bushi>(x);
Messenger.Default.Send<NotificationMessage>(new NotificationMessage(Notifications.ToDetails));
});
}
public ObservableCollection<Bushi> BushiList { get; set; }
public RelayCommand<Bushi> ShowDetailsCommand { get; private set; }
}
BushiListは名前の通りBushiクラスのリストだ。それをItemsControlのItemsSourceとしている。注目して欲しいのはDataTemplate内のButtonの定義だ。実はSilverlight4からButtonBaseクラスはCommandを実装するようになったので、ButtonBaseから派生しているButtonは本来ならばCommand="{Binding ShowDetailsCommand}"といったような記述が可能なのだが、今回はItensControlのDataTemplate内ということで、個々のアイテムのDataContextはBushiになってしまう。そのため単純に前述の定義を行ってもListPageViewModel内で定義されたShowDetailsCommandプロパティは見つからない。SL4ではRelativeSourceがWPFほど強力ではないので、それを補うため今回は下記のブログポストで見つけたBindingHelperクラスを使用している。
RelativeSource Binding with FindAncestor mode in Silverlight
CommandParameterにBindingのみを指定することで個々のアイテムのDataContextであるBushiを渡している。これを受けるのはPateListViewModelのShowDetailsCommandだ。ShowDetailsCommandはRelayCommandのGenericを使用してBushiクラスを受け取るようになっている。ShowDetailsCommand内で、BushiをMessengerでDetailsPageViewModelクラスへと発信している(実際にはTargetを設定していないのでBushiをRegisterしている対象へとばらまいている)。ついでNotificationMessageも発信して詳細ページへの遷移を促している。
一点注目して欲しいのがIsInDesignModeだ。このフラグを使用してデザインモード時に仮のデータを渡してやることにより、アプリケーションを実際に動作させてWCF経由などでデータを取得、画面に表示させなくともExpression BlendやVisual StudioのXamlビューで実際に動作させた場合と同等の画面確認が行えるようになっている。これを容易に可能にするのがMVVMパターンを使うことによってもたらされる恩恵の一つでもあり、Expression Blendとの親和性が高いことからBlendabilityが高いとも表現される。
DetailsPage.xaml
DetailsPage.xamlはGridタグでListPage.xamlで選択されたBushiの詳細を受け取り表示するだけである。また戻るボタン押下時にListPage.xamlへの遷移をNotificationMessageを発信することによりMainViewModelへと通知している。DetailsPage.xaml, DetailsPageViewModel.csの詳細はここまで説明してきたこととほぼ同じなので省略する。
まとめ
前回と今回で、MVVMパターン開発手法のMVVM Light Toolkit版を解説してきた。MVVM Light Toolkitは大きなライブラリではないけれど、MVVMパターンに必要な要素はすべて盛り込まれているので、MVVM Light Toolkitを使えばすぐにでもMVVMパターンで開発を行えるのが理解できたと思うし、容易に使える実感を持てたのではないだろうか。
この解説を通して、MVVMパターンを使用してUIとビジネスロジックの切り離しを明確にすることにより、ビジネスロジックの単体テストが容易になり、それにあわせてアプリケーション全体の保守性もあがり、またデザイナとの共同作業を行う上で重要なExpression Blendとの親和性も高まる、ということが伝わったら幸いだ。