何回か取り上げてきたModel-View-ViewModelパターンですが、正直な感想としてやっぱりメンドクサイというのがあります。
単調なコードの繰り返しは、大して楽しくないコーディングになってしまうこと間違いなしです。
こういうのは自動生成させるに限る!
コードを自動生成させる方法で、自分がよく使ってる(と言っても自動生成自体させることがあまりないですが…)ものにRubyのERBというものがあります。
前にもBlogの記事として書いたりしました。ただ、この方法の欠点は、Visual Studioとは相性は良くないということです。
Visual Studioと相性のいいテンプレートエンジンというと、前にもちょろっと書いたT4 Templateがあります。折角なので、ちょいと使ってみようと思います。
プロジェクトの作成
ViewModelGeneratorSampleという名前でWpfアプリケーションのプロジェクトを作成します。
後で、T4 Template関連のファイルを入れるためのフォルダとしてCodeGeneratorというフォルダを作っておきます。
ViewModelの基本クラスの作成
自動生成させるにしても、全部自動生成させるのは、イマイチです。
共通的な機能なんかは、きちんと基本クラスを作ってそこに置いて、それを継承する形のクラスを自動生成するほうがスマートで、テストもやりやすいです。
ということで、まずViewModelの基本クラスを作ります。名前は安直にWankuma.Kazuki.Wpf.ViewModelBaseにします。
ViewModelは、IDataErrorInfoインターフェースとINotifyPropertyChangedインターフェースの2つのインターフェースを実装します。
このインターフェースの実装をViewModelBaseで行います。
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
namespace Wankuma.Kazuki.Wpf
{
/// <summary>
/// ViewModelを作成するときの基本クラス
/// </summary>
public class ViewModelBase : INotifyPropertyChanged, IDataErrorInfo
{
#region 検証エラー操作用API
private Dictionary<string, string> _errors = new Dictionary<string, string>();
/// <summary>
/// 指定したプロパティにエラー情報をセットする
/// </summary>
/// <param name="propertyName"></param>
/// <param name="error"></param>
protected void SetError(string propertyName, string error)
{
_errors[propertyName] = error;
}
/// <summary>
/// 指定したプロパティのエラー情報を消去する
/// </summary>
/// <param name="propertyName"></param>
protected void ClearError(string propertyName)
{
if (!_errors.ContainsKey(propertyName))
{
return;
}
_errors.Remove(propertyName);
}
/// <summary>
/// 指定したプロパティのエラーを取得する
/// </summary>
/// <param name="propertyName"></param>
/// <returns></returns>
protected string GetError(string propertyName)
{
string error = null;
_errors.TryGetValue(propertyName, out error);
return error;
}
/// <summary>
/// 現在エラーがあるプロパティ名の配列を取得する
/// </summary>
/// <returns></returns>
protected string[] GetErrorPropertyNames()
{
return _errors.Keys.ToArray();
}
/// <summary>
/// エラーがあるかどうか確認する。エラーがある場合はtrueを返す。
/// </summary>
/// <returns></returns>
public bool HasError()
{
return _errors.Count != 0;
}
public virtual void ValidateAll()
{
// no op
}
#endregion
#region IDataErrorInfo メンバ
public string Error
{
get
{
var sb = new StringBuilder();
foreach (var propertyName in GetErrorPropertyNames())
{
sb.AppendLine(this[propertyName]);
}
return sb.ToString();
}
}
public string this[string propertyName]
{
get { return GetError(propertyName); }
}
#endregion
#region INotifyPropertyChanged メンバ
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
var h = PropertyChanged;
if (h != null)
{
h(this, new PropertyChangedEventArgs(propertyName));
}
}
#endregion
}
}
いつもの繰り返しなので、詳細には解説しませんが、いつもあまり書いてないメソッドとしてValidateAllというメソッドを1つ追加してあります。
こいつを呼ぶと値の検証を全部やってくれるという便利メソッドとして、子クラス側で実装してねという思いがこもってます。
テンプレートの作成
ということで、ついにテンプレートの作成に入ります。
前にも書きましたが、T4 Templateのコードのシンタックスハイライトとかしてくれる便利なものがあるので、それを入れておきます。(無料のCommunity editionね)
http://www.visualt4.com/home.html
まず最初に、CodeGeneratorフォルダに以下の内容で、CodeGenMetadata.ttという名前のテキストファイルを作成します。
ttという拡張子でファイルを作ると自動的に、カスタムツールに「TextTemplatingFileGenerator」が設定されますが、このファイルは自動生成させるものじゃないので、カスタムツールの所を空欄にしておきます。
CodeGenerator/CodeGenMetadata.tt
<#+
// 自動生成するコードのメタデータ
class CodeGenMetadata
{
public CodeGenMetadata()
{
Imports = new List<string>
{
"System",
"System.Collections.Generic",
"Wankuma.Kazuki.Wpf"
};
Properties = new Dictionary<string, string>();
}
// using句
public List<string> Imports { get; set; }
// 名前空間
public string Ns { get; set; }
// クラス名
public string ClassName { get; set; }
// プロパティ Keyがプロパティ名でvalueが型
public Dictionary<string, string> Properties { get; set; }
}
#>
このクラスで、自動生成する対象のクラスのメタデータを表します。
次に、CodeGeneratorフォルダにViewModelGenerator.ttという名前のテキストファイルを作成します。
このファイルも、自動生成させるものじゃないのでカスタムツールは空の状態にします。このファイルは、ViewModelBaseを継承したクラスのテンプレートを作りこんでいきます。
前提条件として、metadataという名前の変数に上で定義したCodeGenMetadataクラスのインスタンスが入っているものとして書いています。
// これは自動生成によって作られたファイルです。
// 変更をしないでください。
// 作成日時: <#= DateTime.Now.ToString("yyyy/MM/dd HH:mm:ss.FFF") #>
<#
// using
foreach(var ns in metadata.Imports)
{
#>
using <#= ns #>;
<#
}
#>
namespace <#= metadata.Ns #>
{
public partial class <#= metadata.ClassName #> : ViewModelBase
{
// 検証メソッド
<#
foreach (var prop in metadata.Properties)
{
#>
partial void Validate<#= prop.Key #>();
<#
} // end foreach
#>
public override void ValidateAll()
{
<#
foreach (var prop in metadata.Properties)
{
#>
Validate<#= prop.Key #>();
<#
} // end foreach
#>
}
// プロパティ
<#
foreach (var prop in metadata.Properties)
{
var fieldName = "_" + prop.Key.ToLower();
#>
private <#= prop.Value #> <#= fieldName #>;
/// <summary>
/// <#= prop.Key #>の取得及び設定を行う。
/// </summary>
public <#= prop.Value #> <#= prop.Key #>
{
get { return <#= fieldName #>; }
set
{
if (Equals(<#= fieldName #>, value)) return;
<#= fieldName #> = value;
Validate<#= prop.Key #>();
OnPropertyChanged("<#= prop.Key #>");
OnPropertyChanged("Error");
OnPropertyChanged("HasError");
}
}
<#
} // end foreach
#>
}
}
ちょっと長いですが、基本的には[Validateプロパティ名]という名前のパーシャルメソッドと、[Validateプロパティ名]メソッドを全部呼ぶ[ValidateAll]メソッドとプロパティを定義しています。
プロパティでは、[Validateプロパティ名]メソッドを呼び出して値の検証をするのと、[OnPropertyChanged]メソッドを呼び出しています。
使い方
最後に、使い方です。
例としてNameプロパティとAgeプロパティを持つPersonViewModelクラスを作ってみようと思います。
PersonViewModel.ttという名前でテキストファイルを作成します。
中身は、以下のような感じです。
<#@ template language="C#v3.5" debug="True" #>
<#@ output extension=".generated.cs" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="CodeGenerator/CodeGenMetadata.tt" #>
<#
var metadata = new CodeGenMetadata
{
};
#>
<#@ include file="CodeGenerator/ViewModelGenerator.tt" #>
この形は変わらないので、何処かに雛形としてとっておくのがいいと思います。
本当はVisual Studioの新規作成で、この形のファイルが作られるのが一番いいのですが…(^^;)
この状態で保存すると、まだ必要なプロパティがCodeGenMetadataクラスに定義されてないのでエラーになってしまいます。
定義する必要があるのは、Nsプロパティ、ClassNameプロパティ、Propertiesプロパティの3つです。
<#@ template language="C#v3.5" debug="True" #>
<#@ output extension=".generated.cs" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ include file="CodeGenerator/CodeGenMetadata.tt" #>
<#
var metadata = new CodeGenMetadata
{
Ns = "ViewModelGeneratorSample",
ClassName = "PersonViewModel",
Properties =
{
{ "Name", "string" },
{ "Age", "string" }
}
};
#>
<#@ include file="CodeGenerator/ViewModelGenerator.tt" #>
これを保存すると、以下のようなコードがPersonViewModel.generated.csという名前で作成されます。
PersonViewModel.generated.cs
// これは自動生成によって作られたファイルです。
// 変更をしないでください。
// 作成日時: 2009/03/22 12:28:47.17
using System;
using System.Collections.Generic;
using Wankuma.Kazuki.Wpf;
namespace ViewModelGeneratorSample
{
public partial class PersonViewModel : ViewModelBase
{
// 検証メソッド
partial void ValidateName();
partial void ValidateAge();
public override void ValidateAll()
{
ValidateName();
ValidateAge();
}
// プロパティ
private string _name;
/// <summary>
/// Nameの取得及び設定を行う。
/// </summary>
public string Name
{
get { return _name; }
set
{
if (Equals(_name, value)) return;
_name = value;
ValidateName();
OnPropertyChanged("Name");
OnPropertyChanged("Error");
OnPropertyChanged("HasError");
}
}
private string _age;
/// <summary>
/// Ageの取得及び設定を行う。
/// </summary>
public string Age
{
get { return _age; }
set
{
if (Equals(_age, value)) return;
_age = value;
ValidateAge();
OnPropertyChanged("Age");
OnPropertyChanged("Error");
OnPropertyChanged("HasError");
}
}
}
}
次に、PersonViewModelというクラスを新規作成して、検証ロジック等を足してクラスを完成させます。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace ViewModelGeneratorSample
{
public partial class PersonViewModel
{
// 年齢の検証メソッド
partial void ValidateAge()
{
if (string.IsNullOrEmpty(Age))
{
this.SetError("Age", "年齢を入力してください");
return;
}
int age;
if (!int.TryParse(Age, out age))
{
this.SetError("Age", "年齢は数字で入力してください");
return;
}
if (age < 0 || age > 120)
{
this.SetError("Age", "年齢は0~120の間で入力してください");
return;
}
ClearError("Age");
}
// 名前の検証メソッド
partial void ValidateName()
{
if (string.IsNullOrEmpty(Name))
{
this.SetError("Name", "名前を入力してください");
return;
}
ClearError("Name");
}
}
}
画面も作ってみよう
最後に、PersonViewModelを表示する画面もつくってみます。
手書きにそろそろ飽きてきたので、XAML Power Toysを使って作ります。
XAML Power Toys: http://karlshifflett.wordpress.com/xaml-power-toys/
右クリックメニューからXAML Power Toys→Create Form, ListView or DataGrid For Classを選びます。
下のように設定します。
DataContextにPersonViewModelを設定します。ついでに、エラーメッセージ表示の機能もつけます。
<Window x:Class="ViewModelGeneratorSample.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:ViewModelGeneratorSample">
<Window.DataContext>
<l:PersonViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- XAML Power Toysで作った奴を貼り付ける -->
<TextBlock Grid.Row="1" Foreground="Red" Text="{Binding Error}" />
</Grid>
</Window>
実行してみよう
おもむろに実行してみます。
名前と年齢に適当なもの入力します。
ちゃんと動いてそうです。