.Net中的AOP系列之《攔截位置》
本篇目錄
本節(jié)的源碼本人已托管于Coding上:點(diǎn)擊查看。
本系列的實(shí)驗(yàn)環(huán)境:VS 2013 Update 5(建議最好使用集成了Nuget的VS版本,VS Express版也夠用),安裝了PostSharp。
至今,我們的關(guān)注點(diǎn)都是集中在方法上,本節(jié),就看一下位置,這里的位置指的是字段或?qū)傩浴N恢脭r截是AOP框架不太通用的功能,因此,本節(jié)大多數(shù)的例子都是使用支持位置的PostSharp框架,此外,這節(jié)還會(huì)看到一個(gè)特殊的AOP工具(與常見(jiàn)的AOP框架截然不同),叫做PropertyChanged.Fody。
位置攔截
也許很多人沒(méi)有聽(tīng)過(guò)C#中有位置這一說(shuō),其實(shí),一個(gè)字段或者一個(gè)屬性都是一個(gè)位置。字段和屬性是OOP中常見(jiàn)的東西,它們?yōu)?strong>類(lèi)提供數(shù)據(jù)和結(jié)構(gòu)。下面簡(jiǎn)單復(fù)習(xí)一下,覺(jué)得沒(méi)問(wèn)題的同學(xué)可以直接跳過(guò),記住:屬性只是getter/setter方法的語(yǔ)法糖。
.Net中的字段和屬性
字段是類(lèi)的成員。它們可以聲明為public,private,protected,internal等等,這樣就可以限制訪(fǎng)問(wèn)級(jí)別了(默認(rèn)是private)。
通常,如果封裝很重要的話(huà),就不會(huì)使用public字段,因此,字段通常被設(shè)置成private,然后通過(guò)訪(fǎng)問(wèn)器方法在類(lèi)外面使用該字段。如果有一個(gè)private的_balance(余額)字段,那么只能通過(guò)其它對(duì)象調(diào)用Deposit(存款)或者Withdrawal(取款)方法來(lái)改變這個(gè)字段的值:
public class BankAccount {
decimal _balance;
public void SetBalance(decimal amount) {
_balance = amount;
}
public decimal GetBalance(decimal amount) {
return _balance;
}
}
在C#中,我們可以使用屬性語(yǔ)法(get 和set )來(lái)減少代碼量,下面的代碼中,Balance屬性封裝了一個(gè)私有字段_balance:
public class BankAccount {
decimal _balance;
public decimal Balance {
get {
return _balance;
}
set {
_balance = value;
}
}
}
get 和set都是可選的:如果不需要設(shè)置一個(gè)字段的值,那么就不需要寫(xiě)setter,getter同樣如此。但是,這后面,.Net編譯器幫我們創(chuàng)建了方法,如果使用反編譯工具如ILSpy看一下IL代碼,就會(huì)發(fā)現(xiàn)編譯器創(chuàng)建了一個(gè)decimal get_Balance()方法和一個(gè)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代碼
}
}
自動(dòng)屬性是在C#2.0中引入的,這個(gè)工具讓語(yǔ)法糖變得更甜了,我們甚至不需要顯式創(chuàng)建字段就可以創(chuàng)建一個(gè)屬性,如下:
public class MyClass {
public string MyProperty {get; set;}
}
當(dāng)使用自動(dòng)屬性時(shí),必須同時(shí)使用get和set,但是可以使用不同的訪(fǎng)問(wèn)級(jí)別。比如,get可以設(shè)置成公共的,set可以設(shè)置成私有的。
對(duì)于我們.Net開(kāi)發(fā)者來(lái)說(shuō),這并不是什么新鮮事兒,因?yàn)槲覀儙缀趺刻於紩?huì)使用這些,但是越是最常用的東西,通常你也認(rèn)為最理所當(dāng)然,因此,在深入涉及位置攔截的AOP代碼之前有必要重溫一下細(xì)節(jié)問(wèn)題。
PostSharp位置攔截
之前的教程我們知道了,AOP工具可以攔截方法,那么從上面我們又知道,屬性的底層就是方法,因此,我們可以猜想可以在屬性上使用方法攔截切面。事實(shí)上這是可行的,可以使用PostSharp或Castle DynamicProxy在屬性上創(chuàng)建方法攔截。下面就是一個(gè)使用PostSharp在屬性上創(chuàng)建方法攔截的控制臺(tái)例子:
public class TestClass
{
public string TestProperty
{
get;
[MyMethodAspect]
set;//在一個(gè)屬性的setter上使用方法攔截切面
}
}
[Serializable]
public class MyMethodAspect:MethodInterceptionAspect
{
public override void OnInvoke(MethodInterceptionArgs args)
{
Console.WriteLine("這條語(yǔ)句來(lái)自自定義方法攔截切面");
args.Proceed();
}
}
class Program
{
static void Main(string[] args)
{
var test=new TestClass();
test.TestProperty = "測(cè)試屬性";//這里會(huì)調(diào)用屬性的setter方法
Console.Read();
}
}
效果如下:

但是這樣使用有幾個(gè)問(wèn)題:
- 笨拙。可能必須寫(xiě)兩個(gè)切面,一個(gè)給setter,一個(gè)給getter。
- 只能給屬性使用切面,字段的底層不是方法,所以行不通。
沒(méi)關(guān)系,PostSharp給我們提供了一個(gè)更方便的方法,只需要寫(xiě)一個(gè)類(lèi)就可以處理getting和setting,還允許為字段和屬性編寫(xiě)切面。這就是PostSharp中的LocationInterceptionAspect,下面的例子和上面的一樣,只是這次使用了LocationInterceptionAspect:
[Serializable]
public class MyLocationAspect:LocationInterceptionAspect
{
public override void OnGetValue(LocationInterceptionArgs args)
{
Console.WriteLine("這條語(yǔ)句來(lái)自位置攔截的{0}方法",MethodBase.GetCurrentMethod());
args.ProceedGetValue();
}
public override void OnSetValue(LocationInterceptionArgs args)
{
Console.WriteLine("這條語(yǔ)句來(lái)自位置攔截的{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 = "測(cè)試屬性";
var test2=new TestClass2();
test2.TestProperty = "位置攔截測(cè)試";
Console.WriteLine(test2.TestProperty);
Console.Read();
}
Main方法中,先是給屬性賦值,所以會(huì)被MyLocationAspect的OnSetValue方法攔截到,然后打印test2.TestProperty時(shí)會(huì)被OnGetValule方法攔截,因此運(yùn)行結(jié)果如下:

這里新出現(xiàn)的args.ProceedSetValue();和 args.ProceedGetValue();和之前的args.Proceed();是一樣的道理,是繼續(xù)執(zhí)行屬性方法(屬性的本質(zhì)就是方法)的意思。
真實(shí)案例——懶加載
懶加載的目的就是延遲一些耗時(shí)操作的執(zhí)行,相反,預(yù)加載的目的是一個(gè)或多個(gè)操作在得到結(jié)果前每次都要執(zhí)行,以防需要這些操作。NHibernate和EF都是用在持久層的數(shù)據(jù)庫(kù)工具,當(dāng)使用懶加載從DB中檢索實(shí)體時(shí),它們只會(huì)拉取你需要的實(shí)體而不會(huì)拉取相關(guān)實(shí)體,相反,使用預(yù)加載,它們就會(huì)把你需要的實(shí)體(比如A),和該實(shí)體相關(guān)的實(shí)體(B),以及和B相關(guān)的實(shí)體(C)等等都會(huì)加載出來(lái)。此時(shí),就需要在性能和方便之間進(jìn)行權(quán)衡了。
.Net中的懶加載
懶加載的一種方式是使用具有字段的屬性來(lái)實(shí)現(xiàn)。當(dāng)首次使用get時(shí),會(huì)創(chuàng)建一個(gè)新對(duì)象,后續(xù)再使用字段時(shí)都會(huì)像以往那樣返回字段。如下所示:
SlowConstructor _myProperty;
public SlowConstructor MyProperty {
get {
if (_myProperty == null)
_myProperty = new SlowConstructor();
return _myProperty;
}
}
細(xì)心的你可能會(huì)發(fā)現(xiàn)這不是線(xiàn)程安全的代碼,如果這是一個(gè)關(guān)注點(diǎn)的話(huà),就需要放一個(gè)lock語(yǔ)句。這里使用雙重檢查的鎖機(jī)制再合適不過(guò)了,因?yàn)樵诘谝淮螜z查和lock之間可能會(huì)發(fā)生競(jìng)爭(zhēng)情況(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之間可能有另一個(gè)線(xiàn)程正在給字段賦值
return _myProperty;
}
}
這樣,就可以使用懶加載了。你可以像平時(shí)那樣訪(fǎng)問(wèn)屬性,如果不用它的話(huà),那么SlowConstructor永遠(yuǎn)都不會(huì)運(yùn)行。也可以使用工廠(chǎng)或者IoC工具代替new來(lái)實(shí)例化對(duì)象。但無(wú)論怎樣,lock,兩次if檢查和字段都始終是保持不變的。
從.NET 4.0開(kāi)始,.Net Framework提供了System.Lazy<T>,它是一個(gè)方便類(lèi),可以使用更少的代碼完成和上面相同的事情。代碼如下:
var lazy = new Lazy<SlowConstructor>(()=>new SlowConstructor());
工廠(chǎng)代碼是以L(fǎng)ambda表達(dá)式(匿名函數(shù))傳入的,這就告訴Lazy首次訪(fǎng)問(wèn)時(shí)使用這個(gè)代碼來(lái)實(shí)例化對(duì)象,System.Lazy<T>默認(rèn)也是線(xiàn)程安全的,因此它封裝了所有的lock代碼。但是,跟前面那個(gè)例子不同的是,這樣字段就成了LazySlowConstructor c = MyProperty.Value;。
現(xiàn)在,想要使用懶加載時(shí)有兩種選擇,第一種有許多樣板代碼和字段,第二種使用Lazy
使用AOP實(shí)現(xiàn)懶加載
結(jié)合上面兩種方法的優(yōu)點(diǎn),那就是可以直接訪(fǎng)問(wèn)屬性(不需要通過(guò)Value屬性),而且也沒(méi)有很多的樣板代碼,就像下面這個(gè)樣子:
[LazyLoadGetter]//使用特性告訴PostSharp這個(gè)屬性是懶加載屬性
static SlowConstructor MyProperty {
get { return(new SlowConstructor() ); }
}
get方法體內(nèi)包含了懶加載的工廠(chǎng),直到get執(zhí)行時(shí)才會(huì)調(diào)用,后續(xù)的get調(diào)用也會(huì)使用首次操作的結(jié)果。
下面?zhèn)鲃?chuàng)建一個(gè)控制臺(tái)應(yīng)用,命名為LazyLoadingDemo,安裝PostSharp。定義一個(gè)模擬耗時(shí)的操作SlowConstructor(比如一個(gè)調(diào)用了一個(gè)很慢的web service等):
public class SlowConstructor
{
public SlowConstructor()
{
Console.WriteLine("正在初始化SlowConstructor,請(qǐng)稍等...");
Thread.Sleep(5000);//睡5秒,模擬耗時(shí)操作
}
public void DoSomething()
{
Console.WriteLine("{0}:正在處理一些業(yè)務(wù)...",DateTime.Now);
}
}
在Main方法種定義該類(lèi)的一個(gè)屬性,并連續(xù)調(diào)用該類(lèi)的DoSomething方法:
class Program
{
static SlowConstructor SlowService
{
get { return new SlowConstructor();}
}
static void Main(string[] args)
{
SlowService.DoSomething();
SlowService.DoSomething();
Console.Read();
}
}
這樣寫(xiě)代碼的話(huà)就應(yīng)該優(yōu)化了,因?yàn)槊看握{(diào)用屬性的get方法時(shí)都會(huì)重新實(shí)例化SlowConstructor對(duì)象。
執(zhí)行結(jié)果很明顯,如下:

但我們這里計(jì)劃的是懶加載這個(gè)屬性,不需要添加所有的字段、雙重檢查鎖,或切換使用Lazy<T>,這里我們可以創(chuàng)建一個(gè)切面,該切面繼承PostSharp的LocationInterceptionAspect,然后把自定義的切面當(dāng)作特性用在需要懶加載的屬性上即可:
[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();//繼續(xù)像往常那樣執(zhí)行g(shù)et
_backingField = args.Value;//將get獲得的屬性值保存到支持字段中
}
}
args.Value = _backingField;//因?yàn)橹С肿侄沃幸呀?jīng)有值了,直接賦值即可
}
}
}
雖然切面中的代碼和之前的原始代碼很類(lèi)似,但這是在切面里面,切面可以用在很多不同的地方,在需要使用的地方只需要像特性那樣使用就可以了,很方便。
首次調(diào)用get時(shí),OnGetValue會(huì)被調(diào)用,一開(kāi)始支持字段_backingField=null,因此需要像之前那樣加鎖并雙重檢查,然后args.ProceedGetValue()告訴PostSharp繼續(xù)運(yùn)行g(shù)et中的代碼,這時(shí),就會(huì)創(chuàng)建一個(gè)SlowConstructor的實(shí)例,然后,就會(huì)使用get執(zhí)行的結(jié)果填充args.Value。然后我們把該值存入支持字段中,方便下次使用。
之后,每個(gè)后續(xù)調(diào)用,PostSharp都會(huì)將支持字段的值設(shè)置給args.Value,因此args.ProceedGetValue()只會(huì)在首次調(diào)用,這樣,就不需要每次都實(shí)例化類(lèi)了。有了這個(gè)切面,我們就有了和Lazy<T>類(lèi)似的語(yǔ)法了,而且可以直接訪(fǎng)問(wèn)屬性。
直接在需要懶加載的屬性上以特性的方式使用:
[MyLazyLoadingGetterAspect]
static SlowConstructor SlowService
{
get { return new SlowConstructor();}
}
執(zhí)行結(jié)果如下:

從上面的運(yùn)行結(jié)果可以看出,只創(chuàng)建了1個(gè)實(shí)例,因而,大大提高了性能。
我們都知道,字段沒(méi)有get,因此對(duì)字段進(jìn)行懶加載稍微有點(diǎn)不同。
如何懶加載字段?
使用反射的Activator
字段是類(lèi)級(jí)別的變量,這就意味著我們不能找到一種方法顯式指定應(yīng)該如何懶加載一個(gè)字段。假設(shè)我們以隱式的方式懶加載指定的字段,首先,編寫(xiě)代碼如下,這次用的不是屬性,而是字段:
#region 2.0 懶加載字段
private static SlowConstructor SlowService;
#endregion
static void Main(string[] args)
{
SlowService.DoSomething();
SlowService.DoSomething();
Console.Read();
}
最簡(jiǎn)單的做法就是使用反射的Activator創(chuàng)建字段類(lèi)型的實(shí)例,下面我們創(chuàng)建一個(gè)繼承了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會(huì)使用位置的類(lèi)型創(chuàng)建一個(gè)新對(duì)象
}
}
}
args.Value = _backingField;
}
}
反射之Activator
反射是位于
System.Reflection命名空間下的一系列工具,它允許我們編寫(xiě)一些在程序運(yùn)行時(shí)進(jìn)行讀取或者生成代碼的代碼。Activator可以創(chuàng)建運(yùn)行時(shí)中對(duì)象的新實(shí)例,這在直到運(yùn)行時(shí)才知道該實(shí)例化哪種類(lèi)型的對(duì)象時(shí)很有用。上面的切面可以在任何類(lèi)型的字段上重復(fù)使用,但是這種靈活性也帶來(lái)了性能損耗,因此,確保必要的時(shí)候才使用反射。
上面的代碼和之前懶加載屬性切面的代碼很相似,但是我們這里應(yīng)該注意的是不同點(diǎn),比如,這里沒(méi)有使用args.ProceedGetValue(),而是使用了Activator.CreateInstance()。PostSharp的args.Location.LocationType可以告訴我們被攔截位置的類(lèi)型Type(字段和屬性都可以),有了這個(gè)信息,我們就可以使用System.Activator創(chuàng)建那個(gè)類(lèi)型的實(shí)例了。和之前一樣,將結(jié)果存到支持字段_backingField中。
這種方法適用面很窄,更加現(xiàn)實(shí)的方式是使用工廠(chǎng),服務(wù)定位器或者IoC容器取代Activator。比如,如果使用的是StructureMap(一個(gè)流行的.Net IoC工具),那么可以使用ObjectFactory.GetInstance代替Activator,這種方法可以讓我們對(duì)更復(fù)雜的依賴(lài)(即,沒(méi)有無(wú)參構(gòu)造函數(shù)的類(lèi))使用懶加載。
使用IoC工具
假設(shè)SlowConstructor只有一個(gè)構(gòu)造函數(shù),并且該構(gòu)造函數(shù)有一個(gè)IMyService參數(shù),修改之后的代碼如下:
public class SlowConstructor
{
//public SlowConstructor()
//{
// Console.WriteLine("正在初始化SlowConstructor,請(qǐng)稍等...");
// Thread.Sleep(5000);
//}
private IMyService _myService;
public SlowConstructor(IMyService myService)//只有一個(gè)構(gòu)造函數(shù),并且需要一個(gè)參數(shù)
{
_myService = myService;
Console.WriteLine("正在初始化SlowConstructor,請(qǐng)稍等...");
Thread.Sleep(5000);
}
//public void DoSomething()
//{
// Console.WriteLine("{0}:正在處理一些業(yè)務(wù)...",DateTime.Now);
//}
public void DoSomething()
{
_myService.DoSomething();
}
}
public interface IMyService
{
void DoSomething();
}
public class MyService:IMyService
{
public void DoSomething()
{
Console.WriteLine("{0}:正在處理一些業(yè)務(wù)...", DateTime.Now);
}
}
在切面中,仍然可以使用Activator創(chuàng)建對(duì)象,但是同時(shí)必須創(chuàng)建該對(duì)象依賴(lài)的對(duì)象,在上面的代碼中就是MySevice,在一個(gè)真實(shí)應(yīng)用中,依賴(lài)鏈可能會(huì)更長(zhǎng)或更復(fù)雜,因此,一般都會(huì)把這個(gè)任務(wù)交給一個(gè)工具,比如StructureMap。下面的代碼是如何在控制臺(tái)的Main方法中初始化StructureMap,其它的IoC工具都是類(lèi)似的【下一個(gè)系列教程就是關(guān)于DI/IoC的】:
#region 2.0 懶加載字段
//[MyLazyLoadingFieldAspect]
[LazyLoadStructureMapAspect]
private static SlowConstructor SlowService;
#endregion
static void Main(string[] args)
{
//ObjectFactory.Initialize告訴StructureMap使用哪個(gè)實(shí)現(xiàn)
ObjectFactory.Initialize(cfg =>
{
cfg.For<IMyService>().Use<MyService>();//當(dāng)調(diào)用IMyService的構(gòu)造函數(shù)時(shí),使用MyService作為實(shí)現(xiàn)
cfg.For<SlowConstructor>().Use<SlowConstructor>();//這行代碼可選,StructureMap會(huì)自動(dòng)綁定
});
SlowService.DoSomething();
SlowService.DoSomething();
Console.Read();
}
現(xiàn)在依賴(lài)配置好了,并且給字段添加了新的特性切面。這里簡(jiǎn)單介紹一下StructureMap的依賴(lài)配置,下一個(gè)系列教程會(huì)詳細(xì)講解哦!首先使用ObjectFactory.Initialize【已經(jīng)過(guò)時(shí)了,在新版本已經(jīng)不建議使用這種方式】指定依賴(lài),如果StructureMap請(qǐng)求IMyService的實(shí)現(xiàn),那么就會(huì)返回MyService對(duì)象,如果請(qǐng)求的是SlowConstructor,那么就會(huì)使用SlowConstructor。更重要的是,當(dāng)創(chuàng)建SlowConstructor的實(shí)例時(shí),它會(huì)識(shí)別出SlowConstructor的構(gòu)造函數(shù)有一個(gè)IMyService類(lèi)型的參數(shù),因此會(huì)自動(dòng)使用配置的依賴(lài)并傳給該構(gòu)造函數(shù)MyService的新實(shí)例。
下面我們需要?jiǎng)?chuàng)建一個(gè)新切面,和之前使用Activator的例子看起來(lái)很像,但是這次使用了ObjectFactory.GetInstance而不是Activator,這樣StructureMap會(huì)自動(dòng)提供需要的對(duì)象:
[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;
}
}
執(zhí)行結(jié)果和之前的一樣,只不過(guò)這次的例子更加具有現(xiàn)實(shí)意義,因?yàn)轭?lèi)有關(guān)于接口的依賴(lài),配置這些依賴(lài)使用了IoC工具。

適當(dāng)?shù)氖褂脩屑虞d可以改善耗時(shí)操作的性能,AOP通過(guò)攔截訪(fǎng)問(wèn)的字段和屬性以及將樣板代碼移到單獨(dú)的切面類(lèi)中使得對(duì)位置進(jìn)行懶加載不再那么痛苦。字段或?qū)傩钥赡軙?huì)遇到樣板代碼問(wèn)題的其它地方在可響應(yīng)的GUI。
真實(shí)案例——INotifyPropertyChanged
在桌面應(yīng)用中使用INotifyPropertyChanged
首先創(chuàng)建一個(gè)WPF應(yīng)用,搭建的界面如下圖所示:

需求是,當(dāng)輸入進(jìn)行輸入時(shí),需要將姓和名兩個(gè)文本框中的內(nèi)容連接起來(lái)填充到姓名那行所在的Label控件上。在WPF中一種普遍的做法是創(chuàng)建一個(gè)封裝數(shù)據(jù)(姓和名)和導(dǎo)出數(shù)據(jù)(姓名)的視圖模型。創(chuàng)建視圖模型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); }
}
}
還需要做以下幾步才能實(shí)現(xiàn)需求:
- 將該視圖模型的一個(gè)實(shí)例綁定到WPF窗體的數(shù)據(jù)上下文
DataContext上,這可以在xaml的代碼后置類(lèi)中對(duì)窗體的DataContext屬性賦值; - 將該視圖模型的每個(gè)屬性分別綁定到各自的文本框和Label控件上,這可以通過(guò)分別將視圖模型的屬性綁定TextBox和Label控件的
Text和Content屬性上完成; - 告訴兩個(gè)文本框,無(wú)論何時(shí)文本框內(nèi)容變化時(shí),都應(yīng)該觸發(fā)一個(gè)更新,因此,修改姓和名所對(duì)應(yīng)的文本框,指定Binding中的
UpdateSourceTriger為PropertyChanged,目的是當(dāng)用戶(hù)輸入時(shí),告訴它們更新源數(shù)據(jù)。 - 讓視圖模型類(lèi)NameViewModel實(shí)現(xiàn)
INotifyPropertyChanged,實(shí)現(xiàn)這個(gè)接口需要做的唯一一件事就是PropertyChangedEventHandler類(lèi)型的事件。由于已經(jīng)將綁定添加到了文本框上,WPF會(huì)自動(dòng)尋找要觸發(fā)的事件,這意味著我們必須在每個(gè)屬性的setter方法中編碼來(lái)觸發(fā)該屬性名所對(duì)應(yīng)的事件。
第一步:
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); }
}
}
如果你對(duì)WPF熟悉的話(huà),那么上面的代碼沒(méi)什么可說(shuō)的:無(wú)論何時(shí)在這些屬性上使用了set,PropertyChanged事件都會(huì)被觸發(fā)。比如,在姓的文本框上輸入了A,那么就會(huì)導(dǎo)致FirstName的屬性值被設(shè)置set。在set期間,觸發(fā)了兩次PropertyChanged:一次是宣布FirstName屬性修改了,然后是宣布FullName屬性修改了。
編譯、運(yùn)行程序,結(jié)果如下:

雖然這個(gè)例子不是很復(fù)雜,但是在真實(shí)的WPF應(yīng)用中,可能會(huì)有更多的字段以及這些字段之間關(guān)系更復(fù)雜,如果熟悉MVVM(Model-View-ViewModel)模式的話(huà),那么這種類(lèi)型的綁定對(duì)于實(shí)現(xiàn)那種模式很重要。此外,雖然這只是一個(gè)簡(jiǎn)單的示例,但是NameViewModel從一個(gè)只有自動(dòng)屬性的小類(lèi)變得越來(lái)越大,代碼越來(lái)越多,有了支持字段,而且每個(gè)set方法中還要邏輯。雖然可以在View和ViewModel之間進(jìn)行干凈的分離,但是使用INotifyPropertyChanged會(huì)面臨很多陷阱和問(wèn)題。
使用INotifyPropertyChanged的問(wèn)題和約束
雖然使用INotifyPropertyChanged有很多好處,但是也有很多弊端,比如,潛在產(chǎn)生了樣板代碼,脆弱的代碼以及可能維護(hù)起來(lái)困難的代碼。
產(chǎn)生的樣板代碼很明顯,因?yàn)閺淖钤嫉闹挥腥齻€(gè)自動(dòng)屬性的NameViewModel類(lèi)現(xiàn)在體積已經(jīng)膨脹了好幾倍,有了顯式的支持字段,setter里面也有了邏輯,因此,這里有很多重復(fù),可以使用AOP減少重復(fù)。
其次,要觸發(fā)PropertyChanged事件,就需要有一個(gè)PropertyChangedEventArgs對(duì)象,它需要一個(gè)字符串來(lái)識(shí)別已經(jīng)改變的屬性。因此,當(dāng)每次調(diào)用OnPropertyChanged時(shí),需要傳一個(gè)和屬性名稱(chēng)對(duì)應(yīng)的字符串,如果不小心手誤輸錯(cuò)了,就會(huì)導(dǎo)致觸發(fā)事件失敗。
最后,使用INotifyPropertyChanged很難維護(hù)。因?yàn)樗褂昧俗址绻牧藢傩悦捅仨氂浀靡惨薷淖址ò惭b了ReSharp等重構(gòu)工具時(shí),如果重命名屬性,ReSharp可以幫我們完成這件事)。還要注意,因?yàn)槲覀冇幸粋€(gè)導(dǎo)出屬性(FullName),所以要記得當(dāng)發(fā)送關(guān)于其它屬性更改的消息時(shí)要包括該屬性。
使用ReSharp重構(gòu)
雖然屬性名FirstName和字符串“FirstName”對(duì)于我們?nèi)祟?lèi)來(lái)說(shuō)看起來(lái)是相同的,但是對(duì)于編譯器它們是不同的符號(hào),如果更改了一個(gè)符號(hào),編譯器不會(huì)聰明到也能意識(shí)到其它相關(guān)的符號(hào),當(dāng)運(yùn)行代碼時(shí)最終會(huì)報(bào)錯(cuò)。
一些重構(gòu)工具比如ReSharp,Telerik JustCode等都會(huì)嘗試使用智能分析和演繹找出相關(guān)的符號(hào)。比如,當(dāng)使用ReSharp重命名FirstName屬性時(shí),它可能會(huì)詢(xún)問(wèn)你是否想要更改“FirstName”字符串的值。
不使用AOP也可以緩解這些問(wèn)題,比如可以寫(xiě)單元測(cè)試或者防御性編程,它們可以驗(yàn)證所有將會(huì)發(fā)送的正確通知。雖然可以使用發(fā)射使得這件事簡(jiǎn)單些,但是會(huì)潛在產(chǎn)生很多代碼。(比如可以使用反射可以循環(huán)遍歷類(lèi)的所有屬性,獲得屬性名稱(chēng),進(jìn)而確保當(dāng)事件觸發(fā)時(shí)屬性名稱(chēng)時(shí)匹配的)。
如果使用了.NET 4.5,那么可以使用一個(gè)叫做CallMemberName的新工具來(lái)處理INotifyPropertyChanged,CallMemberName是一個(gè)特性,可以使用它將一個(gè)參數(shù)設(shè)置成調(diào)用的屬性的名稱(chēng)。這里我們可以使用它來(lái)減少NameViewModel類(lèi)中字符串的依賴(lài),代碼如下:
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不再是問(wèn)題了,因?yàn)?Net Framework會(huì)幫我們填充空白。拼寫(xiě)失誤也不是問(wèn)題了,因?yàn)镃allMemberName只會(huì)使用屬性的名稱(chēng),但是還必須通知FullName更改了,因?yàn)樗且粋€(gè)導(dǎo)出屬性。而且仍然有很多樣板代碼,包括顯式支持字段和許多setter中的代碼。
我們可以使用AOP來(lái)處理這些問(wèn)題,下面我們就使用一個(gè)新的AOP工具來(lái)協(xié)助處理INotifyPropertyChanged。
使用AOP減少樣板代碼
之前已經(jīng)使用PostSharp和Castle Dynamic這兩個(gè)AOP框架演示了很多例子,現(xiàn)在再引入一個(gè)新的框架,這個(gè)框架之前叫INotifyPropertyWeaver,是專(zhuān)為INotifyPropertyChanged而生的,然而,現(xiàn)在這個(gè)框架已經(jīng)棄用了,在網(wǎng)上基本找不到關(guān)于它的消息了,然而,他的作者將它集成到了Fody項(xiàng)目中,而且它現(xiàn)在的名字叫PropertyChanged.Fody,安裝時(shí),直接在Nuget控制臺(tái)輸入Install-Package PropertyChanged.Fody即可。
安裝好之后,我們只需要定義一個(gè)原始的NameViewModel類(lèi):
public class NameViewModel
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string FullName
{
get
{
return String.Format("{0}{1}", FirstName, LastName);
}
}
}
然后,神奇的地方來(lái)了,只需要在這個(gè)類(lèi)的上方添加一個(gè)特性ImplementPropertyChanged即可,當(dāng)然前面的數(shù)據(jù)綁定的步驟還是要做的:
[ImplementPropertyChanged]
public class NameViewModel
{
運(yùn)行程序,如下:

簡(jiǎn)直太方便了,只需要加一個(gè)特性就把之前要寫(xiě)那么多代碼的事情完成了。事實(shí)上,這個(gè)特性做的就是這件事,代碼編譯之后的效果和我們寫(xiě)那么多代碼是一樣的。
當(dāng)然,這個(gè)工具里面還要很多其它特性,感興趣的同學(xué)可以去Github上去學(xué)習(xí),鏈接在此:https://github.com/Fody/PropertyChanged/wiki。
PropertyChanged.Fody幫我們填充了所有的間隙,它很聰明,可以探測(cè)出導(dǎo)出屬性,從而填充通知間隙。PostSharp也以PostSharp Ultimate的形式提供了類(lèi)似的功能。
PostSharp Ultimate
PostSharp Ultimate收集了很多現(xiàn)成的開(kāi)源切面,包括的現(xiàn)成切面如下:
- 多線(xiàn)程
- 診斷(logging)
- 有限的INotifyPropertyChanged切面
這些工具都是免費(fèi)的,但是它們要求PostSharp是全商業(yè)版本。這些工具的優(yōu)勢(shì)是可以獲得AOP的所有優(yōu)勢(shì)來(lái)解決特定的問(wèn)題,不需要從零開(kāi)始編寫(xiě)切面。當(dāng)然,我們也可以使用開(kāi)源免費(fèi)的Fody代替。
接下來(lái)我們使用PostSharp來(lái)實(shí)現(xiàn)和上面使用ProperyChanged.Fody相同的功能。先安裝Postsharp,再創(chuàng)建一個(gè)叫做NotifyPropertyChangedAspect切面類(lèi),它繼承于LocationIntercetionAspect:
[Serializable]
public class NotifyPropertyChangedAspect:LocationInterceptionAspect
{
private string[] _derivedProperties;
public NotifyPropertyChangedAspect(params string[] derived)//構(gòu)造函數(shù)參數(shù)為可變長(zhǎng)參數(shù),用于接收導(dǎo)出屬性
{
_derivedProperties = derived;
}
public override void OnSetValue(LocationInterceptionArgs args)
{
//base.OnSetValue(args);
}
}
回憶一下,之前我們一開(kāi)始實(shí)現(xiàn)INotifyPropertyChanged時(shí),困難的工作都放到setter中了,因此,這里需要重寫(xiě)OnSetValue方法,這個(gè)方法會(huì)在使用屬性的setter時(shí)運(yùn)行,而且它會(huì)代替setter運(yùn)行。
在OnSetValue里面,需要做2件事:
- 比較新值和舊值,如果不等,那么應(yīng)該允許set操作通過(guò)(使用PostSharp的args.ProceedSetValue方法);
- 需要通知已經(jīng)改變的屬性(包括構(gòu)造函數(shù)中指定的任何導(dǎo)出屬性)。創(chuàng)建可以重復(fù)使用的
RaisePropertyChanged方法,對(duì)指定的所有導(dǎo)出屬性進(jìn)行循環(huán)遍歷,并調(diào)用RaisePropertyChanged方法。代碼如下:
[Serializable]
public class NotifyPropertyChangedAspect:LocationInterceptionAspect
{
private string[] _derivedProperties;
public NotifyPropertyChangedAspect(params string[] derived)//構(gòu)造函數(shù)參數(shù)為可變長(zhǎng)參數(shù),用于接收導(dǎo)出屬性
{
_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);//只要屬性執(zhí)行了setter,就觸發(fā)RaisePropertyChanged事件
if (_derivedProperties!=null)
{
//對(duì)每個(gè)導(dǎo)出屬性觸發(fā)事件
foreach (string derivedProperty in _derivedProperties)
{
RaisePropertyChanged(args.Instance,derivedProperty);
}
}
}
}
private void RaisePropertyChanged(object p1, string p2)
{
throw new NotImplementedException();
}
}
RaisePropertyChanged待會(huì)再實(shí)現(xiàn)。先來(lái)學(xué)習(xí)一下之前沒(méi)有碰到過(guò)的PostSharp的API。args.GetCurrentValue獲取當(dāng)前的位置值,但是它還沒(méi)有把值value放到args.Value。因此,這里把它存儲(chǔ)在oldValue變量中再合適不過(guò)了。args.Value返回即將到來(lái)的位置值。args.ProceedSetValue指示PostSharp允許繼續(xù)set操作。
如果屬性值發(fā)生了變化,那么我們就觸發(fā)屬性改變的事件。看一下傳入的實(shí)參,args.Instance返回的是屬性所在的對(duì)象(比如,NameViewModel類(lèi)的實(shí)例),它應(yīng)該是一個(gè)實(shí)現(xiàn)了INotifyPropertyChanged的類(lèi)。args.LocationName返回被攔截的屬性名,比如可能是FirstName或LastName。
當(dāng)屬性更改的通知發(fā)出之后,遍歷所有指定的導(dǎo)出屬性(如FullName),并為這些屬性調(diào)用RaisePropertyChanged方法。下面我們完成最后這個(gè)切面并寫(xiě)完RaisePropertyChanged方法。在該方法中,你期望找到傳入的實(shí)例對(duì)象上的PropertyChanged事件,并使用傳入的位置名觸發(fā)那個(gè)事件。然而,只有純粹的一個(gè)對(duì)象object傳入,所以必須借助反射來(lái)處理:
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));
}
這個(gè)方法中沒(méi)使用任何PostSharp API,只有反射的API。反射會(huì)檢索實(shí)例instance的類(lèi)型,從該類(lèi)型中可以找到PropertyChanged事件字段。使用那個(gè)字段可以調(diào)用事件。
這里使用發(fā)射,是因?yàn)閺念?lèi)外面觸發(fā)事件的唯一方式就是反射了。這樣做并不好,因?yàn)榉瓷涫且粋€(gè)緩慢的過(guò)程,這樣編寫(xiě)切面的話(huà)意味著屬性每次改變時(shí)都會(huì)執(zhí)行反射。此外,如果這個(gè)切面用在一個(gè)沒(méi)有PropertyChanged事件的類(lèi)上,那么就會(huì)報(bào)錯(cuò)。(解決辦法請(qǐng)關(guān)注后面的教程,特別是PostSharp的CompileTimeValidate功能)
小結(jié)
這節(jié)我們覆蓋了一個(gè)新的攔截類(lèi)型:攔截屬性和字段(位置)。和攔截方法一樣,位置攔截切面扮演著getter/setter和處理getting/setting代碼之間的中間人。
C#中的屬性提供了簡(jiǎn)明的方式編寫(xiě)getter/setter方法,可以攔截方法的工具也可以攔截屬性(比如Castle DynamicProxy)。但PostSharp為位置提供了一個(gè)特殊的類(lèi),該API可以同時(shí)為屬性和字段服務(wù)。和方法攔截一樣,我們可以繼續(xù)執(zhí)行g(shù)et/setca操作,也可以獲得關(guān)于位置的信息(比如字段名和屬性名),實(shí)例對(duì)象等等。
這節(jié)也引入了一個(gè)新的AOP工具——PropertyChanged.Fody,這個(gè)工具很專(zhuān)一,只做一件事,不像PostSharp和Castle DynamicProxy是通用框架。
現(xiàn)在,我們已經(jīng)可以編寫(xiě)攔截方法、邊界方法、攔截位置的切面了。但是學(xué)習(xí)AOP不僅僅是數(shù)量(可以少寫(xiě)代碼),而且還有質(zhì)量。下一篇我們看下如何將單元測(cè)試和切面結(jié)合起來(lái)。
已將所有贊助者統(tǒng)一放到單獨(dú)頁(yè)面!簽名處只保留最近10條贊助記錄!查看贊助者列表
| 衷心感謝打賞者的厚愛(ài)與支持!也感謝點(diǎn)贊和評(píng)論的園友的支持! | |||
|---|---|---|---|
| 打賞者 | 打賞金額 | 打賞日期 | |
| 微信:匿名 | 10.00 | 2017-08-03 | |
| 微信:匿名 | 10.00 | 2017-08-04 | |
| 微信:匿名 | 5.00 | 2017-06-15 | |
| 支付寶:一個(gè)名字499***@qq.com | 5.00 | 2017-06-14 | |
| 微信:匿名 | 16.00 | 2017-04-08 | |
| 支付寶:向京劉 | 10.00 | 2017-04-13 | |
| 微信:匿名 | 10.00 | 2017-003-08 | |
| 微信:匿名 | 5.00 | 2017-03-08 | |
| 支付寶:lll20001155 | 5.00 | 2017-03-03 | |
| 支付寶:她是一個(gè)弱女子 | 5.00 | 2017-03-02 | |

浙公網(wǎng)安備 33010602011771號(hào)