65.9K
CodeProject 正在变化。 阅读更多。
Home

方面示例 (INotifyPropertyChanged 通过 Aspect)

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (77投票s)

2010 年 12 月 27 日

CPOL

30分钟阅读

viewsIcon

225251

downloadIcon

1938

探讨不同的面向切面编程框架。

引言

本文将全面介绍面向切面编程 (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 提供 TransparentProxyVirtualMethodProxy 对象。既然我们知道有些代理在使用(至少在某些 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 模板,到使用表达式树,再到使用 StackFrames,相信我,我见过不少实现。

问题是,对于所有这些方法,您仍然需要做一些工作。这让我想,如果我们能用一个属性来标记一个自动属性,告诉它是否是一个 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
  • 创建了一个属性目标属性,当应用于实现 INotifyPropertyChanged 的类时,它将在属性设置时调用 INotifyPropertyChangedPropertyChanged 事件。
  • 创建了一个属性目标属性,当应用于实现 INotifyPropertyChanged 的类时,它将自动实现 INotifyPropertyChanged 接口,并在设置具有该属性的属性时调用 PropertyChanged 事件。
Unity
  • 创建了一个属性目标属性,当应用于实现 INotifyPropertyChanged 的类时,它将在属性设置时调用 INotifyPropertyChangedPropertyChanged 事件。
  • 全局应用了一个拦截行为,当设置属性时,如果目标类型实现 INotifyPropertyChanged,则会使任何被拦截的属性 setter 调用 INotifyPropertyChangedPropertyChanged 事件。
PostSharp (非免费)
  • 创建了一个类目标属性,当应用于实现 INotifyPropertyChanged 的类时,它将自动实现 INotifyPropertyChanged 接口,并在设置属性时调用 PropertyChanged 事件。
LinFu.AOP *
  • 创建了一个属性目标属性,当应用于实现 INotifyPropertyChanged 的类时,它将在属性设置时调用 INotifyPropertyChangedPropertyChanged 事件。

* 依赖于 Mono.Cecil

深入研究演示代码示例

下一节将概述我选择研究的各种 AOP 框架如何实现我上面表格中描述的功能。

Castle (基于代理)

价格 使用的版本 可从以下网址获取
免费 2.5.0.0 http://www.castleproject.org/

Castle 是一个 IOC/依赖注入 容器。现在回想一下我使用 Castle 实现了什么。

目标 1

创建了一个属性目标属性,当应用于实现 INotifyPropertyChanged 的类时,它将在属性设置时调用 INotifyPropertyChangedPropertyChanged 事件。

目标 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 的类时,它将在属性设置时调用 INotifyPropertyChangedPropertyChanged 事件。

目标 2

全局应用了一个拦截行为,当设置属性时,如果目标类型实现 INotifyPropertyChanged,则会使任何被拦截的属性 setter 调用 INotifyPropertyChangedPropertyChanged 事件。.

现在,让我们看看我们是如何实现这两个目标的,从目标 1 开始。

目标 1,步骤 1:创建特殊的启用拦截的属性

在 Unity 中,可以通过几种不同的方式启用拦截,但一种常见的方式是继承一个特殊的 Unity 属性,称为 HandlerAttributeHandlerAttribute 是一个属性,继承它允许您的自定义属性可能将方面注入方法调用管道。演示应用程序使用这个继承自 HandlerAttributeINPCAttribute

[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,它内部确保我们得到一个基于 INPCHandlerICallHandler 类,我们只需要将其应用于某个目标类型。在演示应用程序中,此 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 调用 INotifyPropertyChangedPropertyChanged 事件。

目标 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。这意味着在更改这两个具有 INPCAttributeMainWindowViewModel 属性时,我们将收到两个 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 代码隐藏中,如下所示,监听 MainWindowViewModelDummyModel 实例的 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 的类时,它将在属性设置时调用 INotifyPropertyChangedPropertyChanged 事件。

要实现这一点,我只需要执行几个步骤。

步骤 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。

© . All rights reserved.