LCGTypeBuilderでINotifyPropertyChangedを自動実装するコードを書いてみた
http://d.hatena.ne.jp/okazuki/20110116/1295166605 を見てたら「それLCGTypeBuilderでできるよ!」と言いたくなったので、INotifyPropertyChangedをTypeBuilderLCG(Lightweight Code Gen)で実装するコードを書いてみました。
最終的に次のコードでINotifyPropertyChangedが実装できるようになります。
// publicクラスじゃないとダメ public class MyViewModel : ViewModelBase { // newで作ったインスタンスではPropertyChanged呼び出されないのでnew禁止 protected MyViewModel() { } // Notify属性を付けるとPropertyChangedイベントが呼び出される [Notify] public virtual string Memory { get; set; } }
で、このクラスはnewではなく、下で説明するViewModelFactoryでインスタンス化します。
public class Program { static void Main(string[] args) { // インスタンスはViewModelFactory.Create()で作る var m = ViewModelFactory.Create<MyViewModel>(); m.PropertyChanged += (sender, e) => Console.WriteLine("あの日の {0} は失われてしまいました!!", e.PropertyName); m.Memory = "ハヤシライス"; } }
newが使えなくなるのが難点ですが、コードはとてもシンプル。
以下はViewModelFactoryの実装です。外部ライブラリには依存していないので、そのままコピー&ペーストして利用可能。.NET Framework 4 と Silverlight 4 で動作確認をしています。
using System; using System.Linq; using System.Reflection.Emit; using System.Reflection; using System.ComponentModel; namespace FrozenLib { public static class ViewModelFactory { public static T Create<T>() where T : IRaiseNotifyPropertyChangedEvent { return ViewModelFactoryHelper<T>.Create(); } } public interface IRaiseNotifyPropertyChangedEvent { void RaisePropertyChangedEvent(string name); } public class ViewModelBase : INotifyPropertyChanged, IRaiseNotifyPropertyChangedEvent { public event PropertyChangedEventHandler PropertyChanged; protected void RaisePropertyChangedEvent(string name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); } void IRaiseNotifyPropertyChangedEvent.RaisePropertyChangedEvent(string name) { RaisePropertyChangedEvent(name); } } static class ViewModelFactoryHelper<T> where T : IRaiseNotifyPropertyChangedEvent { public static T Create() { return (T)Activator.CreateInstance(type); } static readonly Type type; static ViewModelFactoryHelper() { type = new ViewModelFactoryHelper(typeof(T)).CreateType(); } } class ViewModelFactoryHelper { public ViewModelFactoryHelper(Type type) { this.type = type; var typeName = typeof(ViewModelFactoryHelper) + ".DynamicType_" + type.FullName; this.builder = module.DefineType(typeName, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Sealed, type); } readonly Type type; readonly TypeBuilder builder; public Type CreateType() { var flags = BindingFlags.Public | BindingFlags.Instance; foreach (var p in type.GetProperties(flags)) { if (!p.IsDefined(typeof(NotifyAttribute), true)) continue; var setMethod = p.GetSetMethod(true); var setMethodOverride = builder.DefineOverrideMethod(setMethod); var g = setMethodOverride.GetILGenerator(); g.Emit(OpCodes.Ldarg_0); g.Emit(OpCodes.Ldarg_1); g.Emit(OpCodes.Call, setMethod); g.Emit(OpCodes.Ldarg_0); g.Emit(OpCodes.Ldstr, p.Name); g.Emit(OpCodes.Callvirt, raisePropertyChangedEventMethod); g.Emit(OpCodes.Ret); builder.DefineMethodOverride(setMethodOverride, setMethod); } return builder.CreateType(); } static ViewModelFactoryHelper() { var assemblyName = new AssemblyName(typeof(ViewModelFactoryHelper).FullName + ".DynamicAssembly"); assembly = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run); module = assembly.DefineDynamicModule(typeof(ViewModelFactoryHelper).FullName + ".DynamicModule.dll"); raisePropertyChangedEventMethod = typeof(IRaiseNotifyPropertyChangedEvent).GetMethod("RaisePropertyChangedEvent"); } static readonly AssemblyBuilder assembly; static readonly ModuleBuilder module; static readonly MethodInfo raisePropertyChangedEventMethod; } [AttributeUsage(AttributeTargets.Property)] public class NotifyAttribute : Attribute { public NotifyAttribute() { } } static class TypeBuilderExt { public static MethodBuilder DefineOverrideMethod(this TypeBuilder type, MethodInfo methodInfoDeclaration) { if (!methodInfoDeclaration.IsVirtual) throw new ArgumentException(string.Format("'{0}' をオーバーライドできません。", methodInfoDeclaration)); var a = (methodInfoDeclaration.Attributes & MethodAttributes.MemberAccessMask) | MethodAttributes.Virtual; var p = methodInfoDeclaration.GetParameters().Select(i => i.ParameterType).ToArray(); return type.DefineMethod(methodInfoDeclaration.Name, a, methodInfoDeclaration.ReturnType, p); } } }
ViewModelFactory.Create
public class DynamicType_MyViewModel : MyViewModel { public override string Memory { get { return base.Memory; } set { base.Memory = value; ((IRaiseNotifyPropertyChangedEvent)this).RaisePropertyChangedEvent("Memory"); } } }
のような型を実行時に作成し、そのインスタンスを返しています。
ちなみに、ViewModelBaseを継承したくない場合は、上で定義したIRaiseNotifyPropertyChangedEventを実装すればOK。
public class ViewModelWithoutBase : IRaiseNotifyPropertyChangedEvent, INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChangedEvent(string name) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(name)); } [Notify] public virtual int MyName { get; set; } }
IRaiseNotifyPropertyChangedEventに関するアイディアは http://d.hatena.ne.jp/zecl/20091211/p1 を参考にしました。
※2011/1/25 使ってるのがLCGじゃない気がしてきたので、文中のLCGをTypeBuilderに置き換え。