方面示例 (INotifyPropertyChanged 通过 Aspect)






4.99/5 (77投票s)
探讨不同的面向切面编程框架。
引言
本文将全面介绍面向切面编程 (AOP)。
以下是一些关于 AOP 的技术术语,这些术语来自一些比我更擅长表达的来源。
面向切面编程 (Aspect-oriented programming) 涉及将程序逻辑分解为不同的部分(所谓的关注点,功能内聚区域)。所有编程范式都支持一定程度的将关注点分组和封装到独立的、不可分割的实体中,通过提供可用于实现、抽象和组合这些关注点的抽象(例如,过程、模块、类、方法)。但是,有些关注点难以通过这些形式来实现,它们被称为横切关注点,因为它们“横跨”了程序中的多个抽象。日志记录就是一个横切关注点的例子,因为日志记录策略必然会影响系统中每个被记录的部分。日志记录因此横跨所有被记录的类和方法。
所有 AOP 实现都包含一些横切表达式,用于将每个关注点封装在一个地方。实现之间的区别在于所提供构造函数的强大功能、安全性和可用性。例如,指定要拦截的方法的拦截器表达了有限形式的横切,对类型安全或调试的支持不多。
-- http://en.wikipedia.org/wiki/Aspect-oriented_programming
面向切面软件开发 (Aspect-Oriented Software Development, AOSD),有时简称为面向切面编程 (AOP),是一种新的软件设计方法,它解决了结构化编程和面向对象编程 (OOP) 等其他方法难以处理的模块化问题。AOSD 对这些方法是补充,但并非替代。当今典型的企业和互联网应用程序必须解决“关注点”,如安全性、事务行为、日志记录等。提供这些服务的子系统可以以模块化的方式实现。然而,要使用这些服务,您必须在应用程序的其他各个地方插入相同的“样板”代码片段来调用这些服务。这违反了“不要重复自己”(DRY) 原则,并损害了整体系统的模块化,因为相同的调用代码分散在整个应用程序中。例如,如果您想控制对应用程序中某些服务的访问,可以在每个需要此控制的方法开头插入授权检查的样板代码。由于这种冗余的样板代码出现在代码的许多地方,因此如果将来有必要修改或替换这种安全方法,将会变得困难且容易出错。此外,您的应用程序代码现在与安全(以及可能其他)关注点的代码混在一起,这既损害了清晰度,又使得在其他上下文中重用您的代码变得困难,因为您将不得不携带相同的安全方法,这可能不合适。因为诸如安全之类的关注点通常会横跨多个应用程序模块边界(例如,类)。我们称它们为横切关注点。请注意,模块化的损失发生在不同关注点的交叉处。AOP 通过将横切关注点(或方面)孤立地开发,然后使用声明式或命令式机制将它们与其他模块结合起来,从而恢复模块化。也就是说,交叉点只定义一次,在一个地方,从而易于理解和维护。其他模块无需修改即可被方面建议。这个“交叉”过程,有时称为织合,可以在构建时或运行时发生。AOSD 织合是一项关键创新,它提供了非常精细的查询和组合语义。传统的代码链接能够解析方法和变量名,而织合增加了用新实现替换方法体的能力,在方法调用之前或之后插入代码,检测变量的读写,甚至将新的状态和行为与现有类关联起来,通常用于添加“混入”行为。
-- 什么是面向切面软件开发?
市面上的不同类型 Aspect 框架
在 .NET 中进行面向切面编程(以下简称 AOP)时,有几种不同的选择。这些选项/框架大致可分为两类:
基于代理的 AOP 框架
你们中的一些人(甚至很多人)可能听说过依赖注入,甚至使用过 IOC/依赖注入 容器,如 Castle/Unity/StructureMap/Spring。
恰好,一些 IOC/依赖注入 容器经常使用代理来包装存储在 IOC/依赖注入 容器中的真实实现(源对象)。例如,Castle 使用一个名为 DynamicProxy
的类,而 Unity 提供 TransparentProxy
和 VirtualMethodProxy
对象。既然我们知道有些代理在使用(至少在某些 IOC/依赖注入 容器中),那么就不难想象我们可以使用代理来拦截对真实对象的调用,其中虚拟方法/属性(本质上也是方法)可以在代理调用真实对象之前被代理拦截。用户可以自由地创建任何他们想要的拦截代码类型,这通常通过实现特定接口或继承特定类等方式来实现。
此图有助于说明基于代理的 AOP 如何工作。
使用这种 AOP 框架的问题在于,您被迫使用 DI/IOC 模式,而这可能不是您想要的,仅仅是为了允许 AOP。这对开发者来说意味着,您希望使用 AOP 的任何类型都必须存在于 IOC/依赖注入 容器中,并且必须从中解析。这强迫您采用一种特定的编程方式,而正如我所说的,您可能不想要这种方式。基于代理的 AOP 框架的另一个问题是,它们使用代理通常意味着您希望拦截的任何方法/属性都必须标记为 virtual
。现在,您可能可以接受或无法接受这一点,但这确实会带来一些危险,即其他代码可能会错误地覆盖这些方法/属性,因为它们是虚拟的,这可能会向不熟悉整体情况的人暗示一个扩展点。
最后一点是,基于代理的 AOP 框架似乎无法处理的一个问题是,它们无法围绕后备字段或静态类型、方法和属性引入方面。
IL 织合基于的 AOP 框架
IL 织合是一个有趣的新技术,它在过去一两年里才变得相当主流(至少在我看来是这样)。那么,我所说的 IL 织合是什么呢?
好吧,我们都知道 .NET 代码的正常工作流程是这样的,对吧?
现在,让我们考虑以下图表,它说明了基于 IL 织合的 AOP 框架中发生的情况。
本质上,发生的事情是,您现有的代码库通过实现特殊接口或继承特定基类来扩展,您可以在其中输入新代码。然后在编译时,会获取您在新接口实现/类中输入的新代码,获取原始代码和这些新代码的 IL,并将其直接写入程序集中,而不是原始 IL 代码。
纯粹的 IL 织合使用一个不太为人所知的 DLL,称为 Mono.Cecil,它是 Mono 项目的一部分,可以 100% 实现。尽管 IL 织合非常高级,但我还是 urge 你们所有人都去研究一下,因为我选择研究的现有 AOP 框架中至少有一个在内部使用了 Mono.Cecil。我将在讨论依赖于 Mono.Cecil 的 AOP 框架时详细介绍 Mono.Cecil 的工作原理。
这类代码的主要问题是原始代码的工作流程不再清晰,但话说回来,这对于基于代理的 AOP 框架也是如此。基于 IL 织合的框架的优势在于它们不使用代理,它们会直接写入新的 IL,因此它们根本不需要任何拦截方法/属性是虚拟的。这是因为 IL 织合框架将直接获取原始方法的 IL,并在其前面或后面添加代码,或者可能用新的 IL 全部替换它。基于 IL 织合的 AOP 框架的另一个优点(好吧,有些比其他更高级)是,您甚至可以引入新的成员、字段、事件等,并且没有问题与静态类型一起工作,它们毕竟只是类型,因此也有 IL。
我们打算实现什么
好的,现在您对 AOP 有了基本了解,以及一些现有的框架如何为我们 .NET 开发者提供编写自己的方面的工具,让我们简要谈谈附加的演示应用程序做了什么。
我提供了四个不同的 AOP 框架演示;在可能的情况下,我试图让它们都做同样的事情;在所有情况下都不可能,但通常已经实现了。
那么实现了什么呢?
正如你们中的一些人可能知道的,我非常热衷于 WPF 开发,因此,有一个接口我比其他任何接口都更经常实现。这个接口是 System.ComponentModel.INotifyPropertyChanged
接口(以下简称 INPC),它看起来像这样:
namespace System.ComponentModel
{
// Summary:
// Notifies clients that a property value has changed.
public interface INotifyPropertyChanged
{
// Summary:
// Occurs when a property value changes.
event PropertyChangedEventHandler PropertyChanged;
}
}
通常实现方式如下:
public class MainWindowViewModel : INotifyPropertyChanged
{
private int someProperty;
public int SomeProperty
{
get { return someProperty; }
set
{
someProperty=value;
RaisePropertyChanged("SomeProperty");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
现在,这个接口依赖于传递到 RaisePropertyChanged
方法的魔术字符串。正如我们所知,字符串是不可重构的。事实上,INPC 接口的使用非常普遍,以至于许多开发者(包括我自己)都想出了不同的方法来摆脱这个魔术字符串。解决方案范围从使用 T4 模板,到使用表达式树,再到使用 StackFrame
s,相信我,我见过不少实现。
问题是,对于所有这些方法,您仍然需要做一些工作。这让我想,如果我们能用一个属性来标记一个自动属性,告诉它是否是一个 INPC 属性,那就更好了,比如这样:
public class MainWindowViewModel : INotifyPropertyChanged
{
[INPCAttribute]
public virtual string DummyProp1 { get; set; }
[INPCAttribute]
public virtual string DummyProp2 { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
这让我思考得更多。好吧,如果我能用一些面向切面编程来触发 INPC PropertyChanged
事件,也许我甚至可以写一个方面来自动为我实现 System.ComponentModel.INotifyPropertyChanged
接口,这将导致如下代码:
public class DummyModel
{
[AddINPCAttribute]
public virtual string DummyModelProp1 { get; set; }
[AddINPCAttribute]
public virtual string DummyModelProp2 { get; set; }
public virtual string DummyModelProp3 { get; set; }
}
好的,这并不适合所有属性,有时您希望手动控制何时触发 INPC 事件,或者在属性的 setter 中添加更多代码,但对于 80% 的情况,自动触发 INPC 事件可能已经足够了。
在很大程度上,我已经非常成功地做到了这一点,这就是本文其余部分将要讨论的内容。
然而,在我们继续之前,我想回顾一下我之前提到的一点,即我试图让所有不同的 AOP 框架做同样的事情。好吧,我试过了,但在某些情况下,我要么缺乏动力,要么缺乏技能。我已经选择评估了四个 AOP 框架,它们以及我用它们所做的,显示在下表中:
AOP 框架 | 我取得的成就 |
Castle |
|
Unity |
|
PostSharp (非免费) |
|
LinFu.AOP * |
|
* 依赖于 Mono.Cecil
深入研究演示代码示例
下一节将概述我选择研究的各种 AOP 框架如何实现我上面表格中描述的功能。
Castle (基于代理)
价格 | 使用的版本 | 可从以下网址获取 |
免费 | 2.5.0.0 | http://www.castleproject.org/ |
Castle 是一个 IOC/依赖注入 容器。现在回想一下我使用 Castle 实现了什么。
目标 1
创建了一个属性目标属性,当应用于已实现 INotifyPropertyChanged
的类时,它将在属性设置时调用 INotifyPropertyChanged
的 PropertyChanged
事件。
目标 2
创建了一个属性目标属性,当应用于未实现 INotifyPropertyChanged
的类时,它将自动实现 INotifyPropertyChanged
接口,并在设置具有该属性的属性时调用 PropertyChanged
事件。
现在,让我们看看我们是如何实现这两个目标的,从目标 1 开始。
目标 1,步骤 1:创建 INPCAttribute
第一步很简单;我们只需创建一个标准的 .NET 属性,如下所示:
[AttributeUsage(AttributeTargets.Property)]
public class INPCAttribute : Attribute
{
}
可以看出,这段代码并没有太多功能;它只是一个标记,我们将用它来标记需要在使用时调用 INPC PropertyChanged
事件的属性。
目标 1,步骤 2:创建 INPC IInterceptor
在 Castle 中,方法拦截(记住属性只是方法 get_xxxx
/set_xxxx
)是通过一个名为 IInterceptor
的特殊 Castle 接口实现的,对于目标 1,它可以实现如下:
此拦截器由 ViewModelInstaller
自动应用于 ViewModel 命名空间中的任何类型(我们很快就会看到)。当调用此拦截器时,它会检查方法(属性更改本质上就是 get_xxx()
/set_xxx()
方法),并查找 INPCAttribute
,这是一个标准属性,如果找到,将导致方法调用也触发目标对象的 INPC PropertyChanged
事件。
public class NotifyPropertyChangedInterceptor : IInterceptor
{
#region IInterceptor Implementation
public void Intercept(IInvocation invocation)
{
// let the original call go 1st
invocation.Proceed();
if (invocation.Method.Name.StartsWith("set_"))
{
string propertyName = invocation.Method.Name.Substring(4);
var pi = invocation.TargetType.GetProperty(propertyName);
// check for the special attribute
if (!pi.HasAttribute<INPCAttribute>())
return;
FieldInfo info = invocation.TargetType.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic)
.Where(f => f.FieldType == typeof(PropertyChangedEventHandler))
.FirstOrDefault();
if (info != null)
{
//get the INPC field, and invoke it we managed to get it ok
PropertyChangedEventHandler evHandler =
info.GetValue(invocation.InvocationTarget)
as PropertyChangedEventHandler;
if (evHandler != null)
evHandler.Invoke(invocation.TargetType,
new PropertyChangedEventArgs(propertyName));
}
}
}
#endregion
}
目标 1,步骤 3:在目标类型上使用 INPCAttribute
既然我们有了 INPCAttribute
,我们只需要将其应用于某个目标类型。在演示应用程序中,此 INPCAttribute
应用于 MainWindowViewModel
类型,如下所示:
public class MainWindowViewModel : INotifyPropertyChanged
{
#region Ctor
public MainWindowViewModel()
{
DummyModel = ContainerWiring.Instance.Container.Resolve<DummyModel>();
}
#endregion
#region Public Properties
//Auto properties that will be made into INPC property via
//NotifyPropertyChangedInterceptor, note that the properties MUST be virtual
[INPCAttribute]
public virtual string DummyProp1 { get; set; }
[INPCAttribute]
public virtual string DummyProp2 { get; set; }
public string DummyProp3 { get; set; }
public DummyModel DummyModel { get; set; }
#endregion
#region INPC Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
目标 1,步骤 4:配置 Castle 以拦截 MainWindowViewModel
既然我们有了一个使用 INPCAttribute
的类型,我们就需要确保 Castle 已配置为使用它。这在 ContainerWiring
类中完成,如下所示:
public class ContainerWiring
{
#region Data
private static readonly Lazy<ContainerWiring> instance
= new Lazy<ContainerWiring>(() => new ContainerWiring());
private IWindsorContainer container;
#endregion
#region Ctor
private ContainerWiring()
{
container = new WindsorContainer();
}
#endregion
#region Public Methods/Properties
public static ContainerWiring Instance
{
get
{
return instance.Value;
}
}
public IWindsorContainer Container
{
get { return container; }
}
public void SetUp()
{
container.Install(FromAssembly.This());
}
public void TearDown()
{
if (container != null)
{
container.Dispose();
}
container = null;
}
#endregion
}
注意:我在这里使用的是 Lazy<T>
单例方法,这是我最喜欢的单例实现方式,因为它既是延迟加载又是线程安全的,而且易于阅读,并且不依赖于任何编译器相关的技巧。
嗯,这里没有提到任何拦截代码,只有一些 Setup()
方法……真奇怪。实际上发生的是,Setup()
方法负责安装演示应用程序所需的所有内容。但它实际上在做什么?在 Castle 中,您可以继承另一个名为 IWindsorInstaller
的接口,实现该接口后将执行您在实现中所指定的任何操作,并将允许实现 IWindsorInstaller
的类安装到 Castle 容器中。
演示应用程序包含三个安装程序,其中两个我现在将讨论,一个我稍后将讨论。
ViewModelInstaller
将为与 MainWindowViewModel
具有相同命名空间的任何类型安装拦截,这就是我们如何将拦截应用于上面看到的 MainWindowViewModel
类型。
public class ViewModelInstaller : IWindsorInstaller
{
#region IWindsorInstaller Implementation
public void Install(IWindsorContainer container, IConfigurationStore store)
{
//register ViewModels and add interceptors
container.Register(AllTypes.FromThisAssembly()
.Where(Castle.MicroKernel.Registration.Component.
IsInSameNamespaceAs<MainWindowViewModel>())
.Configure(c => c.LifeStyle.Transient
.Interceptors(typeof(NotifyPropertyChangedInterceptor))));
}
#endregion
}
InterceptorInstaller
任何自定义拦截器也必须安装到 Castle 容器中,才能,嗯,实际上拦截任何东西。这是演示应用程序中的 InterceptorInstaller
。
public class InterceptorInstaller : IWindsorInstaller
{
#region IWindsorInstaller Implementation
public void Install(IWindsorContainer container, IConfigurationStore store)
{
container.Register(AllTypes.FromThisAssembly().BasedOn<IInterceptor>());
}
#endregion
}
目标 1,步骤 5:使用 MainWindowViewModel
现在我们有了所有组件,只需要使用这个被拦截的类型。因此,在演示应用程序中,这在 MainWindow
中完成,如下所示:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainWindowViewModel viewModel =
ContainerWiring.Instance.Container.Resolve<MainWindowViewModel>();
this.DataContext = viewModel;
(viewModel as INotifyPropertyChanged).PropertyChanged +=
MainWindowViewModel_PropertyChanged;
}
private void MainWindowViewModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
MessageBox.Show(string.Format("{0} property changed " +
"in MainWindowViewModel", e.PropertyName));
}
}
您可以在上面看到,我们从 Castle 容器中获取了启用拦截的 MainWindowViewModel
,并且由于 MainWindowViewModel
类型使用了特殊的 INPCAttribute
,因此在设置启用拦截的 MainWindowViewModel
类型中的属性时,我们应该最终调用 INPC PropertyChanged
事件。
为了证明这一点有效,这是运行时的截图:
这就是满足目标 1 所需要做的一切。现在让我们来看看目标 2,它看起来像这样:
目标 2
创建了一个属性目标属性,当应用于未实现 INotifyPropertyChanged
的类时,它将自动实现 INotifyPropertyChanged
接口,并在设置具有该属性的属性时调用 PropertyChanged
事件。
目标 2,步骤 1:创建 AddINPCAttribute
与目标 1 一样,第一步相当简单;我们只需创建一个标准的 .NET 属性,如下所示:
[AttributeUsage(AttributeTargets.Property)]
public class AddINPCAttribute : Attribute
{
}
可以看出,这段代码并没有太多功能;它只是一个标记,我们将用它来标记需要在使用时调用 INPC PropertyChanged
事件的属性。但这次,目标类型将由 Castle 容器直接实现 INotifyPropertyChanged
接口。
目标 2,步骤 2:创建 Add INPC IInterceptor
和以前一样,我们继承了名为 IInterceptor
的特殊 Castle 接口,但这次实现方式大不相同。我们实际上想要为目标对象添加 INotifyPropertyChanged
接口的实现。
所以我们有这个:
public class AddNotifyPropertyChangedInterceptor : IInterceptor
{
#region Data
private PropertyChangedEventHandler handler;
#endregion
#region IInterceptor Implementation
public void Intercept(IInvocation invocation)
{
string methodName = invocation.Method.Name;
object[] arguments = invocation.Arguments;
object proxy = invocation.Proxy;
bool isINPC = false;
try
{
if (invocation.TargetType != null)
{
PropertyInfo realProp = invocation.TargetType.
GetProperty(invocation.Method.Name.Substring(4));
isINPC = realProp.HasAttribute<AddINPCAttribute>();
}
}
catch { }
if (invocation.Method.DeclaringType.Equals(typeof(INotifyPropertyChanged)))
{
if (methodName == "add_PropertyChanged")
StoreHandler((Delegate)arguments[0]);
if (methodName == "remove_PropertyChanged")
RemoveHandler((Delegate)arguments[0]);
}
if (!ShouldProceedWithInvocation(methodName))
return;
invocation.Proceed();
if (isINPC)
NotifyPropertyChanged(methodName, proxy);
}
#endregion
#region Protected Methods
protected void OnPropertyChanged(Object sender, PropertyChangedEventArgs e)
{
var eventHandler = handler;
if (eventHandler != null) eventHandler(sender, e);
}
protected void RemoveHandler(Delegate @delegate)
{
handler = (PropertyChangedEventHandler)Delegate.Remove(handler, @delegate);
}
protected void StoreHandler(Delegate @delegate)
{
handler = (PropertyChangedEventHandler)Delegate.Combine(handler, @delegate);
}
protected void NotifyPropertyChanged(string methodName, object proxy)
{
if (methodName.StartsWith("set_"))
{
var propertyName = methodName.Substring(4);
var args = new PropertyChangedEventArgs(propertyName);
OnPropertyChanged(proxy, args);
}
}
protected bool ShouldProceedWithInvocation(string methodName)
{
var methodsWithoutTarget = new[] {
"add_PropertyChanged", "remove_PropertyChanged" };
return !methodsWithoutTarget.Contains(methodName);
}
#endregion
}
可以看出,我们处理了 INotifyPropertyChanged
接口实现的委托的添加/删除,并且在设置了具有我们特殊 AddINPCAttribute
的属性时,我们也触发了 INPC PropertyChanged
事件。
目标 2,步骤 3:在目标类型上使用 AddINPCAttribute
既然我们有了 AddINPCAttribute
,我们只需要将其应用于某个目标类型。在演示应用程序中,此 AddINPCAttribute
应用于 DummyModel
类型,如下所示:
public class DummyModel
{
//Auto properties that will be made into INPC property via
//AddNotifyPropertyChangedInterceptor
[AddINPCAttribute]
public virtual string DummyModelProp1 { get; set; }
[AddINPCAttribute]
public virtual string DummyModelProp2 { get; set; }
public virtual string DummyModelProp3 { get; set; }
}
请注意,此类型根本不实现 INotifyPropertyChanged
接口。
目标 2,步骤 4:配置 Castle 以拦截 DummyModel
既然我们有了一个使用 AddINPCAttribute
的类型,我们就需要确保 Castle 已配置为使用它。这在之前看到的 ContainerWiring
类中完成。
public class ContainerWiring
{
#region Data
private static readonly Lazy<ContainerWiring> instance
= new Lazy<ContainerWiring>(() => new ContainerWiring());
private IWindsorContainer container;
#endregion
#region Ctor
private ContainerWiring()
{
container = new WindsorContainer();
}
#endregion
#region Public Methods/Properties
public static ContainerWiring Instance
{
get
{
return instance.Value;
}
}
public IWindsorContainer Container
{
get { return container; }
}
public void SetUp()
{
container.Install(FromAssembly.This());
}
public void TearDown()
{
if (container != null)
{
container.Dispose();
}
container = null;
}
#endregion
}
如我之前所述,大部分拦截代码实际上是通过 IWindsorInstaller
完成的,我已经讨论了演示应用程序中的 2 个(共 3 个),我们只需要讨论最后一个,它将 AddNotifyPropertyChangedInterceptor
添加到 DummyModel
类型。这是这样做的:
ModelInstaller
将为与 DummYModel
具有相同命名空间的任何类型安装拦截,这就是我们如何将拦截应用于上面看到的 DummyModel
类型。
public class ModelInstaller : IWindsorInstaller
{
#region IWindsorInstaller Implementation
public void Install(IWindsorContainer container, IConfigurationStore store)
{
//register ViewModels and add interceptors
container.Register(AllTypes.FromThisAssembly()
.Where(Castle.MicroKernel.Registration.Component.
IsInSameNamespaceAs<DummyModel>())
.Configure(c => c.LifeStyle.Transient
.Proxy.AdditionalInterfaces(typeof(INotifyPropertyChanged))
.Interceptors(typeof(AddNotifyPropertyChangedInterceptor))));
}
#endregion
}
目标 2,步骤 5:使用 DummyModel
现在我们有了所有组件,只需要使用这个被拦截的类型。因此,在演示应用程序中,这在持有 DummyModel
实例的 MainWindowViewModel
中完成:
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
DummyModel = ContainerWiring.Instance.Container.Resolve<DummyModel>();
}
.......
.......
.......
public DummyModel DummyModel { get; set; }
.......
.......
.......
}
我们可以在 MainWindow
的代码隐藏中,如下所示,从 INPC PropertyChanged
事件通知中监听:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainWindowViewModel viewModel =
ContainerWiring.Instance.Container.Resolve<MainWindowViewModel>();
this.DataContext = viewModel;
(viewModel as INotifyPropertyChanged).PropertyChanged +=
MainWindowViewModel_PropertyChanged;
(viewModel.DummyModel as INotifyPropertyChanged).PropertyChanged +=
DummyModel_PropertyChanged;
}
private void MainWindowViewModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
MessageBox.Show(string.Format("{0} property changed " +
"in MainWindowViewModel", e.PropertyName));
}
private void DummyModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
MessageBox.Show(string.Format("{0} property changed " +
"in DummyModel", e.PropertyName));
}
}
为了证明这一点有效,这是运行时的截图:
Unity (基于代理)
价格 | 使用的版本 | 可从以下网址获取 |
免费 | 2.0.414.0 | 企业库 5.0 的一部分 |
Unity 是一个 IOC/依赖注入 容器,它是一个独立的应用程序块,但我使用的是企业库 5.0 中附带的那个。
现在回想一下我使用 Unity 实现了什么。
目标 1
创建了一个属性目标属性,当应用于已实现 INotifyPropertyChanged
的类时,它将在属性设置时调用 INotifyPropertyChanged
的 PropertyChanged
事件。
目标 2
全局应用了一个拦截行为,当设置属性时,如果目标类型已实现 INotifyPropertyChanged
,则会使任何被拦截的属性 setter 调用 INotifyPropertyChanged
的 PropertyChanged
事件。.
现在,让我们看看我们是如何实现这两个目标的,从目标 1 开始。
目标 1,步骤 1:创建特殊的启用拦截的属性
在 Unity 中,可以通过几种不同的方式启用拦截,但一种常见的方式是继承一个特殊的 Unity 属性,称为 HandlerAttribute
。HandlerAttribute
是一个属性,继承它允许您的自定义属性可能将方面注入方法调用管道。演示应用程序使用这个继承自 HandlerAttribute
的 INPCAttribute
。
[AttributeUsage(AttributeTargets.Property)]
public class INPCAttribute : HandlerAttribute
{
public override ICallHandler CreateHandler(IUnityContainer container)
{
return new INPCHandler();
}
}
可以看出,这段代码并没有太多功能;它创建了另一个名为 INPCHandler
的类型。所以我们接下来应该看看它。
目标 1,步骤 2:创建 INPCAttribute 的 Handler
正如我们刚才看到的,我们有一个特殊的 INPCAttribute
,它创建了一个 INPCHandler
。那么 INPCHandler
看起来是什么样的呢?
/// <summary>
/// This handler is automatically applied to any Type in the ViewModels
/// which use the <c>INPCAttribute</c>.
/// When this ICallHandler implementation is called it will then examine
/// the method (and property changes are just get_xxx()/set_xxx() methods, and look
/// for a <c>INPCAttribute</c>, which is a standard Attribute which if found will cause
/// the method invocation to also fire the NotifyChanged() method on the target object.
///
/// See the <c>MainWindowViewModel</c> for an example of this
/// </summary>
public class INPCHandler : ICallHandler
{
#region ICallHandler Members
public IMethodReturn Invoke(IMethodInvocation input, GetNextHandlerDelegate getNext)
{
// let the original call go through first, so we can notify *after*
IMethodReturn result = getNext()(input, getNext);
if (input.MethodBase.Name.StartsWith("set_"))
{
string propertyName = input.MethodBase.Name.Substring(4);
var pi = input.Target.GetType().GetProperty(propertyName);
// check for the special attribute
if (pi.HasAttribute<INPCAttribute>())
{
// get the field storing the delegate list that are stored by the event.
FieldInfo info = input.Target.GetType().BaseType.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy)
.Where(f => f.FieldType == typeof(PropertyChangedEventHandler))
.FirstOrDefault();
if (info != null)
{
// get the value of the field
PropertyChangedEventHandler evHandler =
info.GetValue(input.Target) as PropertyChangedEventHandler;
// invoke the delegate if it's not null (aka empty)
if (evHandler != null)
evHandler.Invoke(input.Target.GetType(),
new PropertyChangedEventArgs(propertyName));
}
}
}
return result;
}
public int Order
{
get
{
return 0;
}
set
{
}
}
#endregion
}
从上面可以看出,这个 INPCHandler
也继承自一个名为 ICallHandler
的 Unity 接口。实际上,使用 Unity 可以直接应用实现 ICallHandler
的 Handler。但在此示例中,INPCHandler
是通过将 INPCAttribute
应用于某个目标类型来创建的。那么实现 ICallHandler
有什么好处呢?基本上,继承 ICallHandler
为我们提供了正确的管道挂钩到实际代码方面。只要我们正确配置了 Unity,任何 ICallHandler
实现都会在适当的时候被调用。
从上面的代码中也可以看出,我们正在查看是否处于属性 setter 中,如果是,我们检查要设置的属性是否具有特殊的 INPCAttribute
,如果是,我们就知道需要调用 INPC PropertyChanged
事件。所以我们调用它。
目标 1,步骤 3:在目标类型上使用特殊的 INPCAttribute
既然我们有了一个可以应用的 INPCAttribute
,它内部确保我们得到一个基于 INPCHandler
的 ICallHandler
类,我们只需要将其应用于某个目标类型。在演示应用程序中,此 INPCAttribute
应用于 MainWindowViewModel
类型,如下所示:
/// <summary>
/// Simple ViewModel that has auto properties that are made into
/// INPC based properties by using Unity application block using
/// the <c>INPCAttribute</c> which in turn
/// create a new <c>INPCHandler</c> which
/// will examine turn any auto property adorned with the <c>INPCAttribute</c>
/// into an INPC property
/// </summary>
public class MainWindowViewModel : INotifyPropertyChanged
{
#region Ctor
public MainWindowViewModel()
{
DummyModel = ContainerWiring.Instance.Container.Resolve<DummyModel>();
}
#endregion
#region Public Properties
//Note that these properties MUST be virtual for Unity inception to work
//as Unity is using a VirtualMethodInterceptor
[INPC]
public virtual string DummyProp1 { get; set; }
[INPC]
public virtual string DummyProp2 { get; set; }
public string DummyProp3 { get; set; }
public DummyModel DummyModel { get; set; }
#endregion
#region INotifyPropertyChanged Implementation
/// <summary>
/// Occurs when any properties are changed on this object.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// A helper method that raises the PropertyChanged event for a property.
/// </summary>
/// <param name="propertyNames">The names
/// of the properties that changed.</param>
protected virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="e">Event arguments.</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
#endregion
}
目标 1,步骤 4:配置 Unity 以拦截 MainWindowViewModel
既然我们有了一个使用 INPCAttribute
的类型,我们就需要确保 Unity 已配置为使用它。这在 ContainerWiring
类中完成,如下所示:
public class ContainerWiring
{
#region Data
private static readonly Lazy<ContainerWiring> instance
= new Lazy<ContainerWiring>(() => new ContainerWiring());
private IUnityContainer container;
#endregion
#region Ctor
private ContainerWiring()
{
container = new UnityContainer();
}
#endregion
#region Public Methods/Properties
public static ContainerWiring Instance
{
get
{
return instance.Value;
}
}
public IUnityContainer Container
{
get { return container; }
}
public void SetUp()
{
//register we want Interception
container.AddNewExtension<Interception>();
//register types
container.RegisterType<MainWindowViewModel>();
container.RegisterType<DummyModel>();
//Configure Interception
//Configure MainWindowViewModel to have any virtual methods intercepted
//which will end up using the INPCHandler
//due to the use of the INPCAttribute(s) in the
//MainWindowViewModel type
//Configure DummyModel to have any virtual methods
//intercepted using the NonAttributedINPCHandler
//but only for property setters
PolicyDefinition policy = container.Configure<Interception>().
SetInterceptorFor<DummyModel>(new VirtualMethodInterceptor()).
SetInterceptorFor<MainWindowViewModel>(
new VirtualMethodInterceptor()).AddPolicy("NotifyPolicy");
}
public void TearDown()
{
if (container != null)
{
container.Dispose();
}
container = null;
}
#endregion
}
目标 1,步骤 5:使用 MainWindowViewModel
现在我们有了所有组件,只需要使用这个被拦截的类型。因此,在演示应用程序中,这在 MainWindow
中完成,如下所示:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainWindowViewModel viewModel =
ContainerWiring.Instance.Container.Resolve<MainWindowViewModel>();
this.DataContext = viewModel;
(viewModel as INotifyPropertyChanged).PropertyChanged +=
MainWindowViewModel_PropertyChanged;
}
private void MainWindowViewModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
string extraInfo = "Note as we are using an HandlerAttribute " +
"and a blanket PolicyDefinition \r\n " +
"of * we will get 2 * INPC events for MainWindowViewModel, " +
"But I am keeping this in to show you the options";
MessageBox.Show(string.Format(
"{0} property changed in MainWindowViewModel\r\n\r\n{1}",
e.PropertyName, extraInfo));
}
}
您可以在上面看到,我们从 UnityContainer 获取了启用拦截的 MainWindowViewModel
,并且由于 MainWindowViewModel
类型使用了特殊的 INPCAttribute
,因此在设置启用拦截的 MainWindowViewModel
类型中的属性时,我们应该最终调用 INPC PropertyChanged
事件。
为了证明这一点有效,这是运行时的截图:
这就是满足目标 1 所需要做的一切。现在让我们来看看目标 2,它看起来像这样:
目标 2
全局应用了一个拦截行为,当设置属性时,如果目标类型已实现 INotifyPropertyChanged
,则会使任何被拦截的属性 setter 调用 INotifyPropertyChanged
的 PropertyChanged
事件。
目标 2,步骤 1:创建全局 INPC Handler
所以这次的目标是创建一个可以应用于整个类的全局 Handler,并且在设置属性时会自动调用 INPC PropertyChanged
事件。为此,我们需要实现另一个 Unity ICallHandler
,如下所示。注意:这次,一旦我们意识到我们正在设置一个属性,并且尝试调用目标类型的 INPC PropertyChanged
事件,就没有检查任何特殊属性了。
/// <summary>
/// This handler is used in direct conjuntion with a Unity <c>PolicyDefintion</c> which
/// allows Unity to specify that only property setters
/// will be intercepted using this CallHandler.
/// It makes auto properties fire the NotifyChanged() method
/// on the target object when the property is set.
/// See the <c>DummyModel</c> for an example of this
/// </summary>
public class NonAttributedINPCHandler : ICallHandler
{
#region ICallHandler Members
public IMethodReturn Invoke(IMethodInvocation input,
GetNextHandlerDelegate getNext)
{
// let the original call go through first, so we can notify *after*
IMethodReturn result = getNext()(input, getNext);
if (input.MethodBase.Name.StartsWith("set_"))
{
string propertyName = input.MethodBase.Name.Substring(4);
var pi = input.Target.GetType().GetProperty(propertyName);
// get the field storing the delegate list that are stored by the event.
FieldInfo info = input.Target.GetType().BaseType.GetFields(
BindingFlags.Instance | BindingFlags.NonPublic |
BindingFlags.FlattenHierarchy).Where(
f => f.FieldType ==
typeof(PropertyChangedEventHandler)).FirstOrDefault();
if (info != null)
{
// get the value of the field
PropertyChangedEventHandler evHandler = #
info.GetValue(input.Target) as PropertyChangedEventHandler;
// invoke the delegate if it's not null (aka empty)
if (evHandler != null)
evHandler.Invoke(input.Target.GetType(),
new PropertyChangedEventArgs(propertyName));
}
}
return result;
}
public int Order
{
get
{
return 0;
}
set
{
}
}
#endregion
}
从上面可以看出,这个 NonAttributedINPCHandler
也继承自一个名为 ICallHandler
的 Unity 接口。对于此实现,我们打算通过为 Unity 提供配置信息来自动将其应用于某个目标类型,以便全局应用它。
目标 2,步骤 2:我们需要一个目标类型来使用此全局 ICallHandler
所以我们有一个全局的 ICallHandler
,可以在设置属性时应用于 Unity,但我们仍然需要一个目标类型来应用它。那么目标类型是什么样的呢?在演示代码中,它看起来像这样:
public class DummyModel : INotifyPropertyChanged
{
public virtual string DummyModelProp1 { get; set; }
public virtual string DummyModelProp2 { get; set; }
public string DummyModelProp3 { get; set; }
#region INotifyPropertyChanged Implementation
/// <summary>
/// Occurs when any properties are changed on this object.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// A helper method that raises the PropertyChanged event for a property.
/// </summary>
/// <param name="propertyNames">The names of the properties that changed.</param>
protected virtual void NotifyChanged(params string[] propertyNames)
{
foreach (string name in propertyNames)
{
OnPropertyChanged(new PropertyChangedEventArgs(name));
}
}
/// <summary>
/// Raises the PropertyChanged event.
/// </summary>
/// <param name="e">Event arguments.</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
}
#endregion
}
目标 2,步骤 3:配置 Unity 以拦截 DummyModel
既然我们有一个我们想要应用 NonAttributedINPCHandler
的类型,我们就需要确保 Unity 已配置为使用它。这在 ContainerWiring
类中完成,如下所示:
public class ContainerWiring
{
#region Data
private static readonly Lazy<ContainerWiring> instance
= new Lazy<ContainerWiring>(() => new ContainerWiring());
private IUnityContainer container;
#endregion
#region Ctor
private ContainerWiring()
{
container = new UnityContainer();
}
#endregion
#region Public Methods/Properties
public static ContainerWiring Instance
{
get
{
return instance.Value;
}
}
public IUnityContainer Container
{
get { return container; }
}
public void SetUp()
{
//register we want Interception
container.AddNewExtension<Interception>();
//register types
container.RegisterType<MainWindowViewModel>();
container.RegisterType<DummyModel>();
//Configure Interception
//Configure MainWindowViewModel to have any virtual methods intercepted
//which will end up using the INPCHandler due
//to the use of the INPCAttribute(s) in the
//MainWindowViewModel type
//Configure DummyModel to have any virtual methods
//intercepted using the NonAttributedINPCHandler
//but only for property setters
PolicyDefinition policy = container.Configure<Interception>().
SetInterceptorFor<DummyModel>(new VirtualMethodInterceptor()).
SetInterceptorFor<MainWindowViewModel>(
new VirtualMethodInterceptor()).AddPolicy("NotifyPolicy");
//Configure PolicyDefinition : Note as we are using
//an HandlerAttribute and a blanket PolicyDefinition
//of * we will get 2 * INPC events for MainWindowViewModel,
//But I am keeping this in to show you the options
policy.AddMatchingRule(new PropertyMatchingRule("*",
PropertyMatchingOption.Set));
policy.AddCallHandler<NonAttributedINPCHandler>();
}
public void TearDown()
{
if (container != null)
{
container.Dispose();
}
container = null;
}
#endregion
}
注意:这是 ContainerWiring
类的完整列表,因此它也包括我们之前看到的 MainWindowViewModel
的 Unity 设置,用于目标 1。请看这次我们如何使用 Unity PolicyDefinition
的实例,并指定应匹配任何属性 setter,并添加一个我们的 NonAttributedINPCHandler
Handler 的调用 Handler,该 Handler 在调用者中调用目标对象的 INPC PropertyChanged
事件。
现在,这里有一个值得注意的有趣事情是,因为 MainWindowViewModel
也有可设置的属性,所以它也被包含在这个全局 setter 策略中,但它还使用了我们上面讨论的两个属性上的 INPCAttribute
。这意味着在更改这两个具有 INPCAttribute
的 MainWindowViewModel
属性时,我们将收到两个 INPC PropertyChanged
事件通知。一个来自 INPCAttribute
,另一个来自全局应用的 NonAttributedINPCHandler
。
对于 DummyModel
类型,情况并非如此,因为它只使用全局 setter 策略,所以只适用该规则。
目标 2,步骤 4:使用 DummyModel
好的,现在我们有了这个 DummyModel
类型,它将有一个全局 Unity ICallHandler
应用于它,我们只需要在某处使用这些 DummyModel
类之一。对于演示应用程序,MainWindowViewModel
(本身已设置为拦截)将其 DummyModel
作为属性使用,如下所示:
public class MainWindowViewModel : INotifyPropertyChanged
{
public MainWindowViewModel()
{
DummyModel = ContainerWiring.Instance.Container.Resolve<DummyModel>();
}
......
......
......
public DummyModel DummyModel { get; set; }
......
......
......
......
......
}
因此,我们也可以在 MainWindow
代码隐藏中,如下所示,监听 MainWindowViewModel
的 DummyModel
实例的 INPC PropertyChanged
事件:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainWindowViewModel viewModel =
ContainerWiring.Instance.Container.Resolve<MainWindowViewModel>();
this.DataContext = viewModel;
(viewModel.DummyModel as INotifyPropertyChanged).PropertyChanged +=
DummyModel_PropertyChanged;
}
private void DummyModel_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
MessageBox.Show(string.Format("{0} property changed " +
"in DummyModel", e.PropertyName));
}
}
这是工作演示的截图:
PostSharp (IL 织合)
价格 | 使用的版本 | 可从以下网址获取 |
£120-£220 | 2.0.0.0 | http://www.sharpcrafters.com/postsharp/download
有 45 天免费试用 |
Postsharp 是一个顶级 AOP 框架,可以说是市场上最成熟的,它功能非常丰富;你们中的一些人可能还记得它以前是免费的。不幸的是,天下没有免费的午餐,现在需要付费购买 PostSharp,尽管价格并不高。
总之,如果您还记得我关于使用 PostSharp 实现了什么
创建了一个类目标属性,当应用于未实现 INotifyPropertyChanged
的类时,它将自动实现 INotifyPropertyChanged
接口,并在设置属性时调用 PropertyChanged
事件。
要实现这一点,我只需要执行几个步骤。
步骤 1:引用 PostSharp
添加对 PostSharp.dll 的引用。
步骤 2:创建一个我们想使其实现 INPC 的对象
对于附加的演示应用程序,它的样子如下,我正在使用一个特殊的 INPC 方面类,该类在步骤 3 中讨论。请注意,下面我根本没有实现 INPC,属性也没有任何属性,并且请注意属性是标准的自动属性,它们不是虚拟的。
[INPC]
public class MainWindowViewModel
{
#region Public Properties
//Note that these properties are NOT virtual
public string DummyProp1 { get; set; }
public string DummyProp2 { get; set; }
public string DummyProp3 { get; set; }
#endregion
}
步骤 3:实现 INPC 方面
这是真正的工作所在;我们现在必须实现实际的 INPC 方面类,它看起来像这样:
[Serializable]
[IntroduceInterface(typeof(INotifyPropertyChanged),
OverrideAction = InterfaceOverrideAction.Ignore)]
[MulticastAttributeUsage(MulticastTargets.Class,
Inheritance = MulticastInheritance.Strict)]
public sealed class INPCAttribute : InstanceLevelAspect, INotifyPropertyChanged
{
#region Public Properties / Methods
[ImportMember("OnPropertyChanged", IsRequired = false,
Order = ImportMemberOrder.AfterIntroductions)]
public Action<string> OnPropertyChangedMethod;
[IntroduceMember(Visibility = Visibility.Family,
IsVirtual = true, OverrideAction = MemberOverrideAction.Ignore)]
public void OnPropertyChanged(string propertyName)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this.Instance,
new PropertyChangedEventArgs(propertyName));
}
}
[IntroduceMember(OverrideAction = MemberOverrideAction.Ignore)]
public event PropertyChangedEventHandler PropertyChanged;
[OnLocationSetValueAdvice, MulticastPointcut(Targets = MulticastTargets.Property,
Attributes = MulticastAttributes.Instance | MulticastAttributes.NonAbstract)]
public void OnPropertySet(LocationInterceptionArgs args)
{
// Don't go further if the new value is equal to the old one.
// (Possibly use object.Equals here).
if (args.Value == args.GetCurrentValue()) return;
// Actually sets the value.
args.ProceedSetValue();
this.OnPropertyChangedMethod.Invoke(args.Location.Name);
}
#endregion
那里有很多事情正在发生,但都没有超出我们的能力,所以让我们分解一下。
从下面可以看出,这个方面不仅在每次设置属性时调用 NotifyPropertyChanged
,而且还引入了 INotifyPropertyChanged
接口的实际实现,并且还引入了 INotifyPropertyChanged
实现所需的所有事件和方法。这主要通过 IntroduceMemberAttribute
实现。
基本上就是这样了。如果我们运行演示,其中 MainWindow
将其 DataContext
设置为 MainWindowViewModel
,我们确实可以看到 MainWindowViewModel
正确实现了 INPC。
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
MainWindowViewModel viewModel = new MainWindowViewModel();
this.DataContext = viewModel;
(viewModel as INotifyPropertyChanged).PropertyChanged +=
MainWindowViewModel_PropertyChanged;
}
private void MainWindowViewModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
string extraInfo =
"Note as we are using an PostSharp, we simply not " +
"even have to have virtual properties in our MainWindowViewModel";
MessageBox.Show(string.Format(
"{0} property changed in MainWindowViewModel\r\n\r\n{1}",
e.PropertyName, extraInfo));
}
}
LinFu.AOP (IL 织合)
价格 | 使用的版本 | 可从以下网址获取 |
免费 | 1.0.0.0 | https://codeproject.org.cn/KB/cs/LinFuPart6.aspx |
由于 LinFu.AOP 在内部依赖于我在文章开头提到的 Mono.Cecil DLL,我认为我应该通过一个简单的例子来展示 Mono.Cecil 的工作原理。
Mono Cecil 如何工作
假设我有一个已经实现 INPC 的类型,并且我们有一个标准的 INPC 属性,如下面的 FirstName
:
我们还有一个自动属性 LastName
,如下所示,我们可以看到它有一些编译器生成的代码。
我们检查 StartName
完全 INPC 属性的 IL:
与自动生成的 LastName
属性的 IL 相比:
我们可以看到它们几乎相同,但我们缺少一些 IL 指令:
所以,如果我们能以某种方式进入其中并引入这些 IL 指令,并保存程序集,那么完整的 INPC 属性和自动属性应该都能正常工作。这正是 Mono.Cecil 所允许的,这里是一些示例代码:
CilWorker MSILWorker = prop.SetMethod.Body.CilWorker;
Instruction ldarg0 = MSILWorker.Create(OpCodes.Ldarg_0);
Instruction propertyName = MSILWorker.Create(OpCodes.Ldstr, prop.Name);
Instruction callRaisePropertyChanged =
MSILWorker.Create(OpCodes.Call, raisePropertyChanged);
MSILWorker.InsertBefore(prop.SetMethod.Body.Instructions[0],
MSILWorker.Create(OpCodes.Nop));
MSILWorker.InsertBefore(
prop.SetMethod.Body.Instructions[
prop.SetMethod.Body.Instructions.Count - 1],ldarg0);
MSILWorker.InsertAfter(ldarg0, propertyName);
MSILWorker.InsertAfter(propertyName, callRaisePropertyChanged);
MSILWorker.InsertAfter(callRaisePropertyChanged, MSILWorker.Create(OpCodes.Nop));
显然,LinFu.AOP 完成的工作远不止于此,但它确实使用 Cecil 来完成。它通常按上述方式工作。我将详细介绍 LinFu 的具体工作原理,但我认为这个小小的离题是值得的。
好的,现在回到 LinFu。
LinFu 如何工作
总之,如果您还记得我关于使用 LinFu 实现了什么:
创建了一个属性目标属性,当应用于已实现 INotifyPropertyChanged
的类时,它将在属性设置时调用 INotifyPropertyChanged
的 PropertyChanged
事件。
要实现这一点,我只需要执行几个步骤。
步骤 1:创建 Aspect 拦截项目
不幸的是,除非我将要织合方面和方面本身的项目分开,否则我似乎无法让 LinFu 工作。所以,我创建了一个存储将被拦截的类型的项目,并确保我引用了正确的 LinFu DLL。
步骤 2:编辑 Aspect 拦截项目的 MSBuild 文件
下一步是卸载包含将被织合方面类型的项目。对于演示应用程序,这意味着 LinFu.ViewModels 项目。
然后我必须在项目的 MSBUILD 文件中添加以下几行:
<PropertyGroup>
<PostWeaveTaskLocation>
C:\Users\WIN7LAP001\Desktop\Downloads\LinFu_Src\LinFu.Aop.Tasks.dll
</PostWeaveTaskLocation>
</PropertyGroup>
<UsingTask TaskName="PostWeaveTask" AssemblyFile="$(PostWeaveTaskLocation)" />
<Target Name="AfterBuild" Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU'">
<PostWeaveTask
TargetFile="$(MSBuildProjectDirectory)\
$(OutputPath)$(MSBuildProjectName).dll"
InjectConstructors="true" />
</Target>
此步骤至关重要,因为它指向 LinFu 用于注入方面的 PostWeaver Task。在添加此项并使其指向安装位置的正确位置后,您可以重新加载项目。
步骤 3:标记一个你想使其实现 INPC 的类
这是最简单的一部分;我们只需要创建一个简单的 Attribute,用作标记,应用于属性,以便以后我们可以检查该方面是否应触发被拦截类型的 PropertyChanged
事件。
这个属性看起来很简单:
[AttributeUsage(AttributeTargets.Property)]
public class INPCAttribute : Attribute
{
}
步骤 3:在目标类型上使用 INPCAttribute
下一步是实际使用此 INPCAttribute
在目标类型上。对于 LinFu 演示,这将是一个 MainWindowViewModel
类型,它看起来像这样:
public class MainWindowViewModel : INotifyPropertyChanged
{
#region Public Properties
//Auto properties that will be made into INPC property via
//INPCMethodInvocation, note the properties are NOT virtual
[INPCAttribute]
public string DummyProp1 { get; set; }
[INPCAttribute]
public string DummyProp2 { get; set; }
public string DummyProp3 { get; set; }
#endregion
#region INPC Implementation
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChanged(string propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
#endregion
}
您会注意到,对于这个类,我们确实实现了 INotifyPropertyChanged
接口,所以我们只期望 LinFu 方面调用 MainWindowViewModel
中已有的 INotifyPropertyChanged
接口的 PropertyChanged
事件。
这就是第一个 LinFu AOP 演示项目所需要做的一切,但接下来,我们需要看一下包含方面本身的实际项目。那么,我们继续看看吧?
步骤 4:创建 Aspect 织合项目
正如我在步骤 1 中提到的,除非我将要织合方面和方面本身的项目分开,否则我似乎无法让 LinFu 工作。所以,我创建了一个存储将被拦截的类型的项目。我们已经创建了第一个项目,即存储将被拦截的类型的项目;现在我们需要创建实际执行方面并生成 IL 织合代码的项目。
让我们看看这个其他项目。它是一个 WPF 应用程序,需要以下引用:
其中 LinFu.ViewModels
引用是我们第一步创建的项目。
步骤 5:创建将生成修改后的 IL 的拦截代码
LinFu 使用接口来表示方面,并且有一个名为 IInvokeAround
的特殊接口,您可以在调用方法/属性时实现它(属性实际上是像 get_XXXX
/set_XXXX
这样的方法)。所以我们想要一个方面,它将在属性设置时调用实现 INotifyPropertyChanged
PropertyChanged
事件的目标类型的 INotifyPropertyChanged
。
这是 LinFu.AOP IInvokeAround
实现的拦截代码的完整代码(它与 Castle/Unity 代码非常相似):
public class INPCMethodInvocation : IAroundInvoke
{
#region IAroundInvoke Members
public void AfterInvoke(IInvocationContext context, object returnValue)
{
if (context.TargetMethod.Name.StartsWith("set_"))
{
string propertyName = context.TargetMethod.Name.Substring(4);
var pi = context.Target.GetType().GetProperty(propertyName);
// check for the special attribute
if (!pi.HasAttribute<INPCAttribute>())
return;
FieldInfo info = context.Target.GetType().GetFields(
BindingFlags.Instance | BindingFlags.NonPublic)
.Where(f => f.FieldType == typeof(PropertyChangedEventHandler))
.FirstOrDefault();
if (info != null)
{
//get the INPC field, and invoke it we managed to get it ok
PropertyChangedEventHandler evHandler =
info.GetValue(context.Target) as PropertyChangedEventHandler;
if (evHandler != null)
evHandler.Invoke(context.Target.GetType(),
new PropertyChangedEventArgs(propertyName));
}
}
}
public void BeforeInvoke(IInvocationContext context)
{
//Not going to do anything before context
}
#endregion
}
那么 LinFu 中发生的情况是,在编译发生时,LinFu TaskWeaver
MSBUILD 任务将被运行,它将检查任何允许拦截的类型,然后使用现有类型的 IL 方法,并获取 IAroundInvoke
实现的 XXXXInvoke
代码并获取其 IL,然后它将在 LinFu 内部形成一个 IL 指令列表,然后它将使用 Mono.Cecil DLL 的 IL 重写功能来重写 Assembly
,使其包含原始方法的 IL 以及 IAroundInvoke
实现的 XXXXInvoke
IL。因此,在 IAroundInvoke
实现的 AfterInvoke(...)
的情况下,我们将得到原始方法的 IL 和 IAroundInvoke
实现的 AfterInvoke
IL 被发射到修改后的程序集中。
请记住,所有这些都是在编译时发生的,它不是运行时的事情,程序集实际上被修改以织合新的 IL。这就是 Mono.Cecil 所允许的。而这就是 LinFu 在内部使用的。
步骤 6:将 Aspect 应用于目标类型
到目前为止一切顺利;我们有一个可以应用于类型的方面,现在我们只需要应用它。这是我们使用 LinFu.AOP 的方法(注意:我正在使用与前面所示相同的 Lazy<T>
类型单例)。让我们看看 LinFu 中如何设置拦截。这在演示应用程序中在一个名为 Inception
的类中完成,这个名字没什么特别的,只是我随便选的一个,这是该类的完整代码:
public class Inception
{
#region Data
private static readonly Lazy<Inception> instance
= new Lazy<Inception>(() => new Inception());
#endregion
#region Ctor
private Inception()
{
}
#endregion
#region Public Methods/Properties
public static Inception Instance
{
get
{
return instance.Value;
}
}
public void SetUp()
{
var mainWindowViewModelProvider =
new SimpleAroundInvokeProvider(new INPCMethodInvocation(),
c => c.TargetMethod.DeclaringType == typeof(MainWindowViewModel));
AroundInvokeRegistry.Providers.Add(mainWindowViewModelProvider);
}
#endregion
}
我们只是设置了一个新的 INPCMethodInvocation
拦截器(我们在步骤 5 中创建的),并将其应用于 MainWindowViewModel
类型。很简单……对吧?
步骤 7:使用 Aspect 类型
现在我们有了所有拼图碎片,我们只需要得到一个这些被拦截的 MainWindowViewModel
类型之一,并确保它能正常工作。
这是我们这样做的:
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
var viewModel = new MainWindowViewModel();
this.DataContext = viewModel;
(viewModel as INotifyPropertyChanged).PropertyChanged +=
MainWindowViewModel_PropertyChanged;
//Note I had to EnableInterception after
//there was something listening to the
//INotifyPropertyChanged.PropertyChanged otherwise
//the INPCMethodInvocation code would fail
//to find a PropertyChangedEventHandler delegate
//for the type being intercepted by the
//INPCMethodInvocation code
viewModel.EnableInterception();
}
private void MainWindowViewModel_PropertyChanged(object sender,
PropertyChangedEventArgs e)
{
MessageBox.Show(string.Format("{0} property changed " +
"in MainWindowViewModel", e.PropertyName));
}
}
为了证明一切正常,这里是截图:
其他方面
在这篇文章中,我主要集中于执行一两个基于 INPC 的方面,但我希望你们都能看到如何自己创建方面,例如将方法调用记录到文件的日志方面,或者将对方法的调用Marshaler 到 UI 线程的线程方面,甚至是在当前用户的 IWindowsPrincipal
对象不允许他们访问某个方法时抛出异常等。
就这些
这篇文章花费了大量时间和反复试验来研究,所以如果您喜欢这篇文章并认为它可能对您有用,或者可能只是给您一些关于方面如何为您工作的想法,您能否留下评论/投票?
非常感谢大家…… Sacha。