はじめに
皆さんWPFのアプリケーションを書くときに、MVVMを使ってますでしょうか?
MVVMは、アプリケーションの組み方の作法で、デザインパターンのようなものです。
マイクロソフト発の技術ですが、近年では、AngularJSのヒットで、HTMLの世界にもMVVMが浸透してきています。
MVVMは、Model+View+ViewModelという3層構造を定義したデザインパターンですが、最も重要なのは、Bindingで実装されているという点です。
ViewとViewModelの間をBindingで抽象化しつつ繋ぐ考え方が、MVVMの最も重要な技術的特徴です。
この考え方は、素晴らしいのですが、ひとつ問題があります。
ViewModelのPropertyを作成するのが、非常に退屈な作業の繰り返しなのです。
今までは、どうしていたのか?
プログラムの紹介をする前に、今までViewModelのPropertyをどのように作成していたのか振り返ってみたいと思います。
①手入力
最も初歩的な方法です。Bindingさせるには、PropertyChangedイベントを発生させる必要があるためコメントなどを含めると10行を超えるコードを記述する必要があります。ソースコードもVisualStudioでは表示を折りたたむことができるとはいえ、似たようなコードで汚染されてしまい可読性の悪いソースコードが出来上がってしまいます。
②コードスニペット
入力の手間は、少し減りますが、ソースコードの可読性が悪いという問題は解決しません。
③T4テンプレートを使う
T4テンプレートを使って、partialクラスを生成させるところまでは、比較的よく行われている手法です。この手法の良い所は、Propertyが定義されているクラスが、partialクラスで分離されておりソースコードの可読性が良くなる点です。C#の言語仕様をうまく利用しておりともてもクールな手法です。この手法の問題点は、T4テンプレートというDSLを記述する必要がある点です。DSLといってもC#なのですが、テキストテンプレートとして特殊な記述方法を覚える必要があります。partialクラスを生成するライブラリを作成して、面倒臭さを軽減しようと頑張ってみたのですが、完璧に満足するところまでは行きませんでした。
ソリューションを巡回して、partialクラスを自動生成する
色々とネットを巡って私が辿り着いた答えは、クラス属性を付加して、partialクラスを生成するという方法でした。
T4テンプレートには、VisualStudioのソリューションにDTEインタフェースを使ってアクセスするという機能があります。
処理は単純で、ソリューションに登録されているクラスを総チェックして、クラス属性が付いていれば属性のパラメーター値に従って、partialクラスを生成しているだけです。
T4テンプレートは利用しますが、利用者はテンプレートを記述する必要は一切ありませんので、T4テンプレートを覚える必要は全くありません。
クラス属性を記述したらVisualStudioのメニューから「すべてのT4テンプレートの変換」を選択するだけで、partialクラスが自動生成されます。
誰も気が付かなかった方法という訳ではなく、複数のブログで紹介されてもいるのですが、すぐに実行できるサンプルも見つからない状態で、不思議とあまり使われていない開発手法のようです。
私の作成したソースコードとNuGetパッケージを公開いたしますので、是非参考にして頂ければと思います。
Github Sourcecode
NuGet package(テンプレート)
NuGet package(クラスライブラリ)
※テンプレートは、ソリューションのプログラムに1つ用意されていればOKですが、クラスライブラリは属性を利用する全てのプロジェクトで、参照されている必要があります。そのためNuget packageは、テンプレートとクラスライブラリで分割されています。
生成できるコードの種類
私の作成したコードでは下記の機能の生成に対応しています。
* 単純プロパティの生成
* プロパティのsetterをprivateで生成
* プロパティ値更新時に、PropertyChangedイベントを発火するように生成
* プロパティ値更新時に、XXXNotification()メソッドを呼び出すコードを生成
* 依存プロパティの生成
* 依存関係プロパティの生成(UIPropertyMetadata)
* 依存関係プロパティの生成(UIPropertyMetadata)(Callback関数の生成)
* 依存関係プロパティの生成(PropertyChangedCallback)
* コマンドの生成
* コマンドの生成(CanXXXメソッドも生成)
* コマンドの生成(CommandParameterを受け取れるメソッドを生成)
* イベントの生成
* ルーティングイベントの生成
* ロガークラスの生成(log4net使用)
* シングルトンの生成
使用例
namespace ViewModels
{
using Common.Wpf.Attributes;
using Livet;
[TemplateGenerateAnnotation(Name = "Monday", Type = typeof(Livet.ViewModel), Comment = "月曜日", RaisePropertyChanged = true)]
[TemplateGenerateAnnotation(Name = "Tuesday", Type = typeof(Livet.ViewModel), Comment = "火曜日", RaisePropertyChanged = true)]
[TemplateGenerateAnnotation(Name = "Wednesday", Type = typeof(Livet.ViewModel), Comment = "水曜日", RaisePropertyChanged = true)]
[TemplateGenerateAnnotation(Name = "Thursday", Type = typeof(Livet.ViewModel), Comment = "木曜日", RaisePropertyChanged = true)]
[TemplateGenerateAnnotation(Name = "Friday", Type = typeof(Livet.ViewModel), Comment = "金曜日", RaisePropertyChanged = true)]
public partial class WeekletViewModel : ViewModel
{
public WeekletViewModel()
{
this.Monday = new DayletViewModel();
this.Tuesday = new DayletViewModel();
this.Wednesday = new DayletViewModel();
this.Thursday = new DayletViewModel();
this.Friday = new DayletViewModel();
}
}
}
生成後のファイルは下記のような内容になります。
// <auto-generated />
namespace ViewModels
{
using System;
using System.Windows;
using System.Windows.Controls;
using System.ComponentModel;
/// <summary>
/// このクラスはWeekletViewModelクラスの定義によって
/// 2014/08/26 9:51:36に自動生成されました。
/// このファイルをエディタで直接編集しないでください。
/// </summary>
public partial class WeekletViewModel : Livet.ViewModel
{
#region 月曜日
/// <summary>
/// 月曜日
/// </summary>
private Livet.ViewModel _monday;
public Livet.ViewModel Monday
{
get { return this._monday; }
set
{
if ( this._monday == value ) {
return;
}
this._monday = value;
this.RaisePropertyChanged(() => this.Monday);
}
}
#endregion
#region 火曜日
/// <summary>
/// 火曜日
/// </summary>
private Livet.ViewModel _tuesday;
public Livet.ViewModel Tuesday
{
get { return this._tuesday; }
set
{
if ( this._tuesday == value ) {
return;
}
this._tuesday = value;
this.RaisePropertyChanged(() => this.Tuesday);
}
}
#endregion
#region 水曜日
/// <summary>
/// 水曜日
/// </summary>
private Livet.ViewModel _wednesday;
public Livet.ViewModel Wednesday
{
get { return this._wednesday; }
set
{
if ( this._wednesday == value ) {
return;
}
this._wednesday = value;
this.RaisePropertyChanged(() => this.Wednesday);
}
}
#endregion
#region 木曜日
/// <summary>
/// 木曜日
/// </summary>
private Livet.ViewModel _thursday;
public Livet.ViewModel Thursday
{
get { return this._thursday; }
set
{
if ( this._thursday == value ) {
return;
}
this._thursday = value;
this.RaisePropertyChanged(() => this.Thursday);
}
}
#endregion
#region 金曜日
/// <summary>
/// 金曜日
/// </summary>
private Livet.ViewModel _friday;
public Livet.ViewModel Friday
{
get { return this._friday; }
set
{
if ( this._friday == value ) {
return;
}
this._friday = value;
this.RaisePropertyChanged(() => this.Friday);
}
}
#endregion
}
}
最後に
この機能のアイディアは、ViewModelをもっと簡単に作成する方法を求めて、ネットを検索して拾い上げた情報を元にしています。
私が調べた時にはブログのエントリーが数年前と古くサンプルコードが散逸してしまっていて、実行して試すことができない状態でした。今回のソースコードは足りない部分を補完して再実装したものです。
先人の方の有用な情報に感謝いたします。
ソリューションを巡回して、すべてのpartialクラスを再生成し直すという構造なので、実行速度について懸念していたのですが、大きめなソリューションで利用しても数秒で完了するためストレスになることは無いでしょう。
このエントリーの内容が、皆様のプログラム開発の一助になれたら幸いです。