轉帖|其它|編輯:郝浩|2010-11-04 15:46:03.000|閱讀 957 次
概述:在上一篇WPF基礎到企業應用系列7——深入剖析依賴屬性中,我們首先從依賴屬性基本介紹講起,然后過渡到依賴屬性的優先級、附加屬性、只讀依賴屬性、依賴屬性元數據、依賴屬性回調、驗證及強制值、依賴屬性監聽、代碼段(自動生成) 等相關知識,最后我們模擬一個WPF依賴屬性的實現,由于上篇是根據微軟WPF的BCL源碼剖析的,所以這篇我們就研究一下.NET的跨平臺版本MONO,看下它是怎么來實現這個依賴屬性機制。
# 界面/圖表報表/文檔/IDE等千款熱門軟控件火熱銷售中 >>
一. 摘要
首先圣殿騎士很高興”WPF 基礎到企業應用系列” 能得到大家的關注、支持和認可。看到很多朋友留言希望加快速度的問題,我會盡力的,對你們的熱情關注也表示由衷的感謝。這段時間更新慢的主要原因是因為忙著用TDD還原MONO的框架,同時也因為一直在研究云計算,所以就拖拖拉拉一直沒有發布后面的文章。由于WPF整個系列是自己的一些粗淺心得和微薄經驗,所以不會像寫書那么面面俱到,如果有不足或者錯誤之處也請大家見諒。在今年之內圣殿騎士會盡量完成”WPF 基礎到企業應用系列”和”云計算之旅系列“,誠然,由于本人才識淺薄,所以熱切希望和大家共勉!
由于依賴屬性是WPF和Silverlight的核心概念,微軟在C\S和B\S平臺上主要精力都放到了WPF和Silverlight技術上,同時Silverlight也是Windows Phone的兩大編程模型之一(另外一種是XNA),所以我們花費了大量的時間和篇幅進行論述。在上一篇WPF基礎到企業應用系列7——深入剖析依賴屬性中,我們首先從依賴屬性基本介紹講起,然后過渡到依賴屬性的優先級、附加屬性、只讀依賴屬性、依賴屬性元數據、依賴屬性回調、驗證及強制值、依賴屬性監聽、代碼段(自動生成) 等相關知識,最后我們模擬一個WPF依賴屬性的實現,由于上篇是根據微軟WPF的BCL源碼剖析的,所以這篇我們就研究一下.NET的跨平臺版本MONO,看下它是怎么來實現這個依賴屬性機制。
二. 本文提綱
· 1.摘要
· 2.本文提綱
· 3.兵馬未動、廢話先行
· 4.依賴屬性續前緣
· 5.引入測試驅動開發
· 6.DependencyProperty測試代碼
· 7.DependencyProperty實現代碼
· 8.DependencyObject測試代碼
· 9.DependencyObject實現代碼
· 10.PropertyMetadata測試代碼
· 11.PropertyMetadata實現代碼
· 12.其他協助類測試代碼
· 13.其他協助類的實現代碼
· 14.回歸并統計覆蓋率
· 15.簡單驗證依賴屬性系統
· 16.本文總結
· 17.相關代碼下載
· 18.系列進度
三. 兵馬未動,廢話先行
在講這篇文章之前,我們先來拉一拉家常,說點題外話,就當進入正餐之前的一些甜點,當然這里主要針對.NET平臺而言:
1,淺談軟件技術的發展趨勢及定位
互聯網的普及應用催生了很多技術的發展與更新,如果仔細深究,你會發現軟件技術的發展趨勢將主要體現在以下四個方面:客戶端軟件開發(其中包括客戶端軟件、游戲、中間件和嵌入式開發等)、Web 開發(包括傳統的Web技術、Web游戲以及一些在線應用)、移動設備軟件開發(主要涉及到手機等移動設備)、云計算開發(公有云、私有云、混合云會逐漸界限清晰,云廠商以及云平臺也會逐漸整合和成熟起來)。就微軟來說,這四個方面主要如下:
◆ 客戶端軟件開發
目前微軟主要有Win32 應用程序、MFC 應用程序、WinForm應用程序和WPF 應用程序作為開發選擇,目前這四種技術還會共存,因為不同的需求以及不同的人群都有不同的需要。當然WPF借助于其強大的功能和迅猛的發展速度很快會成為首選,這個是值得肯定的。
◆ Web 開發
在WEB方面微軟主要有ASP.NET、ASP.NET MVC、Silverlight三種技術,ASP.NET技術已經發展了多年,在未來的很長一段時間內還會是主流,同時結合Silverlight作為局部和整體應用效果都還很不錯,所以這也是很多企業的首選。ASP.NET MVC在目前來說應用還不是特別廣泛,不過用過之后感覺也還不錯,只是還需要一段時間的適應過程而已。Silverlight在構建局部應用和整站應用都發揮了不錯的優勢,在Windows Phone中也表現得不錯,所以這個技術將會一直熱下去。
◆ 移動設備軟件開發
移動設備方面可謂是現在眾廠商競爭最激烈的市場之一,也是傳統技術和新型技術的主要戰場之一。微軟現在主推的Windows Phone開發主要包括Silverlight和XNA兩種技術,Windows Phone開發逐漸變得和ASP.NET開發一樣簡單,這也是微軟的一個目標。
◆ 云計算開發
云計算現在基本上成了互聯網的第一大熱門詞,不管是軟件為主導的企業,還是以硬件為主導的企業,都卷入了這場紛爭與革命。微軟的云平臺——Windows Azure Platform,它是微軟完整的云計算平臺,目前包含了如下三大部分(Windows Azure:運行在云中的操作系統,對于用戶來說是虛擬且透明的,其中提供了Compute(計算),Storage(存儲),以及Manage(管理)這三個主要功能及其底層服務,使用起來相當的便捷。SQL Azure:運行于云中的一個關系數據庫,和SQL Server 2008類似,但是在功能上還沒有那么強大。AppFabric:全名是Windows Azure platform AppFabric,提供了訪問控制、服務總線等服務,主要用于把基礎應用連接到云中)。
其實把這四個方面總結起來就是傳說中的微軟“三屏一云”戰略,從中也可以看出微軟逍遙于天地,縱橫于宇內,嘯傲于世間,雄霸于大地的梟雄戰略!
2,淺談微軟跨平臺與MONO
在談之前我們先看一下什么是MONO?MONO項目是由Ximian發起、Miguel de lcaza領導、Novell公司主持的項目。它是一個致力于開創.NET在Linux,FreeBSD,Unix,Mac OS X和Solaris等其他平臺使用的開源工程。它包含了一個C#語言的編譯器,一個CLR的運行時,和一組類庫,并逐漸實現了 ADO.NET、ASP.NET、WinForm、Silverlight(可惜沒有實現強大的WPF),能夠使得開發人員在其他平臺用C#開發程序。
◆ 值得看好的地方:
1,跨平臺:開創.NET在Linux,FreeBSD,Unix,Mac OS X和Solaris等其他平臺使用,這是微軟沒有實現的,但是MONO進行了補充,所以值得看好。
2,開源:不論使用什么技術,大家似乎都希望能夠用開源的產品,一方面是考慮到技術的可控性和可維護性;另一方面則是考慮到安全性,當然在另一個角度也是可以學習到其中的一些技術和思想,所以大家對開源總是報以歡迎的態度。
3,不同的方式實現.NET框架:由于微軟對技術申請了專利,所以MONO不能盲目的模仿,對很多細節都改用自己的方式進行了實現,所以我們也可以學到很多不一樣的實現方式。
4,持續更新:MONO從一開始到現在始終在更新,其中包括bug修復、版本升級、增加新的功能及應用,所以相信它會在不斷的更新中更加完善。
◆ 不足之處:
1.模仿但要避免專利:由于是模仿微軟.NET平臺,但因為微軟對代碼申請了專利,所以MONO只能采用其它實現方式來實現同樣的功能,這樣一來很多地方就會實現得很累贅,效率也會受損。
2.沒有擺脫實驗產品的頭銜:由于它目前的使用比較低,所以信息反饋和持續改進就做得比較弱,這也是目前功能完善得比較慢的原因之一吧。
3,功能還需要完善:一些主要功能還未實現,如作為Windows平臺最基礎的COM和COM+功能沒有保存,像MSMQ等消息隊列,消息傳送的功能也沒有實現,對ADO.NET、XML等核心功能效率有待提升,對BCL庫代碼也有很多需要優化的地方,強大的WPF也沒有引入。
4.效率和用戶體驗還有待提升。
◆ 與微軟之間的關系
微軟與MONO之間的關系也一直處于不冷不熱的狀態,沒有明確的反對,也沒有明確的支持,究其原因筆者認為主要有以下兩點:
1,微軟帶來最大收益的產品仍舊是Windows操作系統和Office等軟件,微軟在其他領域盈利都沒有這兩大產品來得直接。而.NET作為微軟的強大開發平臺,是不希望落在其他平臺上運行的,這樣就會削弱Windows操作系統和Office等軟件的市場占有率,所以讓.NET跨平臺對微軟來說是一件舍本求末的事情,這也是微軟不主張.NET運行于其他平臺的主要原因,你想微軟是一個以技術為主導的公司,任何IT市場都會有它的身影,如果想讓.NET跨平臺,那豈不是一件很輕而易舉的事情嗎?
2,由于MONO還沒有成熟,在很多方面都表現得像一個實驗室產品,在根本上沒有對微軟構成威脅,況且在外界質疑.NET是否能跨平臺的時候,還有一個現身的說法,所以微軟也不會明確的反對和支持。
◆ 總結
雖然目前來說MONO喜憂參半,但優點始終要大于缺點,畢竟每一個框架或者產品都是慢慢不斷改進而完善的,更何況開源必將是未來的一個趨勢,所以我們有理由也有信心期待它接下來的發展。
3,談談源碼研究與TDD
大家都有一個共識:如果你想研究某個框架或者工具的源碼,那先必須熟練使用它,熟練之后自然就有一種研究它的沖動,但是往往這個框架或工具比較龐大,很不容易下手,一個很不錯的方法就是使用TDD。我們都知道TDD的基本思想就是在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能后,首先思考如何對這個功能進行測試,并完成測試代碼的編寫,然后編寫相關的代碼滿足這些測試用例。然后循環進行添加其他功能,直到完全部功能的開發,在此過程中我們可以借助一些工具來協助。比如我們現在要研究Nhibernate,那么我們首先要熟練它的一些功能,然后從一個點出發慢慢編寫單元測試,然后逐漸完善代碼,最后直至完成框架的搭建,這樣會給我們帶來莫大的驅動力和成就感。除了微軟的BCL(Base Class Library)和企業庫以外,大家還可以用TDD來試試還原以下的任一開源代碼:
Spring.NET()、Castle()、log4net()、
NHibernate()、iBATIS.NET()、Caliburn()、
MVVM Light Toolkit()、Prism()、MONO源碼()
四. 依賴屬性續前緣
大家都知道WPF和Silverlight帶來了很多新的特性,其中一大亮點是引入了一種新的屬性機制——依賴屬性。依賴屬性基本應用在了WPF的所有需要設置屬性的元素。依賴屬性根據多個提供對象來決定它的值(可以是動畫、父類元素、綁定、樣式和模板等),同時這個值也能及時響應變化。所以WPF擁有了依賴屬性后,代碼寫起來就比較得心應手,功能實現上也變得非常容易了。如果沒有依賴屬性,我們將不得不編寫大量的代碼。依賴屬性在WPF中用得非常廣泛,具體在以下幾個方面中表現得尤為突出:
◆ UI的強大屬性體系
◆ Property value inheritance(值繼承)
◆ Metadata(強大的元數據)
◆ 屬性變化通知,限制、驗證
◆ Resources(資源)
◆ Data binding(數據綁定)
◆ Styles、Template(樣式、模板和風格)
◆ 路由事件、附加事件、附加行為乃至命令
◆ Animations、3D(動畫和3D)
◆ WPF Designer Integration(WPF設計、開發集成)
在上一篇WPF基礎到企業應用系列7——深入剖析依賴屬性中,我們對依賴屬性做了較詳細的介紹,那么下面我們就簡單回顧一下,其實依賴屬性的實現很簡單,只要做以下步驟就可以實現:
◆ 第一步: 讓所在類型繼承自 DependencyObject基類,在WPF中,我們仔細觀察框架的類圖結構,你會發現幾乎所有的 WPF 控件都間接繼承自DependencyObject類型。
◆ 第二步:使用 public static 聲明一個 DependencyProperty的變量,該變量才是真正的依賴屬性 ,看源碼就知道這里其實用了簡單的單例模式的原理進行了封裝(構造函數私有),只暴露Register方法給外部調用。
◆ 第三步:在靜態構造函數中完成依賴屬性的元數據注冊,并獲取對象引用,看代碼就知道是把剛才聲明的依賴屬性放入到一個類似于容器的地方,沒有講實現原理之前,請容許我先這么陳述。
◆ 第四步:在前面的三步中,我們完成了一個依賴屬性的注冊,那么我們怎樣才能對這個依賴屬性進行讀寫呢?答案就是提供一個依賴屬性的實例化包裝屬性,通過這個屬性來實現具體的讀寫操作。
根據前面的四步操作,我們就可以寫出下面的代碼:
public class SampleDPClass : DependencyObject
{
//聲明一個靜態只讀的DependencyProperty字段
public static readonly DependencyProperty SampleProperty;
static SampleDPClass()
{
//注冊我們定義的依賴屬性Sample
SampleProperty = DependencyProperty.Register("Sample", typeof(string), typeof(SampleDPClass),
new PropertyMetadata("Knights Warrior!", OnValueChanged));
}
private static void OnValueChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
//當值改變時,我們可以在此做一些邏輯處理
}
//屬性包裝器,通過它來讀取和設置我們剛才注冊的依賴屬性
public string Sample
{
get { return (string)GetValue(SampleProperty); }
set { SetValue(SampleProperty, value); }
}
}
通過上面的例子可以看出,我們一般.NET屬性是直接對類的一個私有屬性進行封裝,所以讀取值的時候,也就是直接讀取這個字段;而依賴屬性則是通過調用繼承自DependencyObject的GetValue()和SetValue來進行操作,它實際存儲在DependencyProperty的一個IDictionary的鍵-值配對字典中,所以一條記錄中的鍵(Key)就是該屬性的HashCode值,而值(Value)則是我們注冊的DependencyProperty。 回顧了一些基礎知識,那我們下面就開始今天的依賴屬性系統TDD之旅。
五. 引入測試驅動開發 1,引入概念
由于本篇的依賴屬性體系是基于測試驅動開發完成的,所以我們就先來看一下什么叫測試驅動開發:測試驅動開發的基本思想就是在開發功能代碼之前,先編寫測試代碼。也就是說在明確要開發某個功能后,首先思考如何對這個功能進行測試,并完成測試代碼的編寫,然后編寫相關的代碼滿足這些測試用例。然后循環進行添加其他功能,直到完全部功能的開發。由于過程很長,在寫的時候也省略了不少步驟,所以有些地方銜接不是那么的流暢,對此表示非常的抱歉!
2,注意事項
根據自身做項目使用TDD的一點微薄經驗,總結了以下幾個注意事項:
◆ 找準切入點:
不論是開發一個新的系統還是復原系統,都必須先找準一個或多個切入點,從切入點經歷”測試代碼-功能代碼-測試-重構“來逐漸完善整個系統,往往這個切入點就是功能點,就是這個系統具備哪些功能,然后根據這些功能寫出測試用例。
◆ 測試列表:
大家都知道一個系統或者一個框架都是很龐大的,如果要引入測試驅動開發,首先我們必須要有一個測試列表,在任何階段想添加功能需求問題時,把相關功能點加到測試列表中,然后繼續開發的工作。然后不斷的完成對應的測試用例、功能代碼、重構。這樣可以避免疏漏的同時也能把控當前的進度。
◆ 測試驅動:
這個比較核心。完成某個功能,某個類,首先編寫測試代碼,考慮其如何使用、如何測試。然后在對其進行設計、編碼。這里也強調先編寫對功能代碼的判斷用的斷言語句,然后編寫相應的輔助語句。
◆ 良好的代碼設計及可測性:
功能代碼設計、開發時應該具有較強的可測試性。應該盡量保持良好的設計原則和代碼規范,如盡量依賴于接口、盡量高內聚、低耦合等等。
◆ 模塊或功能隔離:
不同代碼的測試應該相互隔離。對一塊代碼的測試只考慮此代碼的測試,不要考慮其實現細節,不然就會陷入一團亂麻之中,這個可以通過MOCK來實現,同時在開始的時候也要劃分好邊界。
◆ 適當引入MOCK:
在適當情況下引入MOCK來完成單元測試,這種情況尤其是在邊際交互比較多的案例當中,對于交互比較多且復雜的多個類關系可以用MOCK暫時模擬,這是一個不錯的解決方案。
◆ 由小到大、由偏到全、統籌兼顧:
一個產品或者一個項目是比較大的,所以我們這里就需要遵循由小到大、由偏到全、統籌兼顧的原則,分解功能和代碼。把所有的規模大、復雜性高的工作,分解成小的任務來完成,這樣既方便團隊協作,同時也減輕了復雜度,使整個開發一下子變得簡單了許多。
◆ 保持隨時重構的習慣:
很多開發者在經過測試代碼-功能代碼-測試通過以后就當完成了任務,其實你會發現隨著其他功能的引入或者使用過程中發現了很多重復、冗余的代碼、再或者先前的代碼結構和設計不太合理,這個時候就需要隨時的進行重構和單元測試,在一方面可以避免產生風險,另一方面可以使系統更加完善。
◆ 隨時進行回歸:
在”測試代碼-功能代碼-測試-重構“的循環中一定要記住多回歸,因為這樣可以保證當前的代碼是不是會影響到前面的功能,其實只需要看看紅綠燈就行。
◆ 查看和統計代碼覆蓋率:
通過前面的步驟之后,我們就要看一下實現的功能是否達到我們的預期目標,除了功能完善之外,還要保證代碼的覆蓋率,因為它是一個系統穩定與否、可維護性與否的一個重大標志。
3,工具介入
以后寫關于TDD的文章可能比較多,同時也都會用到這個工具,所以我們今天對它也稍帶介紹一下,正所謂“工欲善其事,必先利其器”。根據官方文檔解釋:TestDriven.NET是Visual Studio的一個TDD插件,最近版本是TestDriven.NET-3.0.2749 RTM版。其中一些新特性有:支持MSTest、.NET Reflector 6 Pro、VS 2010、Silverlight 4、NUnit 2.5.3,使用項目所用的.NET框架等。 下載地址:
這個工具使用起來比VS自帶的單元測試和測試覆蓋功能好用,所以從2008年開始基本就用它作為一個必備的工具使用。關于它具體的功能和怎么使用,我們這里不詳細介紹,網上也有很多文章,大家可以做一下參考和研究。下圖是安裝后以插件的形式出現在VS中的效果:
A,基本介紹
TestDriven.NET原來叫做NUnitAddIn,它是個Visual Studio插件,集成了如下測試框架:NUnit、MbUnit、 ZaneBug、MSTest、NCover、NCoverExplorer、Reflector、TypeMock、dotTrace和MSBee,它主要面向使用TDD的開發者,主要特性列舉如下:
◆ 單鍵運行方法、類、命名空間、項目和解決方案中的單元測試
◆ 能夠快速測試實例方法、靜態方法或屬性
◆ 可以直接跳到.NET Reflector中的任何方法、類型、項目或引用中,這個功能提供了相當大的方便
◆ 在調試過程中可以查看.NET Reflector中的任何模塊或堆棧信息
◆ 支持多種單元測試框架,包括NUnit、MbUnit、xUnit和MSTest
◆ 測試運行在自己的進程中以消除其他問題和邊際效應
◆ 可以輕松對任何目標測試進行調試或執行代碼覆蓋率測試(比微軟自帶的單元測試和代碼覆蓋功能要好用多了)
◆ 支持所有主流的.NET語言:C#、VB、C++和F#
B,TestDriven.NET 3.0中的新特性:
◆ TestDriven.Net是基于.NET框架的。再由于VS 2010支持使用多個.NET版本,所以支持各個VS版本和工具就沒有問題了
◆ 完全支持在VS 2008和VS 2010中使用MSTest
◆ 完全支持.NET Reflector 6 Pro
◆ 支持NUnit 2.5.3
◆ 支持和兼容VS 2005、VS 2008、VS 2010幾個版本
◆ 支持Silverlight 4的測試
C,兼容性
TestDriven.NET兼容于如下VS版本:Windows XP、Vista、Windows 7、Windows 2000、Windows 2003和Windows 2008(32和64位)上的Visual Studio 2005、2008和2010。官方已經不再對VS 2003支持。
D,版本
◆ 企業版:每臺機器一個許可認證
◆ 專業版:一般的許可形式
◆ 個人版:面向學生、開源開發者和試驗用戶的免費許可(大家可以下載這個版本,個人感覺很好用)
4,關于本篇
本篇文章沒有明確的寫作意圖,只是最近在深入研究MONO源碼時有感而發,當然作者本人也只是起到了一個研究者或者剖析者的角色。首先實現最簡單且基本的DependencyProperty.Register功能,然后再實現DependencyObject的GetValue和SetValue,接著實現PropertyMetadata的DefaultValue、PropertyChangedCallback、CoerceValueCallback等功能,然后完善DependencyProperty.Register注冊時添加ValidateValueCallback、RegisterAttached、RegisterAttachedReadOnly、RegisterReadOnly、OverrideMetadata、GetMetadata和AddOwner等相關功能。既然有了這些功能,自然就需要完善PropertyMetadata的IsSealed、Merge和OnApply等相關底層操作。當然在中間還需要DependencyObject的ClearValue、CoerceValue、GetLocalValueEnumerator、ReadLocalValue以及其他的Helper類,這里就不一一進行說明。對于邊際交互比較多且關聯比較大的操作,采用了Mock進行暫時模擬,在開發完了以后再進行了替換。在開發過程中,隨時進行單元測試和覆蓋率的檢查,這樣可以方便查看哪些功能還有問題以及整體的進度和質量的監控。
六. DependencyProperty測試代碼
在寫DependencyProperty測試代碼之前,我們先看一下它到底有哪些成員和方法,如下圖:
了解了上面DependencyProperty的基本功能,我們首先創建一個繼承自DependencyObject的類ObjectPoker,由于DependencyObject還沒有被創建,所以我們這里就先創建它,然后在ObjectPoker類里面實現我們的經典語句DependencyProperty.Register,由于Register有很多重載,為了方便TDD,就從最簡單的開始(三個參數,不牽涉到元數據類),然后再創建一個ObjectPoker的子類,這是方便后面測試DependencyProperty的相關功能。
class ObjectPoker : DependencyObject
{
//注冊依賴屬性property1
public static readonly DependencyProperty TestProp1 = DependencyProperty.Register("property1", typeof(string), typeof(ObjectPoker));
}
class SubclassPoker : ObjectPoker
{
}
經過上面的測試用例通過以后,自然DependencyProperty.Register的基本功能也就完善了,然后我們來測試一下Register兩個相同的依賴屬性有什么反應,由于我們為了實現Register時沒有考慮那么多,所以測試是先會失敗,然后在引入鍵值對的形式來存儲DependencyProperty,然后每個DependencyProperty都用Name.GetHashCode() ^ PropertyType.GetHashCode() ^ OwnerType.GetHashCode()來區別唯一,所以相同下面的測試用例也將完成。
[Test]
[ExpectedException(typeof(ArgumentException))]
public void TestMultipleRegisters()
{
//測試注冊相同名的依賴屬性
DependencyProperty.Register("p1", typeof(string), typeof(ObjectPoker));
DependencyProperty.Register("p1", typeof(string), typeof(ObjectPoker));
}
我們說到依賴屬性系統,其實依賴屬性要依附于DependencyObject才能成為真正的依賴屬性系統。所以我們來測試一下AddOwner,每一個Owner都有自己的元數據,這個時候我們需要完善OverrideMetadata方法,然而OverrideMetadata方法需要用到PropertyMetadata類作為參數,同時需要調用PropertyMetadata類的DoMerge方法,我們可以創建該類,然后結合Mock完成該操作。
[Test]
[ExpectedException(typeof(ArgumentException))]
public void TestMultipleAddOwner()
{
//測試AddOwner,添加相同類型的Owner
ObjectPoker.TestProp1.AddOwner(typeof(SubclassPoker), new PropertyMetadata());
ObjectPoker.TestProp1.AddOwner(typeof(SubclassPoker), new PropertyMetadata());
}
通過上面的測試用例以后,其實PropertyMetadata的原型已經具備了,然后我們要做的就是測試DependencyProperty的默認元數據和默認元數據的默認值。
[Test]
public void TestDefaultMetadata()
{
//測試默認元數據
DependencyProperty p;
p = DependencyProperty.Register("TestDefaultMetadata1", typeof(string), typeof(ObjectPoker));
Assert.IsNotNull(p.DefaultMetadata);
//測試元數據的默認值
p = DependencyProperty.Register("TestDefaultMetadata2", typeof(string), typeof(ObjectPoker), new PropertyMetadata("hi"));
Assert.IsNotNull(p.DefaultMetadata);
Assert.AreEqual("hi", p.DefaultMetadata.DefaultValue);
}
我們都知道一個DependencyProperty可以擁有多個Owner,每個Owner之間的區別就是用PropertyMetadata,那么這里就給該DependencyProperty添加一個Owner,然后通過該Owner來獲取元數據。
[Test]
public void TestAddOwnerNullMetadata()
{
//首先注冊一個依賴屬性,然后再AddOwner,最后根據新的Owner獲取元數據
DependencyProperty p = DependencyProperty.Register("TestAddOwnerNullMetadata", typeof(string), typeof(ObjectPoker));
p.AddOwner(typeof(SubclassPoker), null);
PropertyMetadata pm = p.GetMetadata(typeof(SubclassPoker));
Assert.IsNotNull(pm);
}
通過上面的測試用例,我們牽涉到了OverrideMetadata方法,當然上面沒有進行實現,這個時候我們可以來實現OverrideMetadata這個方法,首先注冊一個ObjectPoker類型的依賴屬性,然后通過SubclassPoker來OverrideMetadata。
//首先注冊一個依賴屬性,然后再OverrideMetadata
[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void TestOverrideMetadataNullMetadata()
{
//有Type但PropertyMetadata為null時,OverrideMetadata操作
DependencyProperty p = DependencyProperty.Register("TestOverrideMetadataNullMetadata", typeof(string), typeof(ObjectPoker));
p.OverrideMetadata(typeof(SubclassPoker), null);
}
上面實現了OverrideMetadata的函數,但是只是簡單實現,這里我們可以傳入一個null類型的Type作為測試,當然測試不會通過,然后就修改代碼直到測試通過吧!
[Test]
[ExpectedException(typeof(ArgumentNullException))]
public void TestOverrideMetadataNullType()
{
//當Type為null,OverrideMetadata操作
DependencyProperty p = DependencyProperty.Register("TestOverrideMetadataNullType", typeof(string), typeof(ObjectPoker));
p.OverrideMetadata(null, new PropertyMetadata());
}
如果仔細分析DependencyProperty的源碼,你會發現有一個DependencyPropertyKey類,這個類到底是干嘛的呢?其實這個類的主要作用就是構造函數傳入該DependencyProperty,然后通過Type來OverrideMetadata,這里只是提供了一個簡單的封裝,如果沒有這個類,其他功能照樣正常。
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void TestReadonlyOverrideMetadata()
{
//通過DependencyPropertyKey的方式OverrideMetadata
DependencyPropertyKey ro_key = DependencyProperty.RegisterReadOnly("readonly-prop1",
typeof(double),
typeof(ObjectPoker),
new PropertyMetadata(double.NaN));
ro_key.DependencyProperty.OverrideMetadata(typeof(SubclassPoker), new PropertyMetadataPoker());
}
最后我們來測試一樣通過DependencyPropertyKey類來注冊一個ReadOnly的依賴屬性,然后進行OverrideMetadata,基本和上一個測試用例類似。
[Test]
public void TestReadonlyOverrideMetadataFromKey()
{
//通過DependencyPropertyKey的方式OverrideMetadata
DependencyPropertyKey ro_key = DependencyProperty.RegisterReadOnly("readonly-prop2",
typeof(double),
typeof(ObjectPoker),
new PropertyMetadata(double.NaN));
ro_key.OverrideMetadata(typeof(SubclassPoker), new PropertyMetadataPoker()); }
通過上面的測試用例,DependencyProperty類已經基本完成,除了該類,其他諸如DependencyObject、PropertyMetadata、DependencyPropertyKey也已經初步完成,所以我們這里先以DependencyProperty作為切入點,那么下面就來看一下剛才創建的DependencyProperty類。
七. DependencyProperty實現代碼
具體代碼如下,我們就不做過多闡述,不過有幾點需要注意:
1,一個依賴屬性可能有多個所有者,所以根據每個所有者都有自己的元數據。
2,依賴屬性私有構造函數,作為初始化操作,每個依賴屬性在注冊的時候都會調用并初始化數據
3,為了區別不同的依賴屬性,Name、PropertyType、OwnerType的哈希值取異。
4,注冊依賴屬性有以下幾個種類:Register、RegisterAttached、RegisterAttachedReadOnly和RegisterReadOnly,所以要區別對待。
5,由于一個依賴屬性可能有多個Owner,根據每個Owner都有自己的元數據,所以要有根據Owner的AddOwner、GetMetadata和OverrideMetadata的操作。
using System.Collections.Generic;
namespace System.Windows
{
public sealed class DependencyProperty
{
//一個依賴屬性可能有多個所有者,所以根據每個所有者都有自己的元數據
private Dictionary<Type,PropertyMetadata> metadataByType = new
Dictionary<Type,PropertyMetadata>();
//聲明一個UnsetValue
public static readonly object UnsetValue = new object ();
//依賴屬性私有構造函數,作為初始化操作,每個依賴屬性在注冊的時候都會調用并初始化數
據
private DependencyProperty (bool isAttached, string name, Type propertyType, Type
ownerType,
PropertyMetadata defaultMetadata,
ValidateValueCallback validateValueCallback)
{
IsAttached = isAttached;
DefaultMetadata = (defaultMetadata == null ? new PropertyMetadata() :
defaultMetadata);
Name = name;
OwnerType = ownerType;
PropertyType = propertyType;
ValidateValueCallback = validateValueCallback;
}
internal bool IsAttached { get; set; }
public bool ReadOnly { get; private set; }
public PropertyMetadata DefaultMetadata { get; private set; }
public string Name { get; private set; }
public Type OwnerType { get; private set; }
public Type PropertyType { get; private set; }
public ValidateValueCallback ValidateValueCallback { get; private set; }
//獲取依賴屬性的編號,暫未實現,在上一篇"WPF基礎到企業應用系列7——深入剖析依賴
屬性"有實現,原理是在初始化的時候++
public int GlobalIndex {
get { throw new NotImplementedException (); }
}
//傳入ownerType增加Owner
public DependencyProperty AddOwner(Type ownerType)
{
return AddOwner (ownerType, null);
}
//增加所有者,根據ownerType和typeMetadata
public DependencyProperty AddOwner(Type ownerType, PropertyMetadata typeMetadata)
{
if (typeMetadata == null) typeMetadata = new PropertyMetadata ();
OverrideMetadata (ownerType, typeMetadata);
// MS seems to always return the same DependencyProperty
return this;
}
//獲取元數據,依據forType
public PropertyMetadata GetMetadata(Type forType)
{
if (metadataByType.ContainsKey (forType))
return metadataByType[forType];
return null;
}
//獲取元數據,依據該依賴屬性
public PropertyMetadata GetMetadata(DependencyObject d)
{
if (metadataByType.ContainsKey (d.GetType()))
return metadataByType[d.GetType()];
return null;
}
//獲取元數據,依據dependencyObjectType
public PropertyMetadata GetMetadata(DependencyObjectType dependencyObjectType)
{
if (metadataByType.ContainsKey (dependencyObjectType.SystemType))
return metadataByType[dependencyObjectType.SystemType];
return null;
}
//驗證類型是否有效
public bool IsValidType(object value)
{
return PropertyType.IsInstanceOfType (value);
}
//驗證值是否有效
public bool IsValidValue(object value)
{
if (!IsValidType (value))
return false;
if (ValidateValueCallback == null)
return true;
return ValidateValueCallback (value);
}
//重寫元數據,使用PropertyMetadata類的DoMerge方法來操作
public void OverrideMetadata(Type forType, PropertyMetadata typeMetadata)
{
if (forType == null)
throw new ArgumentNullException ("forType");
if (typeMetadata == null)
throw new ArgumentNullException ("typeMetadata");
if (ReadOnly)
throw new InvalidOperationException (String.Format ("Cannot override metadata on
readonly property '{0}' without using a DependencyPropertyKey", Name));
typeMetadata.DoMerge (DefaultMetadata, this, forType);
metadataByType.Add (forType, typeMetadata);
}
//重寫元數據,使用PropertyMetadata類的DoMerge方法來操作
public void OverrideMetadata (Type forType, PropertyMetadata typeMetadata,
DependencyPropertyKey key)
{
if (forType == null)
throw new ArgumentNullException ("forType");
if (typeMetadata == null)
throw new ArgumentNullException ("typeMetadata");
typeMetadata.DoMerge (DefaultMetadata, this, forType);
metadataByType.Add (forType, typeMetadata);
}
public override string ToString ()
{
return Name; 125: }
//得到哈希值,區別不同的依賴屬性,Name、PropertyType、OwnerType的哈希值取異
public override int GetHashCode ()
{
return Name.GetHashCode() ^ PropertyType.GetHashCode() ^ OwnerType.GetHashCode
();
}
//注冊依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type)
public static DependencyProperty Register(string name, Type propertyType, Type
ownerType)
{
return Register(name, propertyType, ownerType, null, null); 137: }
//注冊依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type、元數據)
public static DependencyProperty Register(string name, Type propertyType, Type
ownerType,
PropertyMetadata typeMetadata)
{
return Register(name, propertyType, ownerType, typeMetadata, null);
}
//注冊依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type、元數據、驗證
回調委托) 147: public static DependencyProperty Register(string name, Type
propertyType, Type ownerType,
PropertyMetadata typeMetadata,
ValidateValueCallback validateValueCallback)
{
if (typeMetadata == null)
typeMetadata = new PropertyMetadata();
DependencyProperty dp = new DependencyProperty(false, name, propertyType,
ownerType,
typeMetadata, validateValueCallback);
DependencyObject.register(ownerType, dp);
dp.OverrideMetadata (ownerType, typeMetadata);
return dp; 161: }
//注冊附加依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type)
public static Dtached(string name, Type propertyType, Type ownerType)
{
return RegisterAttached(name, propertyType, ownerType, null, null);
}
//注冊附加依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type、元數據)
public static DependencyProperty RegisterAttached(string name, Type
propertyType, Type ownerType,
PropertyMetadata defaultMetadata)
{
return RegisterAttached(name, propertyType, ownerType, defaultMetadata, null);
}
//注冊附加依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type、元數據、驗證回調
委托)
public static DependencyProperty RegisterAttached(string name, Type propertyType,
Type ownerType,
PropertyMetadata defaultMetadata,
ValidateValueCallback validateValueCallback)
{
DependencyProperty dp = new DependencyProperty(true, name, propertyType,
ownerType, defaultMetadata, validateValueCallback);
DependencyObject.register(ownerType, dp);
return dp;
}
//注冊只讀依賴屬性,暫未實現
public static DependencyPropertyKey RegisterAttachedReadOnly(string name,
Type propertyType, Type ownerType,
PropertyMetadata defaultMetadata)
{
throw new NotImplementedException("RegisterAttachedReadOnly(string name, Type
propertyType, Type ownerType, PropertyMetadata defaultMetadata)");
}
//注冊只讀依賴屬性,暫未實現
public static DependencyPropertyKey RegisterAttachedReadOnly(string name,
Type propertyType, Type ownerType,
PropertyMetadata defaultMetadata,
ValidateValueCallback validateValueCallback)
{
throw new NotImplementedException("RegisterAttachedReadOnly(string name, Type
propertyType, Type ownerType, PropertyMetadata defaultMetadata, ValidateValueCallback
validateValueCallback)");
}
//注冊只讀依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type、元數據)
public static DependencyPropertyKey RegisterReadOnly(string name, Type
propertyType, Type ownerType,
PropertyMetadata typeMetadata)
{
return RegisterReadOnly (name, propertyType, ownerType, typeMetadata,
null);
}
//注冊只讀依賴屬性(參數:依賴屬性名、依賴屬性的Type、擁有者的Type、元數據、
驗證回調委托)
public static DependencyPropertyKey RegisterReadOnly(string name, Type
propertyType, Type ownerType,
PropertyMetadata typeMetadata,
ValidateValueCallback validateValueCallback)
{
DependencyProperty prop = Register (name, propertyType, ownerType,
typeMetadata, validateValueCallback);
prop.ReadOnly = true;
return new DependencyPropertyKey (prop);
}
}
}
通過前面的步驟,DependencyProperty已經完成,那么下面我們再來看一下DependencyObject類。
八. DependencyObject測試代碼
在寫DependencyObject測試代碼之前,我們先看一下它到底有哪些成員和方法,如下圖:
通過上面的這幅圖,我們知道它的主要功能包括:各種依賴屬性的GetValue、SetValue操作(核心功能)和ClearValue、CoerceValue、GetLocalValueEnumerator、ReadLocalValue等操作。為了測試這些功能,我們首先創建幾個類,第一個類X,內部首先注冊一個附加依賴屬性,我們都知道,不管是附加依賴屬性還是依賴屬性,都需要使用到GetValue和SetValue操作,只是一個封裝成了屬性,而另一個封裝成了靜態方法而已。第二個類直接繼承自我們前面在實現DependencyProperty時創建的DependencyObject原型類。
class X {
//注冊一個附加依賴屬性A
public static readonly DependencyProperty AProperty = DependencyProperty.RegisterAttached("A", typeof(int), typeof(X));
//獲取附加屬性A的值
public static void SetA(DependencyObject obj, int value)
{
obj.SetValue(AProperty, value);
}
//設置附加屬性A的值
public static int GetA(DependencyObject obj)
{
return (int)obj.GetValue(AProperty);
}
//注冊一個附加依賴屬性B
public static readonly DependencyProperty BProperty = DependencyProperty.RegisterAttached("B", typeof(string), typeof(X));
//設置附加屬性B的值
public static void SetB(DependencyObject obj, string value)
{
obj.SetValue(BProperty, value);
}
//獲取附加屬性B的值
public static string GetB(DependencyObject obj)
{
return (string)obj.GetValue(BProperty);
}
}
class Y : DependencyObject {
}
第三個類則是為了直接測試注冊一個依賴屬性,這個類首先繼承自DependencyObject原型類。
class Z : DependencyObject
{
public static readonly DependencyProperty SimpleDPProperty =
DependencyProperty.Register("SimpleDP", typeof(double), typeof(Z),
new PropertyMetadata((double)0.0,
new PropertyChangedCallback(OnValueChanged),
new CoerceValueCallback(CoerceValue)),
new ValidateValueCallback(IsValidValue));
public double SimpleDP
{
get { return (double)GetValue(SimpleDPProperty); }
set { SetValue(SimpleDPProperty, value); }
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Console.WriteLine("當值改變時,我們可以做的一些操作,具體可以在這里定義: {0}", e.NewValue);
}
private static object CoerceValue(DependencyObject d, object value)
{
Console.WriteLine("對值進行限定,強制值: {0}", value);
return value;
}
private static bool IsValidValue(object value)
{
Console.WriteLine("驗證值是否通過,如果返回True表示驗證通過,否則會以異常的形式暴露: {0}", value);
return true;
}
}
首先我們先寫測試GetValue和SetValue操作的測試代碼,然后不能通過,最后完善DependencyObject類的GetValue和SetValue方法直到測試用例通過。
[Test]
[Category ("NotWorking")]
public void TestAttachedProperty()
{
Y y1 = new Y();
X.SetA(y1, 2);
Assert.AreEqual(2, X.GetA(y1));
}
由于這里是y1和y2兩個對象,所以他們的GetValue和SetValue也是設置和取得各自的值。
[Test]
[Category ("NotWorking")]
public void Test2AttachedProperties()
{
Y y1 = new Y();
Y y2 = new Y();
X.SetA(y1, 2);
X.SetA(y2, 3);
Assert.AreEqual(2, X.GetA(y1));
Assert.AreEqual(3, X.GetA(y2));
}
通過前面的圖,大家可以看到DependencyObject提供了一個取得本地值枚舉器的GetLocalValueEnumerator方法,它實現一個IEnumerator來方便訪問LocalValue,這里我們要實現它,所以先寫測試代碼。
[Test]
[Category ("NotWorking")]
public void TestEnumerationOfAttachedProperties()
{
int count = 0;
Y y = new Y();
X.SetA(y, 2);
X.SetB(y, "Hi");
//根據DependencyObject得到所有本地值
LocalValueEnumerator e = y.GetLocalValueEnumerator();
while (e.MoveNext()) {
count++;
if (e.Current.Property == X.AProperty)
Assert.AreEqual(e.Current.Value, 2);
else if (e.Current.Property == X.BProperty)
Assert.AreEqual(e.Current.Value, "Hi");
else
Assert.Fail("Wrong sort of property" + e.Current.Property);
}
//count為2
Assert.AreEqual(2, count);
}
還有幾個功能,既然Mono也沒做研究,我們也就不費那個力氣了,接下來我們就看看剛才實現的DependencyObject代碼吧!
九. DependencyObject實現代碼
通過前面的測試用例,DependencyObject類的基本功能已經完成,不過我們要注意幾個要點:
1,依賴屬性其實終究要DependencyObject和DependencyProperty成對才能算得上真正的DependencyProperty
2,不管是Register、RegisterAttached、RegisterAttachedReadOnly還是RegisterReadOnly操作,我們都要通過DependencyObject來操作DependencyProperty的值,也就是通過DependencyObject這個外部接口來操作,DependencyProperty只負責注冊和內部處理,不負責外部接口。
3,在DependencyObject中提供了幾個操作LocalValue的接口的接口,其中包括ReadLocalValue、GetLocalValueEnumerator、CoerceValue和ClearValue等。
4,在注冊注冊依賴屬性時,實質是關聯DependencyObject的propertyDeclarations,它是一個Dictionary<Type,Dictionary<string,DependencyProperty>>類型,但是在register代碼中并沒有完全關聯起來,我也比較納悶,所以這點還希望和大家一起探討,微軟的BCL并沒有這么實現。
//using System.Windows.Threading;
namespace System.Windows
{
public class DependencyObject
{
//依賴屬性其實終究要DependencyObject和DependencyProperty成對才能算得上真
正的DependencyProperty
private static Dictionary<Type,Dictionary<string,DependencyProperty>>
propertyDeclarations = new Dictionary<Type,Dictionary<string,DependencyProperty>>();
//該依賴屬性的鍵值對,鍵為DependencyProperty,值為object
private Dictionary<DependencyProperty,object> properties = new
Dictionary<DependencyProperty,object>();
//是否已密封,沒有實現DependencyObject層次的IsSealed判斷
public bool IsSealed {
get { return false; }
}
//獲取該DependencyObject的DependencyObjectType
public DependencyObjectType DependencyObjectType {
get { return DependencyObjectType.FromSystemType (GetType()); }
}
//根據該依賴屬性名,清除它的值
public void ClearValue(DependencyProperty dp)
{
if (IsSealed)
throw new InvalidOperationException ("Cannot manipulate property values
on a sealed DependencyObject");
properties[dp] = null;
}
//根據該依賴屬性DependencyPropertyKey,清除它的值
public void ClearValue(DependencyPropertyKey key)
{
ClearValue (key.DependencyProperty);
}
//根據該依賴屬性名,強制值
public void CoerceValue (DependencyProperty dp)
{
PropertyMetadata pm = dp.GetMetadata (this);
if (pm.CoerceValueCallback != null)
pm.CoerceValueCallback (this, GetValue (dp));
}
public sealed override bool Equals (object obj)
{
throw new NotImplementedException("Equals");
}
public sealed override int GetHashCode ()
{
throw new NotImplementedException("GetHashCode");
}
//得到本地值的枚舉器
public LocalValueEnumerator GetLocalValueEnumerator()
{
return new LocalValueEnumerator(properties);
}
//根據依賴屬性名獲取值
public object GetValue(DependencyProperty dp)
{
object val = properties[dp];
return val == null ? dp.DefaultMetadata.DefaultValue : val;
}
public void InvalidateProperty(DependencyProperty dp)
{
throw new NotImplementedException("InvalidateProperty(DependencyProperty dp)");
}
//當屬性值改變時,觸發回調
protected virtual void OnPropertyChanged(DependencyPropertyChangedEventArgs e)
{
PropertyMetadata pm = e.Property.GetMetadata (this);
if (pm.PropertyChangedCallback != null)
pm.PropertyChangedCallback (this, e);
}
//提供一個外界查看LocalValue的接口
public object ReadLocalValue(DependencyProperty dp)
{
object val = properties[dp];
return val == null ? DependencyProperty.UnsetValue : val;
}
//根據依賴屬性名設置其值
public void SetValue(DependencyProperty dp, object value)
{
if (IsSealed)
throw new InvalidOperationException ("Cannot manipulate property values on a
sealed DependencyObject");
if (!dp.IsValidType (value))
throw new ArgumentException ("value not of the correct type for this
DependencyProperty");
ValidateValueCallback validate = dp.ValidateValueCallback;
if (validate != null && !validate(value))
throw new Exception("Value does not validate");
else
properties[dp] = value;
}
//根據依賴屬性DependencyPropertyKey設置其值
public void SetValue(DependencyPropertyKey key, object value)
{
SetValue (key.DependencyProperty, value);
}
protected virtual bool ShouldSerializeProperty (DependencyProperty dp) {
throw new NotImplementedException ();
}
//這里的注冊實質是關聯propertyDeclarations
internal static void register(Type t, DependencyProperty dp)
{
if (!propertyDeclarations.ContainsKey (t))
propertyDeclarations[t] = new Dictionary<string,DependencyProperty>();
Dictionary<string,DependencyProperty> typeDeclarations =
propertyDeclarations[t];
if (!typeDeclarations.ContainsKey(dp.Name))
{
typeDeclarations[dp.Name] = dp;
//這里仍然有一些問題,期待各位共同探討解決
}
else
throw new ArgumentException("A property named " + dp.Name + " already
exists on " + t.Name);
}
}
}
通過前面對DependencyObject和DependencyProperty的研究之后,我們來看看最重要的一個角色,這也是微軟最喜歡用的概念——元數據,如果大家研究過微軟BCL的源碼,應該都知道,它是貫穿于整個CLR當中的。
十. PropertyMetadata測試代碼
前面我們看到一個依賴屬性的注冊最全的形式是下面這樣子的:
public static DependencyProperty Register(string name,
Type propertyType,
Type ownerType,
PropertyMetadata typeMetadata,
ValidateValueCallback validateValueCallback);
第一個參數是該依賴屬性的名字,第二個參數是依賴屬性的類型,第三個參數是該依賴屬性的所有者的類型,第五個參數就是一個驗證值的回調委托,那么最使我們感興趣的還是這個可愛的 PropertyMetadata ,也就是我們接下來要講的元數據。 提到WPF屬性元數據,大家可能第一想到的是剛才的PropertyMetadata,那么這個類到底是怎樣的呢?我們應該怎樣使用它呢?首先我們看它的構造函數(我們選參數最多的來講):
public PropertyMetadata(object defaultValue,
PropertyChangedCallback propertyChangedCallback,
CoerceValueCallback coerceValueCallback);
其中的第一個參數是默認值,最后兩個分別是PropertyChanged(變化通知)以及Coerce(強制)的兩個委托變量,我們在實例化的時候,只需要把這兩個委托變量關聯到具體的方法上即可。
事實上,除了PropertyMetadata以外,常見的還有FrameworkPropertyMetadata,UIPropertyMetadata。他們的繼承關系是F->U->P。其中以FrameworkPropertyMetadata參數最多,亦最為復雜。
FrameworkPropertyMetadata的構造函數提供了很多重載,我們挑選最為復雜的重載來看它到底有哪些參數以及提供了哪些功能:
public FrameworkPropertyMetadata(object defaultValue,
FrameworkPropertyMetadataOptions flags,
PropertyChangedCallback propertyChangedCallback,
CoerceValueCallback coerceValueCallback,
bool isAnimationProhibited,
UpdateSourceTrigger defaultUpdateSourceTrigger);
其中第一個參數是默認值,最后兩個參數分別是是否允許動畫,以及綁定時更新的策略(在Binding當中相信大家并不陌生),這個不詳細解釋了。重點看一下里第三、四兩個參數,兩個 CallBack的委托。結合前面Register的時候提到的ValidateValueCallback共組成三大”金剛“,這三個Callback分別代表Validate(驗證),PropertyChanged(變化通知)以及Coerce(強制)。當然,作為 Metadata,FrameworkPropertyMetadata只是儲存了該依賴屬性的策略信息,WPF屬性系統會根據這些信息來提供功能并在適當的時機回調傳入的delegate,所以最重要的還是我們定義的這些方法,通過他們傳入委托才能起到真正的作用。
具體PropertyMetadata包含哪些成員呢?我們先看微軟的PropertyMetadata類
在寫其他測試用例之前,我們先來創建兩個類,第一個類TestDepObj,內部注冊了四個依賴屬性,前三個沒有元數據操作,也就是沒有顯示聲明并構造元數據類,第四個添加了一個元數據類,這個元數據類包含了默認值、值改變回調委托、強制值回調委托。第二個類TestSubclass繼承自TestDepObj。
class TestDepObj : DependencyObject
{
public static readonly DependencyProperty TestProp1 = DependencyProperty.Register("property1", typeof(string), typeof(TestDepObj));
public static readonly DependencyProperty TestProp2 = DependencyProperty.Register("property2", typeof(string), typeof(TestDepObj));
public static readonly DependencyProperty TestProp3 = DependencyProperty.Register("property3", typeof(string), typeof(TestDepObj));
public static readonly DependencyProperty TestProp4 = DependencyProperty.Register("property4", typeof(string), typeof(TestDepObj), new PropertyMetadata("default", changed, coerce));
static void changed(DependencyObject d, DependencyPropertyChangedEventArgs e) { }
static object coerce(DependencyObject d, object baseValue) { return baseValue; }
}
class TestSubclass : TestDepObj
{
}
大家看到我們在創建PropertyMetadata的時候對某些功能并沒有實現,這里我們就通過子類來具體實現,MONO的這種做法想沿襲微軟PropertyMetadata、FrameworkPropertyMetadata和UIPropertyMetadata的做法,但是個人覺得它實現得并不是太好,很多地方感覺很別扭。
//首先我們自定義一個元數據類,繼承自我們剛創建的PropertyMetadata類
public class PropertyMetadataPoker : PropertyMetadata
{
public bool BaseIsSealed
{
get { return base.IsSealed; }
}
public void CallApply()
{
OnApply(TestDepObj.TestProp1, typeof(TestDepObj));
}
public void CallMerge(PropertyMetadata baseMetadata, DependencyProperty dp)
{
Merge(baseMetadata, dp);
}
protected override void Merge(PropertyMetadata baseMetadata, DependencyProperty dp)
{
Console.WriteLine(Environment.StackTrace);
base.Merge(baseMetadata, dp);
}
protected override void OnApply(DependencyProperty dp, Type targetType) {
//
base.OnApply(dp, targetType);
Console.WriteLine("IsSealed in OnApply? {0}", IsSealed);
Console.WriteLine(Environment.StackTrace);
}
}
下面的測試代碼主要看一下元數據的默認值,實例化一個元數據類,然后調用它的DefaultValue、PropertyChangedCallback、CoerceValueCallback,測試他們是否為Null。
[Test]
public void DefaultValues()
{
//首先看看元數據的默認值
PropertyMetadataPoker m = new PropertyMetadataPoker();
Assert.AreEqual(null, m.DefaultValue);
Assert.AreEqual(null, m.PropertyChangedCallback);
Assert.AreEqual(null, m.CoerceValueCallback);
}
我們在WPF和Silverlight中都有過這樣的體會:到底什么時候這個依賴屬性不能再修改了,其實這個操作得歸功于OnApply什么時候觸發,我們也可以調用IsSealed來查看,那么這里我們就先寫測試代碼。第一段代碼直接顯示調用CallApply方法進行密封;第二段代碼則是通過OverrideMetadata操作后內部調用的CallApply;第三段代碼是通過AddOwner操作中調用的CallApply;最后一段代碼通過調用DependencyProperty.Register時傳入元數據,在其內部調用CallApply。
[Test]
public void IsSealed()
{
//測試元數據是否密封,這個很重要,因為封閉之后就不能修改了,除非用OverrideMetadata或者AddOwner
PropertyMetadataPoker m;
Console.WriteLine(1);
// 直接調用 OnApply 查看元數據是否密封
m = new PropertyMetadataPoker();
Assert.IsFalse(m.BaseIsSealed);
m.CallApply();
Assert.IsFalse(m.BaseIsSealed);
Console.WriteLine(2);
// 直接 OverrideMetadata
m = new PropertyMetadataPoker();
TestDepObj.TestProp1.OverrideMetadata(typeof(TestSubclass), m);
Assert.IsTrue(m.BaseIsSealed);
Console.WriteLine(3);
// 調用 DependencyProperty.AddOwner, 通過這種方式 OverrideMetadata
m = new PropertyMetadataPoker();
TestDepObj.TestProp2.AddOwner(typeof(TestSubclass), m);
Assert.IsTrue(m.BaseIsSealed);
Console.WriteLine(4);
// 最后, 調用DependencyProperty.Register時傳入元數據
m = new PropertyMetadataPoker();
DependencyProperty.Register("xxx", typeof(string), typeof(TestDepObj), m); Assert.IsTrue(m.BaseIsSealed);
}
下面這段測試代碼是驗證AddOwner后的DependencyProperty是否和原來的DependencyProperty是同一個DependencyProperty。
[Test]
public void TestAddOwnerResult()
{
//測試AddOwner后的DependencyProperty是否和原來的DependencyProperty是同一個DependencyProperty
PropertyMetadataPoker m = new PropertyMetadataPoker();
DependencyProperty p = TestDepObj.TestProp3.AddOwner(typeof(TestSubclass), m);
//結果是同一個DependencyProperty
Assert.AreSame(p, TestDepObj.TestProp3);
}
下面這個測試用例是首先實例化元數據并作為注冊依賴屬性時的參數傳入,大家都知道此時如果想修改元數據,可以通過AddOwner或者OverrideMetadata,如果直接賦值,會拋出錯誤,因為元數據已經密封。
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void ModifyAfterSealed1()
{
//首先實例化元數據并注冊依賴屬性時作為參數傳入
PropertyMetadataPoker m = new PropertyMetadataPoker();
DependencyProperty.Register("p1", typeof(string), typeof(TestDepObj), m);
Assert.IsTrue(m.BaseIsSealed);
//由于元數據已密封,所以拋出如下錯誤信息:Cannot change metadata once it has been applied to a property
m.CoerceValueCallback = null;
}
這個和上面的那個測試用例基本一樣,只不過把CoerceValueCallback換成了PropertyChangedCallback
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void ModifyAfterSealed2()
{
//首先實例化元數據并注冊依賴屬性時作為參數傳入
PropertyMetadataPoker m = new PropertyMetadataPoker();
DependencyProperty.Register("p2", typeof(string), typeof(TestDepObj), m); Assert.IsTrue(m.BaseIsSealed);
//由于元數據已密封,所以拋出如下錯誤信息:Cannot change metadata once it has been applied to a property
m.PropertyChangedCallback = null;
下面這個測試用例也和上面的兩個測試用例類似,它是修改元數據的DefaultValue
[Test]
[ExpectedException(typeof(InvalidOperationException))]
public void ModifyAfterSealed3()
{
//首先實例化元數據并注冊依賴屬性時作為參數傳入
PropertyMetadataPoker m = new PropertyMetadataPoker();
DependencyProperty.Register("p3", typeof(string), typeof(TestDepObj), m);
Assert.IsTrue(m.BaseIsSealed);
//由于元數據已密封,所以拋出如下錯誤信息:Cannot change metadata once it has been applied to a property
m.DefaultValue = "hi"; 12: }
通過前面的測試用例,大家可能都會發現有一個Merge這個方法,它在什么時候調用呢?其實它在OverrideMetadata和AddOwner操作中都會調用,在
DependencyProperty中的Register也會顯示調用一次。我們需要注意的是:在元數據密封了以后就會拋出錯誤。
[Test]
public void TestMerge()
{
//需要注意的是:在元數據密封了以后就會拋出錯誤
PropertyMetadataPoker m = new PropertyMetadataPoker();
m.CallMerge(TestDepObj.TestProp4.GetMetadata(typeof(TestDepObj)), TestDepObj.TestProp4);
Assert.AreEqual("default", m.DefaultValue);
Assert.IsNotNull(m.CoerceValueCallback);
Assert.IsNotNull(m.PropertyChangedCallback);
m = new PropertyMetadataPoker();
m.DefaultValue = "non-default";
m.CallMerge(TestDepObj.TestProp4.GetMetadata(typeof(TestDepObj)), TestDepObj.TestProp4);
Assert.AreEqual("non-default", m.DefaultValue);
Assert.IsNotNull(m.CoerceValueCallback);
Assert.IsNotNull(m.PropertyChangedCallback);
//我們知道元數據包括DefaultValue、 coerce 和 property changed等
//這里我們就不一一測試了,其他測試結果都是一樣的
}
下面的測試用例主要是默認值是不能被設置成Unset的
[Test]
[ExpectedException(typeof(ArgumentException))]
public void TestSetDefaultToUnsetValue()
{
//默認值是不能被設置成Unset的
PropertyMetadata m = new PropertyMetadata();
m.DefaultValue = DependencyProperty.UnsetValue;
}
[Test]
[ExpectedException(typeof(ArgumentException))]
public void TestInitDefaultToUnsetValue()
{
//默認值是不能被設置成Unset的
new PropertyMetadata(DependencyProperty.UnsetValue);
}
通過前面的多個測試用例,其實已經包含了PropertyMetadata的基本功能,那我們接下來就看一下PropertyMetadata的內部設計和實現。
十一. PropertyMetadata實現代碼
MONO的PropertyMetadata類要比微軟的PropertyMetadata類簡單很多,不過我們也需要注意一下幾點:
1,元數據類包含哪些成員以及有幾個構造函數重載?因為這些直接關系到外部的調用。
2,大家要注意ValidateValueCallback不是PropertyMetadata的成員,所以在PropertyMetadata的構造函數中不要把它作為參數傳入。
3,注意OnApply函數,因為調用它之后就不能修改元數據的成員,只有通過OverrideMetadata和AddOwner間接實現,如果大家想知道到底這個元數據有沒有被密封,可以調用IsSealed屬性來查看,這個功能我們也會經常用到。
4,元數據類中提供了Merge的功能,用來方便合并父類和子類的元數據。
namespace System.Windows
{
//依賴屬性三大回調委托:PropertyChangedCallback、CoerceValueCallback和
ValidateValueCallback
public delegate void PropertyChangedCallback(DependencyObject d,
DependencyPropertyChangedEventArgs e);
public delegate object CoerceValueCallback(DependencyObject d, object baseValue);
public delegate bool ValidateValueCallback(object value);
public class PropertyMetadata
{
private object defaultValue;
private bool isSealed;
private PropertyChangedCallback propertyChangedCallback;
private CoerceValueCallback coerceValueCallback;
//返回該元數據是否已密封
protected bool IsSealed
{
get { return isSealed; }
}
//獲取和設置元數據默認值
public object DefaultValue
{
get { return defaultValue; }
set
{
if (IsSealed)
throw new InvalidOperationException("Cannot change metadata once it has been
applied to a property");
if (value == DependencyProperty.UnsetValue)
throw new ArgumentException("Cannot set property metadata's default value to
'Unset'");
defaultValue = value;
}
}
//ChangedCallback委托賦值,注意檢查元數據是否已經密封
public PropertyChangedCallback PropertyChangedCallback
{
get { return propertyChangedCallback; }
set
{
if (IsSealed)
throw new InvalidOperationException("Cannot change metadata once it
has been applied to a property");
propertyChangedCallback = value;
}
}
//CoerceValueCallback委托賦值,注意檢查元數據是否已經密封
public CoerceValueCallback CoerceValueCallback
{
get { return coerceValueCallback; }
set
{
if (IsSealed)
throw new InvalidOperationException("Cannot change metadata once
it has been applied to a property");
coerceValueCallback = value;
}
}
#region PropertyMetadata構造函數,根據不同參數做初始化操作
public PropertyMetadata()
: this(null, null, null)
{
}
public PropertyMetadata(object defaultValue)
: this(defaultValue, null, null)
{
}
public PropertyMetadata(PropertyChangedCallback propertyChangedCallback)
: this(null, propertyChangedCallback, null)
{
}
public PropertyMetadata(object defaultValue, PropertyChangedCallback
propertyChangedCallback)
: this(defaultValue, propertyChangedCallback, null)
{
}
public PropertyMetadata(object defaultValue, PropertyChangedCallback
propertyChangedCallback, CoerceValueCallback coerceValueCallback)
{
if (defaultValue == DependencyProperty.UnsetValue)
throw new ArgumentException("Cannot initialize property metadata's
default value to 'Unset'");
this.defaultValue = defaultValue;
this.propertyChangedCallback = propertyChangedCallback;
this.coerceValueCallback = coerceValueCallback;
}
#endregion
//合并元數據
protected virtual void Merge(PropertyMetadata baseMetadata, DependencyProperty
dp)
{
if (defaultValue == null)
defaultValue = baseMetadata.defaultValue;
if (propertyChangedCallback == null)
propertyChangedCallback = baseMetadata.propertyChangedCallback;
if (coerceValueCallback == null)
coerceValueCallback = baseMetadata.coerceValueCallback;
}
protected virtual void OnApply(DependencyProperty dp, Type targetType)
{
//留給子類來實現吧!
}
//合并元數據并密封
internal void DoMerge(PropertyMetadata baseMetadata, DependencyProperty dp, Type
targetType)
{
Merge(baseMetadata, dp);
OnApply(dp, targetType);
isSealed = true;
}
}
}
在上面幾個類就是依賴屬性系統的核心類,下面將看到幾個Helper類。
十二. 其他協助類測試代碼
這里就簡單寫一下對DependencyObjectTypeTest的測試代碼:
using System;
using System.Windows;
using NUnit.Framework;
namespace TDDDependencyTest.System.Windows
{
[TestFixture]
public class DependencyObjectTypeTest
{
[Test]
public void Accessors()
{
DependencyObjectType t = DependencyObjectType.FromSystemType(typeof(TestDepObj));
Assert.AreEqual("TestDepObj", t.Name);
Assert.AreEqual(typeof(TestDepObj), t.SystemType);
Assert.AreEqual(typeof(DependencyObject), t.BaseType.SystemType); 18: }
[Test]
public void IsInstanceOfType()
{
DependencyObjectType t = DependencyObjectType.FromSystemType(typeof(TestDepObj));
DependencyObjectType t2 = DependencyObjectType.FromSystemType(typeof(TestSubclass));
Assert.IsTrue(t.IsInstanceOfType(new TestSubclass()));
Assert.IsTrue(t2.IsSubclassOf(t));
Assert.IsFalse(t.IsSubclassOf(t2));
}
[Test]
public void TestCache()
{
DependencyObjectType t = DependencyObjectType.FromSystemType(typeof(TestDepObj));
DependencyObjectType t2 = DependencyObjectType.FromSystemType(typeof(TestDepObj));
Assert.AreSame(t, t2);
}
}
}
由于它的功能比較簡單,所以我們就不做過多介紹,大家想了解更多,可以參看代碼。
十三. 其他協助類的實現代碼
LocalValueEnumerator:手動實現一個IEnumerator來方便訪問LocalValue
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections;
namespace System.Windows
{
//手動實現一個IEnumerator來方便訪問LocalValue
public struct LocalValueEnumerator : IEnumerator
{
private IDictionaryEnumerator propertyEnumerator;
private Dictionary<DependencyProperty, object> properties;
private int count;
internal LocalValueEnumerator(Dictionary<DependencyProperty, object> properties)
{
this.count = properties.Count;
this.properties = properties;
this.propertyEnumerator = properties.GetEnumerator();
}
public int Count
{
get { return count; }
}
//獲取當前LocalValue
public LocalValueEntry Current
{
get
{
return new LocalValueEntry((DependencyProperty)propertyEnumerator.Key,
propertyEnumerator.Value);
}
}
object IEnumerator.Current
{
get { return this.Current; }
}
public bool MoveNext()
{
return propertyEnumerator.MoveNext();
}
//重置propertyEnumerator
public void Reset()
{
propertyEnumerator.Reset();
}
public static bool operator !=(LocalValueEnumerator obj1, LocalValueEnumerator obj2)
{
throw new NotImplementedException();
}
public static bool operator ==(LocalValueEnumerator obj1, LocalValueEnumerator obj2)
{
throw new NotImplementedException();
}
public override bool Equals(object obj)
{
throw new NotImplementedException();
}
public override int GetHashCode()
{
throw new NotImplementedException();
}
}
//LocalValue實體類
public struct LocalValueEntry
{
private DependencyProperty property;
private object value;
internal LocalValueEntry(DependencyProperty property, object value)
{
this.property = property;
this.value = value;
}
public DependencyProperty Property
{
get { return property; }
}
public object Value
{
get { return value; }
}
public static bool operator !=(LocalValueEntry obj1, LocalValueEntry obj2)
{
throw new NotImplementedException();
}
public static bool operator ==(LocalValueEntry obj1, LocalValueEntry obj2)
{
throw new NotImplementedException();
}
public override bool Equals(object obj)
{
throw new NotImplementedException();
}
public override int GetHashCode()
{
throw new NotImplementedException();
}
}
}
DependencyPropertyChangedEventArgs:PropertyChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)的參數,它的第一個參數為該DependencyProperty、第二個參數為原來的值、第三個參數為改變了的值。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace System.Windows
{
public class DependencyPropertyChangedEventArgs
{
//第一個參數為該DependencyProperty、第二個參數為原來的值、第三個參數為新
值
public DependencyPropertyChangedEventArgs(DependencyProperty property,
object oldValue, object newValue)
{
this.Property = property;
this.OldValue = oldValue;
this.NewValue = newValue;
}
//注意所有的屬性只對外界開放只讀操作
public object NewValue
{
get;
private set;
}
public object OldValue
{
get;
private set;
}
public DependencyProperty Property
{
get;
private set;
}
public override bool Equals(object obj)
{
if (!(obj is DependencyPropertyChangedEventArgs))
return false;
return Equals((DependencyPropertyChangedEventArgs)obj);
}
public bool Equals(DependencyPropertyChangedEventArgs args)
{
return (Property == args.Property &&
NewValue == args.NewValue &&
OldValue == args.OldValue);
}
public static bool operator !=(DependencyPropertyChangedEventArgs left,
DependencyPropertyChangedEventArgs right)
{
throw new NotImplementedException();
}
public static bool operator ==(DependencyPropertyChangedEventArgs left,
DependencyPropertyChangedEventArgs right)
{
throw new NotImplementedException();
}
public override int GetHashCode()
{
throw new NotImplementedException();
}
}
}
DependencyPropertyKey:構造函數傳入該DependencyProperty,然后通過Type來OverrideMetadata,此類只是起到了封裝作用。
namespace System.Windows
{
//構造函數傳入該DependencyProperty,然后通過Type來OverrideMetadata
public sealed class DependencyPropertyKey
{
internal DependencyPropertyKey (DependencyProperty dependencyProperty) {
this.dependencyProperty = dependencyProperty;
}
private DependencyProperty dependencyProperty;
public DependencyProperty DependencyProperty {
get { return dependencyProperty; }
}
public void OverrideMetadata(Type forType, PropertyMetadata typeMetadata)
{
dependencyProperty.OverrideMetadata (forType, typeMetadata, this); }
}
}
DependencyObjectType:用靜態Dictionary<Type, DependencyObjectType>來存儲DependencyObjectType,主要有FromSystemType、IsInstanceOfType和IsSubclassOf三個功能。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace System.Windows
{
public class DependencyObjectType
{
//鍵為Type(即OwnerType),值為DependencyObjectType(即ID和systemType)的鍵值對
private static Dictionary<Type, DependencyObjectType> typeMap = new Dictionary<Type, DependencyObjectType>();
private static int current_id;
private int id;
private Type systemType;
//構造函數私有,在FromSystemType里進行構造,初始化id和systemType
private DependencyObjectType(int id, Type systemType)
{
this.id = id;
this.systemType = systemType;
}
//基類型的DependencyObjectType
public DependencyObjectType BaseType
{
get { return DependencyObjectType.FromSystemType(systemType.BaseType); }
}
public int Id
{
get { return id; }
}
public string Name
{
get { return systemType.Name; }
}
public Type SystemType
{
get { return systemType; }
}
//用靜態Dictionary<Type, DependencyObjectType>來存儲DependencyObjectType
public static DependencyObjectType FromSystemType(Type systemType)
{
if (typeMap.ContainsKey(systemType))
return typeMap[systemType];
DependencyObjectType dot;
typeMap[systemType] = dot = new DependencyObjectType(current_id++, systemType);
return dot;
}
//是否是該DependencyObject的子類實例
public bool IsInstanceOfType(DependencyObject d)
{
return systemType.IsInstanceOfType(d);
}
//該DependencyObjectType是否是傳入DependencyObjectType的子實例
public bool IsSubclassOf(DependencyObjectType dependencyObjectType)
{
return systemType.IsSubclassOf(dependencyObjectType.SystemType);
}
public override int GetHashCode()
{
throw new NotImplementedException();
}
}
}
76:
十四. 回歸并統計覆蓋率
在上面的開發過程中,我們會不斷的運行和查看代碼通過情況,最后我們也來看一下測試用例的總體通過情況,其實在前面已經運行過很多次了,因為每個功能都要經過”測試代碼-功能代碼-測試-重構“等步驟。
最后也看一下代碼測試覆蓋率,代碼測試覆蓋率對一個系統或者產品來說是一個比較重要的質量指標,可以通過它看出系統的穩定性和可控性。一般在項目的開發中,我們都會以85%~90%的測試代碼覆蓋率作為達標的參考標準。
由于MONO本身對依賴屬性沒有那么健全,我們也沒有寫那么詳細的測試代碼,中間直接就實現了一些功能,嚴格地說,所以本文并沒有完全遵從正規的測試驅動開發流程。
十五. 簡單驗證依賴屬性系統
其實通過上面的測試用例,基本就用不著再單獨測試了,但鑒于覆蓋率比較低的問題,所以最后我們還是來測試一下剛才構建的依賴屬性系統:
class Program
{
static void Main(string[] args)
{
SimpleDPClass sDPClass = new SimpleDPClass();
sDPClass.SimpleDP = 8;
Console.ReadLine();
}
}
public class SimpleDPClass : DependencyObject
{
public static readonly DependencyProperty SimpleDPProperty =
DependencyProperty.Register("SimpleDP", typeof(double), typeof(SimpleDPClass),
new PropertyMetadata((double)0.0,
new PropertyChangedCallback(OnValueChanged),
new CoerceValueCallback(CoerceValue)),
new ValidateValueCallback(IsValidValue));
public double SimpleDP
{
get { return (double)GetValue(SimpleDPProperty); }
set { SetValue(SimpleDPProperty, value); }
}
private static void OnValueChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
Console.WriteLine("當值改變時,我們可以做的一些操作,具體可以在這里定義: {0}", e.NewValue);
}
private static object CoerceValue(DependencyObject d, object value)
{
Console.WriteLine("對值進行限定,強制值: {0}", value);
return value;
}
private static bool IsValidValue(object value)
{
Console.WriteLine("驗證值是否通過,如果返回True表示驗證通過,否則會以異常的形式暴露: {0}", value);
return true;
}
}
測試結果:
到處為止,我們這篇文章也宣告結束。
十六. 本文總結
本篇承接上一篇的寫作風格,對上篇模擬一個WPF依賴屬性的實現重現演繹了一遍,上篇是根據微軟WPF的BCL源碼剖析的,所以這篇我們就詳細的研究一下.NET的跨平臺版本MONO關于依賴屬性系統的實現。在這篇文章中,我只是起到了剖析源碼的作用,就像研究微軟的BCL一樣,不過MONO的代碼遠沒有微軟的BCL那么龐大,所以研究和復原起來不是很吃力。如果大家還想繼續深入,可以去下載相關源碼,也希望大家和我一起交流探討。
十七. 相關代碼下載
在文章的最后,和往常一樣,我們提供代碼的下載,再次溫馨提示:這幾篇文章最重要的就是下載代碼來細細研究,代碼里面也添加了比較詳細的注釋,如果大家有什么問題,也可以直接和我聯系,如果有不正確的地方也希望多多海涵并能給我及時反饋,我將感激不盡!
上圖就是整個代碼包的結構圖
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn
文章轉載自:博客轉載自圣殿騎士