代码改变世界

通过反射中的 TypeBuilder 来动态实现 INotifyPropertyChanged 接口

2013-03-06 07:49  Nana's Lich  阅读(2462)  评论(2编辑  收藏  举报

如之前的博文所述,MVVM 虽好,但 WPF 自身并没有提供一个可以快速实现 INotifyPropertyChanged 的手段,很多事情都需要自己做,对于常规的应用开发来说这也未免太繁琐了。

虽然存在着 Caller Info Attributes(调用方信息标注)和代码段,但还是显得不够干净利落,于是我就经常想,如果有一种办法可以利用 C# 中像是自动实现属性之类的特性该多好呀,我们就只需要这样一写就行了: 

public string DisplayName { get; set; }

由于有之前研究 ASP.NET MVC 实现原理的经验,我很自然地就想到可以利用 TypeBuilder 来做这种事情。

不过因为研究 ASP.NET MVC 也是很久以前的事情了,很多细节都记不清,于是只好借助 Google 用关键字“ASP.NET MVC TypeBuilder”去搜索,结果很意外地发现第一个链接居然是之前贴在博客上的备忘记录……

基本原理

反射中的 TypeBuilder 可以在运行时动态生成类型,由于是在运行时生成的,就可以根据我们在运行时的各种需要对它进行调整——例如在本文的主题下我们不想为每个 View-Model 都手工编写 INotifyPropertyChanged 接口和每个成员属性发生变动时触发 PropertyChanged 事件的代码,我们就可以先为每个 View-Model 类型都创建不实现 INotifyPropertyChanged 的版本,但每个成员属性都声明为 virtual,最后通过 TypeBuilder 在它们的基础之上进行派生并实现 INotifyPropertyChanged 接口和相关的逻辑。

于是我们的 View-Model 只需要定义成这样就可以了:

public class SampleViewModel
{
    public virtual string DisplayName { get; set; }
}

接下来我们看看怎么用 TypeBuilder 来为这个基础的类填上“肉”。

使用 TypeBuilder 首先需要新建 AssemblyBuilder 实例和 ModuleBuilder 实例——由于这部分并不是本文的重点,所以想知道此处的细节可以参考 ASP.NET MVC 中的相关代码,或者也可以下载本文最末的代码。

有了 ModuleBuilder 实例之后我们就可以用这个实例来产生 TypeBuilder 实例,然后使其“继承” INotifyPropertyChanged 接口——当然,接口上所具备的事件也是必要去实现的,以反射的角度来看事件其实是一个比较特殊的属性,它的背后也有对应的字段( 因为篇幅的关系,省略了部分没有很密切关系的代码,后面若有省略则不再另外说明):

            typeBuilder.AddInterfaceImplementation(smINotifyPropertyChangedType);

            var fieldBuilder = typeBuilder.DefineField(eventName, smPropertyChangedEventHandler, FieldAttributes.Private);
            var builder = typeBuilder.DefineEvent(eventName, EventAttributes.None, smPropertyChangedEventHandler);

而对事件的 Accessor 的具体实现,当然也是要通过 Emit 调用 Delegate 类型的 Combine 和 Remove 来做的:

            var il = builder.GetILGenerator();

            il.Emit(OpCodes.Ldarg_0);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldfld, eventField);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Call, delegateAction); // 此处 delegateAction 实际上是 Delegate.Combine 或 Delegate.Remove 的 MethodInfo

            il.Emit(OpCodes.Stfld, eventField);

            il.Emit(OpCodes.Ret);

还有一个 RaisePropertyChanged 方法——这个方法是我们自己来定义的,用于暴露给我们将要实现的 Setter,我们用来实现事件的字段是没法直接从派生类型进行访问的,虽然这一点是可以改变的,但我认为可以重用的部分(如从 propertyName 创建 PropertyChangedEventArgs)即使是通过 Emit 产生也不该每处使用都再写一遍,所以还是要写在 RaisePropertyChanged 中的:

            il.Emit(OpCodes.Ldarg_1); // 载入 propertyName
            il.Emit(OpCodes.Newobj, smPropertyChangedEventArgsConstructor);
            il.Emit(OpCodes.Call, smInvokePropertyChangedMethod); // 实际调用 Delegate.Invoke 方法,也就是真正触发事件的时候

把 RaisePropertyChanged 做好之后,就该为具体的每个属性来添加对应的调用了。当然了,RaisePropertyChanged 到底叫什么其实是无所谓的,只不过通常来说会是这个名字。

接下来,如同前面所说,要对属性的实现进行调整,属性本身必须实现为 virtual 的,所以第一步是判断其 setter 是否为 virtual:

            if (setter == null || !setter.IsVirtual)
                /* throw exception */;

确定为 virtual,就可以对它动手了,所添加的代码大体上是这样的:

            var il = builder.GetILGenerator();

            var label = il.DefineLabel();

            il.Emit(OpCodes.Ldarg_1);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Call, getter); // 首先通过 getter 获得现在的值,由于并不知道后端字段的具体名称,所以不可以用直接读取后端字段的办法
            il.Emit(OpCodes.Ceq); // 判断新旧两值是否相等——此处的比较方式其实并不是很严谨,对于引用类型进行的则是引用比较,但目前可以应付绝大部分使用场景
            il.Emit(OpCodes.Brtrue, label); // 若相等则跳出
            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldarg_1);
            il.Emit(OpCodes.Call, setter);

            il.Emit(OpCodes.Ldarg_0);
            il.Emit(OpCodes.Ldstr, property.Name); // 在调用了基类——也就是应用开发人员实际上显式创建或实现的 setter 之后,需要调用 RaisePropertyChanged 来触发事件了
            il.Emit(OpCodes.Call, raisePropertyChangedMethod);

            il.MarkLabel(label);
            il.Emit(OpCodes.Ret);

到这里,通过 Emit 来派生并实现 INotifyPropertyChanged 接口和相关的逻辑的目标基本上就已经达到了。

在应用开发的过程中除了像在本文开头处所提及的需要将属性声明为 virtual 以外,实际创建具体的 View-Model 实例时也应该通过 Activitor 来创建,如:

            var newType = moduleBuilder.MakeComponentType("{Dynamic}.BasicModel", typeof(RaiseModel));
            var model = (BasicModel)Activator.CreateInstance(newType);

这样就会创建出自动实现了 INotifyPropertyChanged 接口及其事件的 View-Model 实例了。

后记

有些时候遇到特定的类型——例如 String 类型时,逻辑上可能会认为应当不仅仅比较引用对象,还应该比较字符串的具体内容,如果这样的话在实现比较的部分或许就需要考虑加入对等比运算符或 Equals 方法的调用了;不过也有很多人在手工实现这部分时并不判断新旧值是否相等。

实际上,在完成了大部分实现之后我发现其实同样的办法已经有人在几年前就去尝试了,但是不知道为什么国内很少有讨论这种办法的,偶尔能找到的也都仅仅是转载代码而已,似乎没有进一步去研究原理的。

所以我在此把大致的原理写出来也希望能调动大家的兴趣、去探索反射还可以怎么样去玩。

当然了,尽管思路上是相似的,在实现的过程中我绝大部分都还是在没有参照物的前提下去实现的,而且我觉得有些细节我做得比网上已经有的方案要好一些,所以我很自豪地把这个成果展示了出来。

相关代码可以在 http://wheel.codeplex.com/SourceControl/changeset/6ecccf1dd44d 找到。