かずきのBlog@hatena

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

サービス参照で追加したクラスにIEditableObjectを実装させるT4 Template

Visual Studio 2010でサービス参照を追加したときに作成されるクラスは、INotifyPropertyChangedまで実装してくれてる。
(VS2008のは未確認。実装してたっけか?)

このT4 Template使えばIEditableObjectが動くように出来るかも。
リフレクション使ってるので、速度的にはハードコーディングするのより遅いけど、お手軽なので。
(因みに未実行コードなので、動くかどうかは謎だけど、それっぽいコードが生成されてた)

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Xml.Linq.dll" #>
<#@ assembly name="System.Xml.dll" #>
<#@ assembly name="System.Core.dll" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Xml.Linq" #>
<#@ import namespace="System.Collections.Generic" #>

<# Init(); #>
<#
foreach (var clazz in classes)
{
#>
namespace <#= ns #>.<#= clazz.Namespace #>
{
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Reflection;
    using System.Linq;
    public partial class <#= clazz.Name #> : IEditableObject
    {
        // プロパティと名前のマップ
        private Dictionary<string, PropertyInfo> _propertyCache;
        // 編集開始時点の値を確保しておくためのマップ
        private Dictionary<string, object> _cacheValues = new Dictionary<string, object>();

        // プロパティと名前のマップを取得する
        private Dictionary<string, PropertyInfo> Properties
        {
            get
            {
                // 一応キャッシュ
                if (_propertyCache == null)
                {
                    _propertyCache = GetType().GetProperties().ToDictionary(p => p.Name);
                }
                return _propertyCache;
            }
        }

        #region IEditableObject Members
        // 編集中かどうかのフラグ
        private bool _isEditing;
        public void BeginEdit()
        {
            if (_isEditing) return;
            // プロパティの現在の値をバックアップ
            foreach (var props in Properties)
            {
                _cacheValues[props.Key] = props.Value.GetValue(this, null);
            }
        }

        public void CancelEdit()
        {
            if (!_isEditing) return;
            _isEditing = false;
            // キャンセルなので、編集開始時点の値に書き戻す
            foreach (var props in Properties)
            {
                props.Value.SetValue(
                    this,
                    _cacheValues[props.Key],
                    null);
            }
            _cacheValues.Clear();
        }

        public void EndEdit()
        {
            if (!_isEditing) return;
            _isEditing = false;
            // 編集確定なので、編集開始時点の値は捨てる
            _cacheValues.Clear();
        }

        #endregion
    }
}
<#
}
#>
<#+
// 名前空間だけは指定しておく
string ns = "Okazuki.WPF.TestApp";
// ここに、Service References以下で作成されたクラスの情報が入る
List<ClassInfo> classes = new List<ClassInfo>();
// クラス
class ClassInfo
{
	public string Namespace { get; set; }
	public string Name { get; set; }
}

public void Init()
{
	// テンプレートのあるフォルダの下のService Referencesの下にあるフォルダを取得
	var path = Path.Combine(Path.GetDirectoryName(Host.TemplateFile), "Service References");
	var dirs = Directory.GetDirectories(path);
	foreach (var dir in dirs)
	{
		// xsdファイルに色々定義されてるので、それを取得
		var files = Directory.GetFiles(dir, "*.xsd");
		ProcessFiles(dir, files);
	}
}
void ProcessFiles(string dir, string[] files)
{
	foreach (var file in files)
	{
		// 多分*1.xsdというファイルにWCFのサービスの引数や戻り値で使うクラスが定義されてる
		if (file.EndsWith("1.xsd"))
		{
			ProcessFile(dir, file);
		}
	}
}
void ProcessFile(string dir, string file)
{
	var elm = XElement.Load(file);
	XNamespace xs = "http://www.w3.org/2001/XMLSchema";
	
	classes.AddRange(
		from complexType in elm.Descendants(xs + "complexType")
		where
			// ArrayOfで始まる奴は配列なので見ない
			!complexType.Attribute("name").Value.StartsWith("ArrayOf")
		select
			new ClassInfo
			{
				Namespace = Path.GetFileName(dir),
				Name = complexType.Attribute("name").Value
			});
}
#>

適当な、WCFサービスを作ってサービス参照に追加してみたら、以下のようなコードが生成されました。

namespace Okazuki.WPF.TestApp.EmployeesManagement
{
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Reflection;
    using System.Linq;
    public partial class Employees : IEditableObject
    {
        // プロパティと名前のマップ
        private Dictionary<string, PropertyInfo> _propertyCache;
        // 編集開始時点の値を確保しておくためのマップ
        private Dictionary<string, object> _cacheValues = new Dictionary<string, object>();

        // プロパティと名前のマップを取得する
        private Dictionary<string, PropertyInfo> Properties
        {
            get
            {
                // 一応キャッシュ
                if (_propertyCache == null)
                {
                    _propertyCache = GetType().GetProperties().ToDictionary(p => p.Name);
                }
                return _propertyCache;
            }
        }

        #region IEditableObject Members
        // 編集中かどうかのフラグ
        private bool _isEditing;
        public void BeginEdit()
        {
            if (_isEditing) return;
            // プロパティの現在の値をバックアップ
            foreach (var props in Properties)
            {
                _cacheValues[props.Key] = props.Value.GetValue(this, null);
            }
        }

        public void CancelEdit()
        {
            if (!_isEditing) return;
            _isEditing = false;
            // キャンセルなので、編集開始時点の値に書き戻す
            foreach (var props in Properties)
            {
                props.Value.SetValue(
                    this,
                    _cacheValues[props.Key],
                    null);
            }
            _cacheValues.Clear();
        }

        public void EndEdit()
        {
            if (!_isEditing) return;
            _isEditing = false;
            // 編集確定なので、編集開始時点の値は捨てる
            _cacheValues.Clear();
        }

        #endregion
    }
}
namespace Okazuki.WPF.TestApp.EmployeesManagement
{
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Reflection;
    using System.Linq;
    public partial class Sample : IEditableObject
    {
        // プロパティと名前のマップ
        private Dictionary<string, PropertyInfo> _propertyCache;
        // 編集開始時点の値を確保しておくためのマップ
        private Dictionary<string, object> _cacheValues = new Dictionary<string, object>();

        // プロパティと名前のマップを取得する
        private Dictionary<string, PropertyInfo> Properties
        {
            get
            {
                // 一応キャッシュ
                if (_propertyCache == null)
                {
                    _propertyCache = GetType().GetProperties().ToDictionary(p => p.Name);
                }
                return _propertyCache;
            }
        }

        #region IEditableObject Members
        // 編集中かどうかのフラグ
        private bool _isEditing;
        public void BeginEdit()
        {
            if (_isEditing) return;
            // プロパティの現在の値をバックアップ
            foreach (var props in Properties)
            {
                _cacheValues[props.Key] = props.Value.GetValue(this, null);
            }
        }

        public void CancelEdit()
        {
            if (!_isEditing) return;
            _isEditing = false;
            // キャンセルなので、編集開始時点の値に書き戻す
            foreach (var props in Properties)
            {
                props.Value.SetValue(
                    this,
                    _cacheValues[props.Key],
                    null);
            }
            _cacheValues.Clear();
        }

        public void EndEdit()
        {
            if (!_isEditing) return;
            _isEditing = false;
            // 編集確定なので、編集開始時点の値は捨てる
            _cacheValues.Clear();
        }

        #endregion
    }
}

本当は、Entityというような名前のクラスを作って、それを継承させようと思ったらサービス参照の追加で生成されるコードが、object型を明示的に継承してるので、怒られた。残念。