.Net中的AOP系列之《拦截位置》

返回《.Net中的AOP》系列学习总目录


本篇目录


本节的源码本人已托管于Coding上:点击查看

本系列的实验环境:VS 2013 Update 5(建议最好使用集成了Nuget的VS版本,VS Express版也够用),安装了PostSharp。


至今,我们的关注点都是集中在方法上,本节,就看一下位置,这里的位置指的是字段或属性。位置拦截是AOP框架不太通用的功能,因此,本节大多数的例子都是使用支持位置的PostSharp框架,此外,这节还会看到一个特殊的AOP工具(与常见的AOP框架截然不同),叫做PropertyChanged.Fody

位置拦截

也许很多人没有听过C#中有位置这一说,其实,一个字段或者一个属性都是一个位置。字段和属性是OOP中常见的东西,它们为提供数据和结构。下面简单复习一下,觉得没问题的同学可以直接跳过,记住:属性只是getter/setter方法的语法糖。

.Net中的字段和属性

字段是类的成员。它们可以声明为public,private,protected,internal等等,这样就可以限制访问级别了(默认是private)。

通常,如果封装很重要的话,就不会使用public字段,因此,字段通常被设置成private,然后通过访问器方法在类外面使用该字段。如果有一个private的_balance(余额)字段,那么只能通过其它对象调用Deposit(存款)或者Withdrawal(取款)方法来改变这个字段的值:

public class BankAccount {
    decimal _balance;
    public void SetBalance(decimal amount) {
        _balance = amount;
    }
    public decimal GetBalance(decimal amount) {
        return _balance;
    }
}

在C#中,我们可以使用属性语法(get 和set )来减少代码量,下面的代码中,Balance属性封装了一个私有字段_balance:

public class BankAccount {
    decimal _balance;
    public decimal Balance {
        get {
            return _balance;
        }
        set {
            _balance = value;
        }
    }
}

get 和set都是可选的:如果不需要设置一个字段的值,那么就不需要写setter,getter同样如此。但是,这后面,.Net编译器帮我们创建了方法,如果使用反编译工具如ILSpy看一下IL代码,就会发现编译器创建了一个decimal get_Balance()方法和一个void set_Balance(decimal)方法:

.class public auto ansi beforefieldinit MyBankingProject.BankAccount
	extends [mscorlib]System.Object
{
	.field private valuetype [mscorlib]System.Decimal _balance
	.method public hidebysig specialname
		instance valuetype [mscorlib]System.Decimal get_Balance ()
			cil managed {
              //此处省略若干IL代码
			}
	.method public hidebysig specialname
		instance void set_Balance (
		valuetype [mscorlib]System.Decimal 'value'
		) cil managed {
           //此处省略若干IL代码
			}
}

自动属性是在C#2.0中引入的,这个工具让语法糖变得更甜了,我们甚至不需要显式创建字段就可以创建一个属性,如下:

public class MyClass {
    public string MyProperty {get; set;}
}

当使用自动属性时,必须同时使用get和set,但是可以使用不同的访问级别。比如,get可以设置成公共的,set可以设置成私有的。
对于我们.Net开发者来说,这并不是什么新鲜事儿,因为我们几乎每天都会使用这些,但是越是最常用的东西,通常你也认为最理所当然,因此,在深入涉及位置拦截的AOP代码之前有必要重温一下细节问题。

PostSharp位置拦截

之前的教程我们知道了,AOP工具可以拦截方法,那么从上面我们又知道,属性的底层就是方法,因此,我们可以猜想可以在属性上使用方法拦截切面。事实上这是可行的,可以使用PostSharp或Castle DynamicProxy在属性上创建方法拦截。下面就是一个使用PostSharp在属性上创建方法拦截的控制台例子:

public class TestClass
{
    public string TestProperty
    {
        get;
        [MyMethodAspect]
        set;//在一个属性的setter上使用方法拦截切面
    }
}

[Serializable]
public class MyMethodAspect:MethodInterceptionAspect
{
    public override void OnInvoke(MethodInterceptionArgs args)
    {
        Console.WriteLine("这条语句来自自定义方法拦截切面");
        args.Proceed();
    }
}

class Program
{
    static void Main(string[] args)
    {
        var test=new TestClass();
        test.TestProperty = "测试属性";//这里会调用属性的setter方法

        Console.Read();
    }
}



效果如下:

但是这样使用有几个问题:

  1. 笨拙。可能必须写两个切面,一个给setter,一个给getter。
  2. 只能给属性使用切面,字段的底层不是方法,所以行不通。

没关系,PostSharp给我们提供了一个更方便的方法,只需要写一个类就可以处理getting和setting,还允许为字段和属性编写切面。这就是PostSharp中的LocationInterceptionAspect,下面的例子和上面的一样,只是这次使用了LocationInterceptionAspect:

[Serializable]
public class MyLocationAspect:LocationInterceptionAspect
{
    public override void OnGetValue(LocationInterceptionArgs args)
    {
        Console.WriteLine("这条语句来自位置拦截的{0}方法",MethodBase.GetCurrentMethod());
        args.ProceedGetValue();
    }

    public override void OnSetValue(LocationInterceptionArgs args)
    {
        Console.WriteLine("这条语句来自位置拦截的{0}方法", MethodBase.GetCurrentMethod());
        args.ProceedSetValue();
    }
}

public class TestClass2
{
    [MyLocationAspect]
    public string TestProperty
    {
        get;
        set;
    }
}

static void Main(string[] args)
{
    //var test=new TestClass();
    //test.TestProperty = "测试属性";

    var test2=new TestClass2();
    test2.TestProperty = "位置拦截测试";
    Console.WriteLine(test2.TestProperty);
    Console.Read();
}


Main方法中,先是给属性赋值,所以会被MyLocationAspectOnSetValue方法拦截到,然后打印test2.TestProperty时会被OnGetValule方法拦截,因此运行结果如下:

这里新出现的args.ProceedSetValue(); args.ProceedGetValue();和之前的args.Proceed();是一样的道理,是继续执行属性方法(属性的本质就是方法)的意思。

真实案例——懒加载

懒加载的目的就是延迟一些耗时操作的执行,相反,预加载的目的是一个或多个操作在得到结果前每次都要执行,以防需要这些操作。NHibernate和EF都是用在持久层的数据库工具,当使用懒加载从DB中检索实体时,它们只会拉取你需要的实体而不会拉取相关实体,相反,使用预加载,它们就会把你需要的实体(比如A),和该实体相关的实体(B),以及和B相关的实体(C)等等都会加载出来。此时,就需要在性能和方便之间进行权衡了。

.Net中的懒加载

懒加载的一种方式是使用具有字段的属性来实现。当首次使用get时,会创建一个新对象,后续再使用字段时都会像以往那样返回字段。如下所示:

SlowConstructor _myProperty;
public SlowConstructor MyProperty {
    get {
        if (_myProperty == null)
	 _myProperty = new SlowConstructor();
        return _myProperty;
    }
}

细心的你可能会发现这不是线程安全的代码,如果这是一个关注点的话,就需要放一个lock语句。这里使用双重检查的锁机制再合适不过了,因为在第一次检查和lock之间可能会发生竞争情况(race condition):

readonly object _syncRoot = new object();
SlowConstructor _myProperty;
public SlowConstructor MyProperty {
    get {
        if (_myProperty == null) 
          lock(_syncRoot)
        	if (_myProperty == null)
              _myProperty = new SlowConstructor();//在第一次if检查和lock之间可能有另一个线程正在给字段赋值
        return _myProperty;
    }
}

这样,就可以使用懒加载了。你可以像平时那样访问属性,如果不用它的话,那么SlowConstructor永远都不会运行。也可以使用工厂或者IoC工具代替new来实例化对象。但无论怎样,lock,两次if检查和字段都始终是保持不变的。

从.NET 4.0开始,.Net Framework提供了System.Lazy<T>,它是一个方便类,可以使用更少的代码完成和上面相同的事情。代码如下:

var lazy = new Lazy<SlowConstructor>(()=>new SlowConstructor());

工厂代码是以Lambda表达式(匿名函数)传入的,这就告诉Lazy首次访问时使用这个代码来实例化对象,System.Lazy<T>默认也是线程安全的,因此它封装了所有的lock代码。但是,跟前面那个例子不同的是,这样字段就成了Lazy类型,而不是SlowConstructor类型,要使用SlowConstructor对象的话,还要多个步骤:SlowConstructor c = MyProperty.Value;

现在,想要使用懒加载时有两种选择,第一种有许多样板代码和字段,第二种使用Lazy,所有的样板代码是没有了,但是必须通过Value属性来获得懒加载对象。下面使用AOP来结合一下这两种方法的优点。

使用AOP实现懒加载

结合上面两种方法的优点,那就是可以直接访问属性(不需要通过Value属性),而且也没有很多的样板代码,就像下面这个样子:

[LazyLoadGetter]//使用特性告诉PostSharp这个属性是懒加载属性
static SlowConstructor MyProperty {
	get { return(new SlowConstructor() ); }
}

get方法体内包含了懒加载的工厂,直到get执行时才会调用,后续的get调用也会使用首次操作的结果。
下面传创建一个控制台应用,命名为LazyLoadingDemo,安装PostSharp。定义一个模拟耗时的操作SlowConstructor(比如一个调用了一个很慢的web service等):

public class SlowConstructor
{
    public SlowConstructor()
    {
        Console.WriteLine("正在初始化SlowConstructor,请稍等...");
        Thread.Sleep(5000);//睡5秒,模拟耗时操作
    }

    public void DoSomething()
    {
        Console.WriteLine("{0}:正在处理一些业务...",DateTime.Now);
    }
}

在Main方法种定义该类的一个属性,并连续调用该类的DoSomething方法:

class Program
{
    static SlowConstructor SlowService
    {
        get { return new SlowConstructor();}
    }
    static void Main(string[] args)
    {
        SlowService.DoSomething();
        SlowService.DoSomething();

        Console.Read();
    }
}


这样写代码的话就应该优化了,因为每次调用属性的get方法时都会重新实例化SlowConstructor对象。
执行结果很明显,如下:

但我们这里计划的是懒加载这个属性,不需要添加所有的字段、双重检查锁,或切换使用Lazy<T>,这里我们可以创建一个切面,该切面继承PostSharp的LocationInterceptionAspect,然后把自定义的切面当作特性用在需要懒加载的属性上即可:

[Serializable]
public class MyLazyLoadingGetterAspect : LocationInterceptionAspect
{
    private object _backingField;
    readonly object _syncRoot = new object();
    public override void OnGetValue(LocationInterceptionArgs args)
    {
        if (_backingField == null)
        {
            lock (_syncRoot)
            {
                if (_backingField == null)
                {
                    args.ProceedGetValue();//继续像往常那样执行get
                    _backingField = args.Value;//将get获得的属性值保存到支持字段中
                }
            }
            args.Value = _backingField;//因为支持字段中已经有值了,直接赋值即可
        }

    }
}


虽然切面中的代码和之前的原始代码很类似,但这是在切面里面,切面可以用在很多不同的地方,在需要使用的地方只需要像特性那样使用就可以了,很方便。
首次调用get时,OnGetValue会被调用,一开始支持字段_backingField=null,因此需要像之前那样加锁并双重检查,然后args.ProceedGetValue()告诉PostSharp继续运行get中的代码,这时,就会创建一个SlowConstructor的实例,然后,就会使用get执行的结果填充args.Value。然后我们把该值存入支持字段中,方便下次使用。
之后,每个后续调用,PostSharp都会将支持字段的值设置给args.Value,因此args.ProceedGetValue()只会在首次调用,这样,就不需要每次都实例化类了。有了这个切面,我们就有了和Lazy<T>类似的语法了,而且可以直接访问属性。

直接在需要懒加载的属性上以特性的方式使用:

[MyLazyLoadingGetterAspect]
static SlowConstructor SlowService
{
    get { return new SlowConstructor();}
}


执行结果如下:

从上面的运行结果可以看出,只创建了1个实例,因而,大大提高了性能。
我们都知道,字段没有get,因此对字段进行懒加载稍微有点不同。

如何懒加载字段?

使用反射的Activator

字段是类级别的变量,这就意味着我们不能找到一种方法显式指定应该如何懒加载一个字段。假设我们以隐式的方式懒加载指定的字段,首先,编写代码如下,这次用的不是属性,而是字段:

#region 2.0 懒加载字段

private static SlowConstructor SlowService;
#endregion
static void Main(string[] args)
{
    SlowService.DoSomething();
    SlowService.DoSomething();

    Console.Read();
}


最简单的做法就是使用反射的Activator创建字段类型的实例,下面我们创建一个继承了LocationInterceptionAspect的切面,然后用于该字段:

#region 2.0 懒加载字段
[MyLazyLoadingFieldAspect]
private static SlowConstructor SlowService;
#endregion
static void Main(string[] args)
{
    SlowService.DoSomething();
    SlowService.DoSomething();

    Console.Read();
}

[Serializable]
public sealed class MyLazyLoadingFieldAspect : LocationInterceptionAspect
{
    private object _backingField;
    readonly object _syncRoot=new object();
    public override void OnGetValue(LocationInterceptionArgs args)
    {
        if (_backingField==null)
        {
            lock (_syncRoot)
            {
                if (_backingField==null)
                {
                    _backingField = Activator.CreateInstance(args.Location.LocationType);//Activator会使用位置的类型创建一个新对象
                }
            }
        }
        args.Value = _backingField;
    }

}

反射之Activator

反射是位于System.Reflection命名空间下的一系列工具,它允许我们编写一些在程序运行时进行读取或者生成代码的代码。Activator可以创建运行时中对象的新实例,这在直到运行时才知道该实例化哪种类型的对象时很有用。上面的切面可以在任何类型的字段上重复使用,但是这种灵活性也带来了性能损耗,因此,确保必要的时候才使用反射。

上面的代码和之前懒加载属性切面的代码很相似,但是我们这里应该注意的是不同点,比如,这里没有使用args.ProceedGetValue(),而是使用了Activator.CreateInstance()。PostSharp的args.Location.LocationType可以告诉我们被拦截位置的类型Type(字段和属性都可以),有了这个信息,我们就可以使用System.Activator创建那个类型的实例了。和之前一样,将结果存到支持字段_backingField中。

这种方法适用面很窄,更加现实的方式是使用工厂,服务定位器或者IoC容器取代Activator。比如,如果使用的是StructureMap(一个流行的.Net IoC工具),那么可以使用ObjectFactory.GetInstance代替Activator,这种方法可以让我们对更复杂的依赖(即,没有无参构造函数的类)使用懒加载。

使用IoC工具

假设SlowConstructor只有一个构造函数,并且该构造函数有一个IMyService参数,修改之后的代码如下:

public class SlowConstructor
{
    //public SlowConstructor()
    //{
    //    Console.WriteLine("正在初始化SlowConstructor,请稍等...");
    //    Thread.Sleep(5000);
    //}
    private IMyService _myService;
    public SlowConstructor(IMyService myService)//只有一个构造函数,并且需要一个参数
    {
        _myService = myService;
        Console.WriteLine("正在初始化SlowConstructor,请稍等...");
        Thread.Sleep(5000);
    }
    //public void DoSomething()
    //{
    //    Console.WriteLine("{0}:正在处理一些业务...",DateTime.Now);
    //}

    public void DoSomething()
    {
        _myService.DoSomething();
    }
}

public interface IMyService
{
    void DoSomething();
}

public class MyService:IMyService
{
    public void DoSomething()
    {
        Console.WriteLine("{0}:正在处理一些业务...", DateTime.Now);
    }
}


在切面中,仍然可以使用Activator创建对象,但是同时必须创建该对象依赖的对象,在上面的代码中就是MySevice,在一个真实应用中,依赖链可能会更长或更复杂,因此,一般都会把这个任务交给一个工具,比如StructureMap。下面的代码是如何在控制台的Main方法中初始化StructureMap,其它的IoC工具都是类似的【下一个系列教程就是关于DI/IoC的】:

#region 2.0 懒加载字段
//[MyLazyLoadingFieldAspect]
[LazyLoadStructureMapAspect]
private static SlowConstructor SlowService;
#endregion
static void Main(string[] args)
{
    //ObjectFactory.Initialize告诉StructureMap使用哪个实现
    ObjectFactory.Initialize(cfg =>
    {
        cfg.For<IMyService>().Use<MyService>();//当调用IMyService的构造函数时,使用MyService作为实现
        cfg.For<SlowConstructor>().Use<SlowConstructor>();//这行代码可选,StructureMap会自动绑定
    });
    SlowService.DoSomething();
    SlowService.DoSomething();

    Console.Read();
}


现在依赖配置好了,并且给字段添加了新的特性切面。这里简单介绍一下StructureMap的依赖配置,下一个系列教程会详细讲解哦!首先使用ObjectFactory.Initialize【已经过时了,在新版本已经不建议使用这种方式】指定依赖,如果StructureMap请求IMyService的实现,那么就会返回MyService对象,如果请求的是SlowConstructor,那么就会使用SlowConstructor。更重要的是,当创建SlowConstructor的实例时,它会识别出SlowConstructor的构造函数有一个IMyService类型的参数,因此会自动使用配置的依赖并传给该构造函数MyService的新实例。

下面我们需要创建一个新切面,和之前使用Activator的例子看起来很像,但是这次使用了ObjectFactory.GetInstance而不是Activator,这样StructureMap会自动提供需要的对象:

[Serializable]
public class LazyLoadStructureMapAspect:LocationInterceptionAspect
{
    private object _backingField;
    readonly object _syncRoot=new object();
    public override void OnGetValue(LocationInterceptionArgs args)
    {
        if (_backingField==null)
        {
            lock (_syncRoot)
            {
                if (_backingField==null)
                {
                    var locationType = args.Location.PropertyInfo.PropertyType;
                    _backingField= ObjectFactory.GetInstance(locationType);
                }

            }
        }
        args.Value = _backingField;
    }
}


执行结果和之前的一样,只不过这次的例子更加具有现实意义,因为类有关于接口的依赖,配置这些依赖使用了IoC工具。

适当的使用懒加载可以改善耗时操作的性能,AOP通过拦截访问的字段和属性以及将样板代码移到单独的切面类中使得对位置进行懒加载不再那么痛苦。字段或属性可能会遇到样板代码问题的其它地方在可响应的GUI。

真实案例——INotifyPropertyChanged

在桌面应用中使用INotifyPropertyChanged

首先创建一个WPF应用,搭建的界面如下图所示:

需求是,当输入进行输入时,需要将姓和名两个文本框中的内容连接起来填充到姓名那行所在的Label控件上。在WPF中一种普遍的做法是创建一个封装数据(姓和名)和导出数据(姓名)的视图模型。创建视图模型NameViewModel如下:

public class NameViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullName
    {
        get { return string.Format("{0}{1}", FirstName, LastName); }
    }
}


还需要做以下几步才能实现需求:

  1. 将该视图模型的一个实例绑定到WPF窗体的数据上下文DataContext上,这可以在xaml的代码后置类中对窗体的DataContext属性赋值;
  2. 将该视图模型的每个属性分别绑定到各自的文本框和Label控件上,这可以通过分别将视图模型的属性绑定TextBox和Label控件的TextContent属性上完成;
  3. 告诉两个文本框,无论何时文本框内容变化时,都应该触发一个更新,因此,修改姓和名所对应的文本框,指定Binding中的UpdateSourceTrigerPropertyChanged,目的是当用户输入时,告诉它们更新源数据。
  4. 让视图模型类NameViewModel实现INotifyPropertyChanged,实现这个接口需要做的唯一一件事就是PropertyChangedEventHandler类型的事件。由于已经将绑定添加到了文本框上,WPF会自动寻找要触发的事件,这意味着我们必须在每个属性的setter方法中编码来触发该属性名所对应的事件。

第一步:

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        DataContext=new NameViewModel();
    }
}


第二步,第三步:

<StackPanel Orientation="Horizontal">
            <Label Content="姓:" Width="100"/>
            <TextBox Height="23" Width="200" Text="{Binding Path=FirstName,UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>
<StackPanel Orientation="Horizontal">
            <Label Content="名:" Width="100"/>
            <TextBox Height="23" Width="200" Text="{Binding Path=LastName,UpdateSourceTrigger=PropertyChanged}" />
        </StackPanel>


第四步:

public class NameViewModel:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged!=null)
            {
                PropertyChanged(this,new PropertyChangedEventArgs(propertyName));
            }
        }

    private string _firstName;

    public string FirstName
        {
            get { return _firstName; }
            set
            {
                if (value!=_firstName)
                {
                    _firstName = value;
                    OnPropertyChanged("FirstName");
                    OnPropertyChanged("FullName");
                }
            }
        }

    private string _lastName;

    public string LastName
        {
            get { return _lastName; }
            set
            {
                if (value!=_lastName)
                {
                    _lastName = value;
                    OnPropertyChanged("LastName");
                    OnPropertyChanged("FullName");
                }
            }
        }

    public string FullName
    {
        get { return string.Format("{0}{1}", FirstName, LastName); }
    }

   
}


如果你对WPF熟悉的话,那么上面的代码没什么可说的:无论何时在这些属性上使用了set,PropertyChanged事件都会被触发。比如,在姓的文本框上输入了A,那么就会导致FirstName的属性值被设置set。在set期间,触发了两次PropertyChanged:一次是宣布FirstName属性修改了,然后是宣布FullName属性修改了。

编译、运行程序,结果如下:

虽然这个例子不是很复杂,但是在真实的WPF应用中,可能会有更多的字段以及这些字段之间关系更复杂,如果熟悉MVVM(Model-View-ViewModel)模式的话,那么这种类型的绑定对于实现那种模式很重要。此外,虽然这只是一个简单的示例,但是NameViewModel从一个只有自动属性的小类变得越来越大,代码越来越多,有了支持字段,而且每个set方法中还要逻辑。虽然可以在View和ViewModel之间进行干净的分离,但是使用INotifyPropertyChanged会面临很多陷阱和问题。

使用INotifyPropertyChanged的问题和约束

虽然使用INotifyPropertyChanged有很多好处,但是也有很多弊端,比如,潜在产生了样板代码,脆弱的代码以及可能维护起来困难的代码。
产生的样板代码很明显,因为从最原始的只有三个自动属性的NameViewModel类现在体积已经膨胀了好几倍,有了显式的支持字段,setter里面也有了逻辑,因此,这里有很多重复,可以使用AOP减少重复。
其次,要触发PropertyChanged事件,就需要有一个PropertyChangedEventArgs对象,它需要一个字符串来识别已经改变的属性。因此,当每次调用OnPropertyChanged时,需要传一个和属性名称对应的字符串,如果不小心手误输错了,就会导致触发事件失败。
最后,使用INotifyPropertyChanged很难维护。因为它使用了字符串,如果更改了属性名,就必须记得也要修改字符串(安装了ReSharp等重构工具时,如果重命名属性,ReSharp可以帮我们完成这件事)。还要注意,因为我们有一个导出属性(FullName),所以要记得当发送关于其它属性更改的消息时要包括该属性。

使用ReSharp重构
虽然属性名FirstName和字符串“FirstName”对于我们人类来说看起来是相同的,但是对于编译器它们是不同的符号,如果更改了一个符号,编译器不会聪明到也能意识到其它相关的符号,当运行代码时最终会报错。
一些重构工具比如ReSharp,Telerik JustCode等都会尝试使用智能分析和演绎找出相关的符号。比如,当使用ReSharp重命名FirstName属性时,它可能会询问你是否想要更改“FirstName”字符串的值。

不使用AOP也可以缓解这些问题,比如可以写单元测试或者防御性编程,它们可以验证所有将会发送的正确通知。虽然可以使用发射使得这件事简单些,但是会潜在产生很多代码。(比如可以使用反射可以循环遍历类的所有属性,获得属性名称,进而确保当事件触发时属性名称时匹配的)。

如果使用了.NET 4.5,那么可以使用一个叫做CallMemberName的新工具来处理INotifyPropertyChanged,CallMemberName是一个特性,可以使用它将一个参数设置成调用的属性的名称。这里我们可以使用它来减少NameViewModel类中字符串的依赖,代码如下:

public class NameViewModel2:INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

  

    void OnPropertyChanged([CallerMemberName]string propertyName="")
        {
            if (PropertyChanged!=null)
            {
                PropertyChanged(this,new PropertyChangedEventArgs(propertyName));
            }
        }

    string _firstName;
    public string FirstName
        {
            get { return _firstName; }
            set {
                if (value!=_firstName)
                {
                    _firstName = value;
                    OnPropertyChanged();//这里就可以移除“FirstName”了
                    OnPropertyChanged("FullName");
                }
            }
        }
    string _lastName;
    public string LastName
        {
            get { return _lastName; }
            set
            {
                if (value != _firstName)
                {
                    _lastName = value;
                    OnPropertyChanged();//这里就可以移除“LastName”了
                    OnPropertyChanged("FullName");
                }
            }
        }
    public string FullName
        {
            get
            {
                return string.Format("{0} {1}", _firstName, _lastName);
            }
        }
}


这是一种改善,重命名属性FirstName和LastName不再是问题了,因为.Net Framework会帮我们填充空白。拼写失误也不是问题了,因为CallMemberName只会使用属性的名称,但是还必须通知FullName更改了,因为它是一个导出属性。而且仍然有很多样板代码,包括显式支持字段和许多setter中的代码。
我们可以使用AOP来处理这些问题,下面我们就使用一个新的AOP工具来协助处理INotifyPropertyChanged。

使用AOP减少样板代码

之前已经使用PostSharp和Castle Dynamic这两个AOP框架演示了很多例子,现在再引入一个新的框架,这个框架之前叫INotifyPropertyWeaver,是专为INotifyPropertyChanged而生的,然而,现在这个框架已经弃用了,在网上基本找不到关于它的消息了,然而,他的作者将它集成到了Fody项目中,而且它现在的名字叫PropertyChanged.Fody,安装时,直接在Nuget控制台输入Install-Package PropertyChanged.Fody即可。
安装好之后,我们只需要定义一个原始的NameViewModel类:

public class NameViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }

    public string FullName
    {
        get
        {
            return String.Format("{0}{1}", FirstName, LastName);
        }
    }
}


然后,神奇的地方来了,只需要在这个类的上方添加一个特性ImplementPropertyChanged即可,当然前面的数据绑定的步骤还是要做的:

[ImplementPropertyChanged]
public class NameViewModel
{

运行程序,如下:

简直太方便了,只需要加一个特性就把之前要写那么多代码的事情完成了。事实上,这个特性做的就是这件事,代码编译之后的效果和我们写那么多代码是一样的。

当然,这个工具里面还要很多其它特性,感兴趣的同学可以去Github上去学习,链接在此:https://github.com/Fody/PropertyChanged/wiki。

PropertyChanged.Fody帮我们填充了所有的间隙,它很聪明,可以探测出导出属性,从而填充通知间隙。PostSharp也以PostSharp Ultimate的形式提供了类似的功能。

PostSharp Ultimate
PostSharp Ultimate收集了很多现成的开源切面,包括的现成切面如下:

  • 多线程
  • 诊断(logging)
  • 有限的INotifyPropertyChanged切面
    这些工具都是免费的,但是它们要求PostSharp是全商业版本。这些工具的优势是可以获得AOP的所有优势来解决特定的问题,不需要从零开始编写切面。当然,我们也可以使用开源免费的Fody代替。

接下来我们使用PostSharp来实现和上面使用ProperyChanged.Fody相同的功能。先安装Postsharp,再创建一个叫做NotifyPropertyChangedAspect切面类,它继承于LocationIntercetionAspect:

[Serializable]
public class NotifyPropertyChangedAspect:LocationInterceptionAspect
{
    private string[] _derivedProperties;
    public NotifyPropertyChangedAspect(params string[] derived)//构造函数参数为可变长参数,用于接收导出属性
    {
        _derivedProperties = derived;
    }
    public override void OnSetValue(LocationInterceptionArgs args)
    {
        //base.OnSetValue(args);
    }
}


回忆一下,之前我们一开始实现INotifyPropertyChanged时,困难的工作都放到setter中了,因此,这里需要重写OnSetValue方法,这个方法会在使用属性的setter时运行,而且它会代替setter运行。

在OnSetValue里面,需要做2件事:

  1. 比较新值和旧值,如果不等,那么应该允许set操作通过(使用PostSharp的args.ProceedSetValue方法);
  2. 需要通知已经改变的属性(包括构造函数中指定的任何导出属性)。创建可以重复使用的RaisePropertyChanged方法,对指定的所有导出属性进行循环遍历,并调用RaisePropertyChanged方法。代码如下:
[Serializable]
public class NotifyPropertyChangedAspect:LocationInterceptionAspect
{
    private string[] _derivedProperties;
    public NotifyPropertyChangedAspect(params string[] derived)//构造函数参数为可变长参数,用于接收导出属性
    {
        _derivedProperties = derived;
    }
    public override void OnSetValue(LocationInterceptionArgs args)
    {
        var oldValue = args.GetCurrentValue();
        var newValue = args.Value;
        if (oldValue!=newValue)
        {
            args.ProceedSetValue();
            RaisePropertyChanged(args.Instance, args.LocationName);//只要属性执行了setter,就触发RaisePropertyChanged事件
            if (_derivedProperties!=null)
            {
                //对每个导出属性触发事件
                foreach (string derivedProperty in _derivedProperties)
                {
                    RaisePropertyChanged(args.Instance,derivedProperty);
                }
            }
        }
    }

    private void RaisePropertyChanged(object p1, string p2)
    {
        throw new NotImplementedException();
    }
}


RaisePropertyChanged待会再实现。先来学习一下之前没有碰到过的PostSharp的API。args.GetCurrentValue获取当前的位置值,但是它还没有把值value放到args.Value。因此,这里把它存储在oldValue变量中再合适不过了。args.Value返回即将到来的位置值。args.ProceedSetValue指示PostSharp允许继续set操作。
如果属性值发生了变化,那么我们就触发属性改变的事件。看一下传入的实参,args.Instance返回的是属性所在的对象(比如,NameViewModel类的实例),它应该是一个实现了INotifyPropertyChanged的类。args.LocationName返回被拦截的属性名,比如可能是FirstName或LastName。

当属性更改的通知发出之后,遍历所有指定的导出属性(如FullName),并为这些属性调用RaisePropertyChanged方法。下面我们完成最后这个切面并写完RaisePropertyChanged方法。在该方法中,你期望找到传入的实例对象上的PropertyChanged事件,并使用传入的位置名触发那个事件。然而,只有纯粹的一个对象object传入,所以必须借助反射来处理:


private void RaisePropertyChanged(object instance, string propertyName)
{
    var type = instance.GetType();
    var propertyChanged = type.GetField("PropertyChanged", BindingFlags.Instance|BindingFlags.NonPublic);
    var handler = propertyChanged.GetValue(instance) as PropertyChangedEventHandler;
    handler(instance,new PropertyChangedEventArgs(propertyName));
}


这个方法中没使用任何PostSharp API,只有反射的API。反射会检索实例instance的类型,从该类型中可以找到PropertyChanged事件字段。使用那个字段可以调用事件。

这里使用发射,是因为从类外面触发事件的唯一方式就是反射了。这样做并不好,因为反射是一个缓慢的过程,这样编写切面的话意味着属性每次改变时都会执行反射。此外,如果这个切面用在一个没有PropertyChanged事件的类上,那么就会报错。(解决办法请关注后面的教程,特别是PostSharp的CompileTimeValidate功能)

小结

这节我们覆盖了一个新的拦截类型:拦截属性和字段(位置)。和拦截方法一样,位置拦截切面扮演着getter/setter和处理getting/setting代码之间的中间人。

C#中的属性提供了简明的方式编写getter/setter方法,可以拦截方法的工具也可以拦截属性(比如Castle DynamicProxy)。但PostSharp为位置提供了一个特殊的类,该API可以同时为属性和字段服务。和方法拦截一样,我们可以继续执行get/setca操作,也可以获得关于位置的信息(比如字段名和属性名),实例对象等等。

这节也引入了一个新的AOP工具——PropertyChanged.Fody,这个工具很专一,只做一件事,不像PostSharp和Castle DynamicProxy是通用框架。

现在,我们已经可以编写拦截方法、边界方法、拦截位置的切面了。但是学习AOP不仅仅是数量(可以少写代码),而且还有质量。下一篇我们看下如何将单元测试和切面结合起来。

posted @ 2016-08-30 10:02  tkbSimplest  阅读(11300)  评论(28编辑  收藏  举报