かずきのBlog@hatena

すきな言語は C# + XAML の組み合わせ。Azure Functions も好き。最近は Go 言語勉強中。日本マイクロソフトで働いていますが、ここに書いていることは個人的なメモなので会社の公式見解ではありません。

T4 TemplateでViewModelを生成する その2「DSLちっくにする」

前回たたき台を作ったMVVM生成のためのT4 Templateですが正直あんまり楽になった気がしないです。何故かというと、クラスのメタデータの定義が非常に冗長な感じになってたからだと思います。
1プロパティしかないクラスを定義するだけで、20行以上も書かないといけないです。ViewModelを直接定義するよりは楽とは言っても、あんまり嬉しい気がしないです。


ということで、改善してみようと思います。少し前に流行ったような気がする、流れるようなインターフェースを取り入れてみようと思います。あと、NSDefとかClassDefは別アセンブリにするためにクラスライブラリのプロジェクトにコピーしました。

VMDslプロジェクトを作って、この間作ったクラス群をコピーします。あと、NSDefクラスの一部を配列からListを使うように変更しました。ということで、クラス群は以下のようになりました。

namespace VMDsl
{
    using System.Collections.Generic;

    // 名前空間
    public class NSDef
    {
        public NSDef()
        {
            this.Classes = new List<ClassDef>();
        }

        public string NS { get; set; }
        public List<ClassDef> Classes { get; private set; }
    }

    // クラスの定義
    public class ClassDef
    {

        public ClassDef(NSDef ns)
        {
            this.NS = ns;
            this.Properties = new List<PropertyDef>();
            this.NS.Classes.Add(this);
        }

        public NSDef NS { get; private set; }
        public string Name { get; set; }
        public List<PropertyDef> Properties { get; private set; }
    }

    // プロパティの定義
    public class PropertyDef
    {
        public PropertyDef()
        {
            this.Attributes = new List<string>();
        }

        public string Type { get; set; }
        public string Name { get; set; }
        public List<string> Attributes { get; private set; }
    }
}

こいつらを構築するための流れるようなインターフェースを構築していきます。
まず、入り口となるNSDefクラスを生成するファクトリを作ります。こいつは単純なstaticメソッドになります。

// 流れるようなインターフェースの起点
public static class Namespace
{
    public static NSDef Make(string name)
    {
        return new NSDef { NS = name };
    }
}

そして、メソッドチェインを構成するためにNSDefクラスやClassDefクラスを拡張していきます。

// 流れるようなインターフェースのための拡張メソッド
public static class ObjectExtensions
{
    // クラスを追加する
    public static ClassDef Class(this NSDef self, string name)
    {
        var clazz = new ClassDef(self)
        {
            Name = name
        };
        return clazz;
    }

    // プロパティをクラスに追加する
    public static ClassDef Property(this ClassDef self, string type, string name, params string[] attrs)
    {
        var prop = new PropertyDef
        {
            Type = type,
            Name = name
        };
        prop.Attributes.AddRange(attrs);
        self.Properties.Add(prop);
        return self;
    }

    // クラスの定義を終わる
    public static NSDef EndClass(this ClassDef self)
    {
        return self.NS;
    }
}

こいつをビルドして、T4テンプレートを入れていたフォルダにコピーします。そして、ClassDef.ttincludeを削除します。
そして、MyClasses.ttincludeファイルの先頭にこのDLLを読み込むように定義を追加します。

<#@ assembly name="$(ProjectDir)\T4\VMDsl.dll" #>
<#@ import namespace="VMDsl" #>

ここでのポイントは、アセンブリを指定するときにDLLをフルパスで指定しないといけないところをVisual Studio環境変数を使って指定してるところです。
これで、ほかのマシンにプロジェクトを持って行っても問題なく動きます。


そして、クラス定義を先ほど定義した拡張メソッドを使って書き換えます。

var ns = new[]
{
	Namespace.Make("WpfApplication15.ViewModels")
		.Class("PersonViewModel")
			.Property("string", "Name",
				"Required(ErrorMessage=\"だめよ\")")
			.Property("int?", "Age")
		.EndClass()
	
		.Class("HogeViewModel")
			.Property("string", "Foo")
		.EndClass(),
	
	Namespace.Make("WpfApplication15.Hogehoge")
		.Class("BarViewModel")
			.Property("int", "Value")
		.EndClass()
};
#>

見た目だいぶすっきりしました。3クラスも定義してるのに、前回より行数が減ってすっきりしてます。これなら書いてもいいかなって思うかも。


上記の定義で生成されるコードは以下のようになります。大分ましになってきたかな。

// <autogenerated />
namespace WpfApplication15.ViewModels
{
	using System;
	using System.ComponentModel.DataAnnotations;
	public partial class PersonViewModel : WpfApplication15.ViewModelBase
	{
		private string _Name;
		[Required(ErrorMessage="だめよ")]
		public string Name
		{
			get
			{
				return _Name; 
			}
			
			set
			{
				if (Equals(value, _Name))
				{
					return;
				}
				Validator.ValidateProperty(
					value,
					new ValidationContext(this, null, null)
					{
						MemberName = "Name"
					});
			
				_Name = value;
				base.OnPropertyChanged("Name");
			}
		}
		private int? _Age;
		public int? Age
		{
			get
			{
				return _Age; 
			}
			
			set
			{
				if (Equals(value, _Age))
				{
					return;
				}
				Validator.ValidateProperty(
					value,
					new ValidationContext(this, null, null)
					{
						MemberName = "Age"
					});
			
				_Age = value;
				base.OnPropertyChanged("Age");
			}
		}
	}
	public partial class HogeViewModel : WpfApplication15.ViewModelBase
	{
		private string _Foo;
		public string Foo
		{
			get
			{
				return _Foo; 
			}
			
			set
			{
				if (Equals(value, _Foo))
				{
					return;
				}
				Validator.ValidateProperty(
					value,
					new ValidationContext(this, null, null)
					{
						MemberName = "Foo"
					});
			
				_Foo = value;
				base.OnPropertyChanged("Foo");
			}
		}
	}
}
namespace WpfApplication15.Hogehoge
{
	using System;
	using System.ComponentModel.DataAnnotations;
	public partial class BarViewModel : WpfApplication15.ViewModelBase
	{
		private int _Value;
		public int Value
		{
			get
			{
				return _Value; 
			}
			
			set
			{
				if (Equals(value, _Value))
				{
					return;
				}
				Validator.ValidateProperty(
					value,
					new ValidationContext(this, null, null)
					{
						MemberName = "Value"
					});
			
				_Value = value;
				base.OnPropertyChanged("Value");
			}
		}
	}
}

このプロジェクトは↓からダウンロードできます。
T4VMDsl.zip