1. 程式人生 > >WPF基礎到企業應用系列7——深入剖析依賴屬性(WPF/Silverlight核心)

WPF基礎到企業應用系列7——深入剖析依賴屬性(WPF/Silverlight核心)

一. 摘要

  首先聖殿騎士很高興這個系列能得到大家的關注和支援,這個系列從七月份開始到現在才第七篇,上一篇釋出是在8月2日,掐指一算有二十多天沒有繼續更新了,最主要原因一來是想把它寫好,二來是因為最近幾個月在籌備“雲端計算之旅”系列,所以一再推遲了釋出進度。之前一直都沒有想過要錄製視訊,主要的原因還是怕自己知識有限,從而誤導他人,所以前幾次浪曦和51CTO邀請錄製視訊,我都以工作忙、公司內部培訓需要時間和自己有待提高等理由委婉的拒絕了,說實在的,自己也知道自己還有很多地方有待提高,還需要向各位學習,所以這幾年都會一直努力,相信總有一天自己頭上也會長出兩隻角的。錄製視訊的事也就這樣不了了之了,直到前一段時間MSDN WebCast的再三邀請,我才決定努力試一試,同時也希望各位能夠支援,現在都把社群當成自己的堅強後盾了,所以我打算先以部落格的形式釋出,這樣就可以先和大家一起討論,糾正自己的某些錯誤認識,這樣在錄製視訊的時候就不會誤導他人了。

  前幾篇我們講了WPF的一些基本知識,但是始終沒有接觸最核心的概念,那麼從這篇文章開始的下面幾篇文章中,我們會分別深入討論一下依賴屬性、路由事件、命令和繫結等相關概念,希望這幾篇文章對大家能有所幫助。由於自己才疏學淺且是對這些技術的使用總結和心得體會,錯誤之處在所難免,懷著技術交流的心態,在這裡發表出來,所以也希望大家能夠多多指點,這樣在使一部分人受益的同時也能糾正我的錯誤觀點,以便和各位共同提高。

  這篇文章比較多,在開篇之前我們會先介紹比本篇更重要的一些東西,然後插播一段“雲端計算之旅”的廣告( 這裡廣告費比較貴喲!),作為最近幾個月執著研究的東西,終於可以和大家見面了,希望自己能從實踐中深入淺出的講明白。在前面的兩個內容之後我們正式進入本篇的主題——依賴屬性。依賴屬性是WPF的核心概念,所以我們花費了大量的時間和篇幅進行論述,首先從依賴屬性基本介紹講起,然後過渡到依賴屬性的優先順序、附加屬性、只讀依賴屬性、依賴屬性元資料、依賴屬性回撥、驗證及強制值、依賴屬性監聽、程式碼段(自動生成) 等相關知識,最後我們會模擬一個WPF依賴屬性的實現,來看看它裡面的內部究竟是怎樣處理的,這樣就可以幫助我們更好的認清它的本質,出現問題的時候我們也可以根據原理快速找到原因了。

二. 本文提綱

· 1.摘要

· 2.本文提綱

· 3.比這篇文章更重要的東西

· 4.雲端計算廣告插播

· 5.依賴屬性基本介紹

· 6.依賴屬性的優先順序

· 7.依賴屬性的繼承

· 8.只讀依賴屬性

· 9.附加屬性

· 10.清除本地值

· 11.依賴屬性元資料

· 12.依賴屬性回撥、驗證及強制值

· 13.依賴屬性監聽

· 14.程式碼段(自動生成)

· 15.模擬依賴屬性實現

· 16.本文總結

· 17.相關程式碼下載

· 18.系列進度

三. 比這篇文章更重要的東西

  在講這篇文章之前,我們先來聊一點更重要的東西,正所謂“授人與魚不如授人以漁”,那麼我們這個“漁”究竟是什麼呢?大家做軟體也有不少年了,對自己擅長的一門或多門技術都有自己的經驗和心得,但總的來說可以分為向內和向外以及擴充套件三個方面(這裡只針對.NET平臺):

(一)向外:

  會使用ASP.NET、WinForm、ASP.NET MVC、WPF、Silverlight、WF、WCF等技術,在用這些技術做專案的同時積累了較豐富的經驗,那麼大家就可以形成自己的一套開發知識庫,知道這些技術怎麼能快速搭建企業所需要的應用、知道這些技術在使用中會出現什麼樣的問題以及如何解決。那麼在這個時候你就可能已經在團隊中起到比較核心的作用,如果專案經理給你一個任務,你也可以很輕鬆且高效的勝任,同時在專案當中由於你也比較清楚業務邏輯,所以當機會來臨的時候,你很快會成為團隊的骨幹,逐漸你就會帶幾個初級一點的工程師一起做專案;如果你不喜歡帶團隊,你就會成為資深的高階開發或者架構師。那麼在向外方面我個人認為最重要的是積累經驗,對常見的應用要比較熟悉且有自己總結的一套開發庫——比如對普通的網站、電子商務系統、ERP、OA、客戶端應用等等有比較豐富的經驗。

(二)向內:

  在前面你使用了這些技術開發專案之後,你會遇到很多問題,為了解決這些問題,你會逐漸研究一些比較底層次的東西。比如對於ASP.NET,你會逐漸的去深入理解ASP.NET的整個處理過程、頁面的生命週期、自定義控制元件的開發等等,由於自己最初是由C、C++、Java這樣過渡到.NET的,所以對一些細節總喜歡鑽牛角尖,這也浪費了不少時間,但同時也得到了很多意外之喜。

對於C#語言上,你也會逐漸去刨根問底,想看看這些語法糖背後到底隱藏著什麼祕密,很多對.NET技術比較痴迷的人都會選擇對C# 1.0 語言通過IL程式碼來深層次認識,然後對C#2.0、C#3.0、C#4.0都編譯為C# 1.0 來學習,這樣他們就能認識到語言的內部到底是怎麼執行的,正所謂知道的同時也知道其所以然。

對於WF,你不僅要知道各 Activity的使用,你也得知道其內部的原理,比如WF 內部就是依靠依賴屬性來在工作流中的各 Activity 間傳遞屬性值的,如果你細心,你還原WF的依賴屬性原始碼,你會發現它和WPF、Silverlight中的依賴屬性大同小異,原理基本一樣、只是針對特定技術進行了適當的調整。

對於資料底層操作也一樣,不管你是用的拼接SQL、儲存過程、IBATIS.NET、Nhibernate,Active Record,Linq to sql、Entity framework還是自己開發的ORM元件,你得明白它內部的原理,你要知道這些開源的程式碼還是很值得研究的,我的經驗是先熟練使用這些功能,然後再剖析它的原始碼,然後自己寫一套自己的框架,現在我也在精簡自己的ORM框架,因為之前把重點放在了實現儘可能多的功能,所以對效能等細節沒有做過多優化,後面也會向大家慢慢學習。

對WPF和Silverlight一樣,你不僅要知道怎麼用這些技術,你要知道它的原理,比如對依賴屬性,你知道它的內部原理,就可以對平時出現的諸如我設定的值怎麼沒有起作用、我Binding的元素怎麼沒有出現等等問題; 對路由事件,你也會經常遇到我的事件怎麼沒有執行、我的自定義控制元件事件怎麼處理不對、路由傳遞怎麼沒有起作用等等,這個時候你如果深入理解了路由事件的內部處理,這些問題就迎刃而解了;對WPF和Silverlight新多出來的命令特性,大家很多時候也是比較疑惑,也會遇到命令失效等等問題,其實如果你深入的瞭解了它的原理,你就會知道,它其實在內部也是事件,只不過微軟在裡面做了很多封裝而已;對Binding就更是如此,我們也不想在這篇文章談開去,畢竟在下面的幾篇文章會詳細的對這些技術進行涉及。

(三)擴充套件:

  通過前面的向內和向外的修煉以後,接下來要做的就是不斷實踐,不斷總結經驗,在這個過程中更重要的是要懂得分享,有分享才會使自己和他人共同提高,有分享才能讓自己擺脫狂妄的井底之蛙思想,還記得自己剛做技術的一兩年裡,天天喜歡提及大型架構、大型資料處理、作業系統底層程式碼如何如何,甚至把AOP、IOC、SSH、OO及設計模式、SOA等詞語時常掛在嘴邊,生怕別人不知道自己不懂。但隨著自己技術實質上的提高以及經驗的積累,自己也就逐漸成熟起來,對這些技術逐漸深入理解且理解了其內部實現原理,這樣反而自己變得謙虛起來了,對之前的那些思想感到無比的羞愧。同時也明白自己在慢慢成長了,現在都習慣戲稱自己為打雜工,其實更多時候用打字員會合理一些,所以希望大家能多多指教,這樣我才能更快地擺脫打字員的生活。我在這裡也對擴充套件做一點小的總結:

記錄學習:這是學習很重要的一步,你不一定要寫技術部落格,你也可以做一些例子來記錄,你也可以在學習之後寫一個總結,畢竟人的精力十分有限,在很多時候,它並不能像硬碟一樣儲存起來就不會丟失,更多的時候它更像一塊記憶體。

同道交流:在這一層裡我覺得最重要的就是和一些技術較好的人成為朋友,和他們經常探討一些技術,這樣可以縮短學習的週期,同時也能快速的解決問題,畢竟人的精力十分有限,你沒有遇到過的問題,說不定其他人遇到過。在這方面自己也體會頗深,也很感謝之前幾個公司及現在公司的同事、社群朋友以及一些志同道合之士,感謝你們的指點,沒有你們的指點,我也不可能從小鳥進化成逐鹿程式界的菜鳥,我也為自己能成為一隻老菜鳥感到自豪!

少考證、多務實:在擴充套件的這一層裡,我們要謹記不要為了考證而去考證,那樣是沒有任何實際作用的。對MVP也一樣,一切順其自然為好,記得大學時候身邊就有人連續四次榮獲MVP稱號,這叫我在當時是相當的佩服,在佩服之餘我們要切記務實,沒有務實的東西都是很虛擬飄渺的。還記得自己當初在大學裡面受到考證風氣的影響,神經兮兮的去考過了什麼國家計算機四級和MCP等一大堆證件,後來到公司面試興高采烈拿著20多張證書,才知道那些東西根本就沒有什麼價值,反而讓自己去學習了最不喜歡的技術,同時也給自己掛上了考證族的名號。所以後來總結就是勞民傷財、徒添傷悲!

技術分享:在自己公司及其他公司進行一些技術培訓或者討論,其實重要的不是什麼榮譽,而是在把這個培訓看成是一些技術交流和分享,因為在這個過程中,你可能會重新認識你所掌握的技術、你可能會遇到一些志同道合的人、你可能會在分享過程中糾正以前的錯誤認識、你可能會在各方面得到提高從而完善自己的知識體系,但是最重要的是你要認真對待每一次培訓,知之為知之不知為不知,不要不能教導他人反而誤導了他人。記得有一次在公司培訓OO與設計模式,我知道這個專題想在一下午的時間把它講清楚是非常困難的,這個不像之後培訓的WPF、WCF和Silverlight那麼單純,並且每個人的基礎都不一樣,當中有還沒有畢業的實習生、剛畢業不久的畢業生、工作了數年的工程師及技術大牛們,所以如何把這些知識很好的插入到每個人的知識樹上面成了我考慮的重點。同時我的心裡也比較矛盾,一方面希望參加培訓的同事多一些,另一方面希望人越少越好。前者則是按照常理來考慮的,畢竟培訓者都希望自己培訓,越受歡迎越好,這樣才能使自己的思想得到更多人的認可,自己也能實現分享知識的目的。後者則是擔心怕講不好,少一點人就少一點罪過。可是恰巧這一次是歷次培訓中最多的一次,來參加培訓的同事有一百多人,不過幸好由於會議室坐不下,才分成了兩批,這樣就可以讓我具備了更充分的時間和更好的心態。總之培訓是向內和向外的提煉與昇華,正所謂“自己理解的知識未必能使人家理解”,這不僅考驗的是技術,還考驗了一個人的綜合能力。

(四)結論:

  前面從向內和向外以及擴充套件三個方面進行了簡單闡述,用一句話概括就是:向內深不可測、向外漫無邊際、擴展才能超越極限。由於這裡只是對本文及下面的三篇文章做一些鋪墊工作,所以我們也不細細分解,那麼我也得稍微推薦一點資料才對得起大家:第一還是研究微軟的類庫,對我們常見的應用進行研究,可以結合Reflector+VS除錯內部程式碼功能一起研究(IL能幫我們看清楚一些內部原理,但是不推薦細究,因為它會浪費我們很多時間,畢竟是微軟搞出來的這麼一套東西,說不定微軟哪天就換了)。其次就是研究MONO原始碼(www.mono-project.com),這個是個非常好的東西,對.NET的功能大部分都進行了實現,我之前研究它不是因為它的跨平臺,是感興趣它的原始碼,大家也可以線上檢視它的原始碼(www.java2s.com),說到java2s這個網站,也是我平時去得比較多的網站,因為它比較全面和方便,同時也會給我們帶來意想不到的收穫。再其次就是研究一些開源的框架和專案,比如pet shop 4.0(http://software.informer.com/getfree-net-pet-shop-4.0-download/)、BlogEngine.NET(http://www.dotnetblogengine.net/)、Spring.NET(http://www.springframework.net/)、Castle(http://www.castleproject.org)、log4net(http://logging.apache.org/log4net/)、NHibernate(http://www.hibernate.org/343.html)、iBATIS.NET(http://ibatis.apache.org)、Caliburn(http://caliburn.codeplex.com/)、MVVM Light Toolkit(http://mvvmlight.codeplex.com/)、Prism(http://compositewpf.codeplex.com/)等等。這裡要注意的是:在研究的過程中一定要先熟悉功能,再研究它內部的原始碼和實現,然後再創造出自己的框架。這樣才能激發我們研究的慾望,才會產生作用和反作用力,從而才會使我們真正受益。

四. 雲端計算廣告插播

  由於這段時間白天要研究雲端計算專題(公司專案原因,最主要還是自己的興趣使然),晚上和閒暇時間又要寫WPF,所以感覺有點心猿意馬。原打算寫完WPF這個系列以後才繼續”雲端計算之旅“這個系列,但是經過慎重的思考,同時也考慮到錄製MSDN WebCast視訊,所以決定兩個系列同時進行,經過幾個月的籌備(期間包括折騰公司的雲端計算專案、研究相關雲端計算的電子書、國外技術視訊和國外各技術社群和部落格等),自己也頗有收穫。期間最重要的還是自己寫例子,寫完了以後再分析它的原理直至最後總結,這樣才能把它變成自己的東西,現在回過頭來感覺雲端計算終於在自己心目中走下了神壇,逐漸揭開了那一層神祕面紗,所以才有下面這個系列的分享,也希望大家能給出建議,從而達到技術交流、共同提高的目的。

雲端計算之旅1—開篇有益

雲端計算之旅2—雲端計算總覽

雲端計算之旅3—雲端計算提供商綜合對比

雲端計算之旅4—Windows Azure總覽

雲端計算之旅5—第一個Windows Azure程式

雲端計算之旅6—剖析Windows Azure程式內部原理

雲端計算之旅7—ASP.NET Web Role

雲端計算之旅8—ASP.NET MVC Web Role

雲端計算之旅9—WCF Service Web Role

雲端計算之旅10—Work Role Castle

雲端計算之旅11—CGI Web Role

雲端計算之旅12—雲端儲存之Blob

雲端計算之旅13—雲端儲存之Table

雲端計算之旅14—雲端儲存之Quee

雲端計算之旅15—雲端儲存之Dive

雲端計算之旅16—SQL Azure(一)

雲端計算之旅17—SQL Azure(二)

雲端計算之旅18—SQL Azure(三)

雲端計算之旅19—AppFabric(一)

雲端計算之旅20—AppFabric(二)

雲端計算之旅21—AppFabric(三)

雲端計算之旅22—雲平臺安全問題

雲端計算之旅23—老技術相容問題

雲端計算之旅24—ASP.NET+SQL專案移植到雲平臺

雲端計算之旅25—WinForm/WPF專案移植到雲平臺(雲/端模式)

雲端計算之旅26—ASP.NET+Silverlight專案移植到雲平臺

雲端計算之旅27—Amazon雲端計算

雲端計算之旅28—Google雲端計算

雲端計算之旅29—SalesForce雲端計算

雲端計算之旅30—雲端計算開發總結

  上面的分類是按照最近學習的總結歸類的,在這幾個月中也先後寫了一些文章和程式碼示例,同時有些知識沒有羅列上去,在後面可能會有一些小的修改。總之,我們堅決抵制”忽悠“,爭取以實際程式碼說話,在此過程中希望大家能夠積極踴躍的加入進來,如果有什麼不對的地方,也希望向大家學習,最重要的是大家有所收穫就好!

五. 依賴屬性基本介紹

  前面廢話了這麼久,到現在才真正進入今天的主題,對此感到非常抱歉,如果各位不喜歡,可以直接跳到這裡閱讀。大家都知道WPF帶來了很多新的特性,它的一大亮點是引入了一種新的屬性機制——依賴屬性。依賴屬性基本應用在了WPF的所有需要設定屬性的元素。依賴屬性根據多個提供物件來決定它的值(可以是動畫、父類元素、繫結、樣式和模板等),同時這個值也能及時響應變化。所以WPF擁有了依賴屬性後,程式碼寫起來就比較得心應手,功能實現上也變得非常容易了。如果沒有依賴屬性,我們將不得不編寫大量的程式碼。關於WPF的依賴屬性,主要有下面三個優點,我們的研究也重點放在這三點上:
1、新功能的引入:加入了屬性變化通知,限制、驗證等等功能,這樣就可以使我們更方便的實現我們的應用,同時也使程式碼量大大減少了,許多之前不可能的功能都可以輕鬆的實現了。
2、節約記憶體:在WinForm等專案開發中,你會發現UI控制元件的屬性通常都是賦予的初始值,為每一個屬性儲存一個欄位將是對記憶體的巨大浪費。WPF依賴屬性解決了這個問題,它內部使用高效的稀疏儲存系統,僅僅儲存改變了的屬性,即預設值在依賴屬性中只儲存一次。
3、支援多個提供物件:我們可以通過多種方式來設定依賴屬性的值。同時其內部可以儲存多個值,配合Expression、Style、Animation等可以給我們帶來很強的開發體驗。
  在.NET當中,屬性是我們很熟悉的,封裝類的欄位,表示類的狀態,編譯後被轉化為對應的get和set方法(在JAVA裡面沒有屬性的概念,通常都是寫相應的方法來對欄位進行封裝)。屬性可以被類或結構等使用。 一個簡單的屬性如下,也是我們常用的寫法:

private string sampleProperty;
public string SampleProperty
{
    get
    {
        return sampleProperty;
    }
    set
    {
        if (value != null)
        {
            sampleProperty = value;
        }
        else
        {
            sampleProperty = "Knights Warrior!";
        }
    }
}

屬性是我們再熟悉不過的了,那麼究竟依賴屬性怎麼寫呢?依賴屬性和屬性到底有什麼區別和聯絡呢?其實依賴屬性的實現很簡單,只要做以下步驟就可以實現:
第一步: 讓所在型別繼承自 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。

六. 依賴屬性的優先順序

  由於WPF 允許我們可以在多個地方設定依賴屬性的值,所以我們就必須要用一個標準來保證值的優先級別。比如下面的例子中,我們在三個地方設定了按鈕的背景顏色,那麼哪一個設定才會是最終的結果呢?是Black、Red還是Azure呢?

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="300" Width="300">
    <Grid>
        <Button x:Name="myButton" Background="Azure">
            <Button.Style>
                <Style TargetType="{x:Type Button}">
                    <Setter Property="Background" Value="Black"/>
                    <Style.Triggers>
                        <Trigger Property="IsMouseOver" Value="True">
                            <Setter Property="Background" Value="Red" />
                        </Trigger>
                    </Style.Triggers>
                </Style>
            </Button.Style>
            Click
        </Button>
    </Grid>
</Window>

通過前面的簡單介紹,我們瞭解了簡單的依賴屬性,每次訪問一個依賴屬性,它內部會按照下面的順序由高到底處理該值。詳細見下圖

first  

  由於這個流程圖偏理想化,很多時候我們會遇到各種各樣的問題,這裡也不可能一句話、兩句話就能夠把它徹底說清楚,所以我們就不過多糾纏。等遇到問題之後要仔細分析,在找到原因之後也要不斷總結、舉一反三,只有這樣才能逐漸提高。

七. 依賴屬性的繼承

  依賴屬性繼承的最初意願是父元素的相關設定會自動傳遞給所有層次的子元素 ,即元素可以從其在樹中的父級繼承依賴項屬性的值。這個我們在程式設計當中接觸得比較多,如當我們修改窗體父容器控制元件的字型設定時,所有級別的子控制元件都將自動使用該字型設定 (前提是該子控制元件未做自定義設定),如下面的程式碼:

<Window x:Class="Using_Inherited_Dps.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    WindowStartupLocation="CenterScreen" 
    FontSize="20"
    Title="依賴屬性的繼承" Height="400" Width="578">
    <StackPanel >
        <Label Content="繼承自Window的FontSize" />
        <Label Content="重寫了繼承" 
               TextElement.FontSize="36"/>
        <StatusBar>沒有繼承自Window的FontSize,Statusbar</StatusBar>
    </StackPanel>
</Window> 

  Window.FontSize 設定會影響所有的內部元素字型大小,這就是所謂的屬性值繼承,如上面程式碼中的第一個Label沒有定義FontSize ,所以它繼承了Window.FontSize的值。但一旦子元素提供了顯式設定,這種繼承就會被打斷,如第二個Label定義了自己的FontSize,所以這個時候繼承的值就不會再起作用了。

  這個時候你會發現一個很奇怪的問題:雖然StatusBar沒有重寫FontSize,同時它也是Window的子元素,但是它的字型大小卻沒有變化,保持了系統預設值。那這是什麼原因呢?作為初學者可能都很納悶,官方不是說了原則是這樣的,為什麼會出現表裡不一的情況呢?其實仔細研究才發現並不是所有的元素都支援屬性值繼承。還會存在一些意外的情況,那麼總的來說是由於以下兩個方面:

1、有些Dependency屬性在用註冊的時候時指定Inherits為不可繼承,這樣繼承就會失效了。

2、有其他更優先順序的設定設定了該值,在前面講的的“依賴屬性的優先順序”你可以看到具體的優先級別。

  這裡的原因是部分控制元件如StatusBar、Tooptip和Menu等內部設定它們的字型屬性值以匹配當前系統。這樣使用者通過作業系統的控制面板來修改它們的外觀。這種方法存在一個問題:StatusBar等截獲了從父元素繼承來的屬性,並且不影響其子元素。比如,如果我們在StatusBar中添加了一個Button。那麼這個Button的字型屬性會因為StatusBar的截斷而沒有任何改變,將保留其預設值。所以大家在使用的時候要特別注意這些問題。

proinher

前面我們看了依賴屬性的繼承,當我們自定義的依賴屬性,應該如何處理繼承的關係呢? 請看下面的程式碼(註釋很詳細,我就不再費口水了):

public class MyCustomButton : Button
 {
     static MyCustomButton()
     {
         //通過MyStackPanel依賴屬性MinDateProperty的AddOwner方式實現繼承,注意FrameworkPropertyMetadataOptions的值為Inherits
         MinDateProperty = MyStackPanel.MinDateProperty.AddOwner(typeof(MyCustomButton),
         new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));
     }

     public static readonly DependencyProperty MinDateProperty;

     public DateTime MinDate
     {
         get { return (DateTime)GetValue(MinDateProperty); }
         set { SetValue(MinDateProperty, value); }
     }
 }


 public class MyStackPanel : StackPanel
 {
     static MyStackPanel()
     {
         //我們在MyStackPanel裡面註冊了MinDate,注意FrameworkPropertyMetadataOptions的值為Inherits
         MinDateProperty = DependencyProperty.Register("MinDate",
         typeof(DateTime),
         typeof(MyStackPanel),
         new FrameworkPropertyMetadata(DateTime.MinValue, FrameworkPropertyMetadataOptions.Inherits));
     }

     public static readonly DependencyProperty MinDateProperty;

     public DateTime MinDate
     {
         get { return (DateTime)GetValue(MinDateProperty); }
         set { SetValue(MinDateProperty, value); }
     }
 }

那麼就可以在XAML中進行使用了

<Window x:Class="Custom_Inherited_DPs.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:Custom_Inherited_DPs" 
    xmlns:sys="clr-namespace:System;assembly=mscorlib"   
    WindowStartupLocation="CenterScreen" 
    Title="使用自動以依賴屬性繼承" Height="300" Width="300">
    <Grid>
        <local:MyStackPanel x:Name="myStackPanel" MinDate="{x:Static sys:DateTime.Now}">
            <!-- myStackPanel的依賴屬性 -->
            <ContentPresenter Content="{Binding Path=MinDate, ElementName=myStackPanel}"/>
            <!-- 繼承自myStackPanel的依賴屬性 -->
            <local:MyCustomButton 
                    Content="{Binding RelativeSource={x:Static RelativeSource.Self},
                    Path=MinDate}" 
                Height="20"/>
        </local:MyStackPanel>
   </Grid>
</Window>    

最後的效果如下:

2010-8-26 1-14-30

八. 只讀依賴屬性

  我們以前在對簡單屬性的封裝中,經常會對那些希望暴露給外界只讀操作的欄位封裝成只讀屬性,同樣在WPF中也提供了只讀屬性的概念,如一些WPF控制元件的依賴屬性是隻讀的,它們經常用於報告控制元件的狀態和資訊,像IsMouseOver等屬性, 那麼在這個時候對它賦值就沒有意義了。 或許你也會有這樣的疑問:為什麼不使用一般的.Net屬性提供出來呢?一般的屬性也可以繫結到元素上呀?這個是由於有些地方必須要用到只讀依賴屬性,比如Trigger等,同時也因為內部可能有多個提供者修改其值,所以用.Net屬性就不能完成天之大任了。
  那麼一個只讀依賴屬性怎麼建立呢?其實建立一個只讀的依賴屬性和建立一個一般的依賴屬性大同小異(研究原始碼你會發現,其內部都是呼叫的同一個Register方法)。僅僅是用DependencyProperty.RegisterReadonly替換了DependencyProperty.DependencyProperty而已。和前面的普通依賴屬性一樣,它將返回一個DependencyPropertyKey。該鍵值在類的內部暴露一個賦值的入口,同時只提供一個GetValue給外部,這樣便可以像一般屬性一樣使用了,只是不能在外部設定它的值罷了。

下面我們就用一個簡單的例子來概括一下:

public partial class Window1 : Window
 {
     public Window1()
     {
         InitializeComponent();

         //內部用SetValue的方式來設定值
         DispatcherTimer timer =
             new DispatcherTimer(TimeSpan.FromSeconds(1),
                                 DispatcherPriority.Normal,
                                 (object sender, EventArgs e)=>
                                 {
                                     int newValue = Counter == int.MaxValue ? 0 : Counter + 1;
                                     SetValue(counterKey, newValue);
                                 },
                                 Dispatcher);
         
     }

     //屬性包裝器,只提供GetValue,這裡你也可以設定一個private的SetValue進行限制
     public int Counter
     {
         get { return (int)GetValue(counterKey.DependencyProperty); }
     }

     //用RegisterReadOnly來代替Register來註冊一個只讀的依賴屬性
     private static readonly DependencyPropertyKey counterKey =
         DependencyProperty.RegisterReadOnly("Counter",
                                             typeof(int),
                                             typeof(Window1),
                                             new PropertyMetadata(0));
 }


XAML中程式碼:

<Window x:Name="winThis" x:Class="WpfApplication1.Window1"
  xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
  Title="Read-Only Dependency Property" Height="300" Width="300">
    <Grid>
        <Viewbox>
            <TextBlock Text="{Binding ElementName=winThis, Path=Counter}" />
        </Viewbox>
    </Grid>
</Window>

效果如下圖所示: 

ReadOnly_DPs

九. 附加屬性

  前面我們講了依賴屬性。現在我們再繼續探討另外一種特殊的Dependency屬性——附加屬性。附加屬性是一種特殊的依賴屬性。他允許給一個物件新增一個值,而該物件可能對此值一無所知。

  最好的例子就是佈局面板。每一個佈局面板都需要自己特有的方式來組織它的子元素。如Canvas需要Top和left來佈局,DockPanel需要Dock來佈局。當然你也可以寫自己的佈局面板(在上一篇文章中我們對佈局進行了比較細緻的探討,如果有不清楚的朋友也可以再回顧一下)。

下面程式碼中的Button 就是用了CanvasCanvas.TopCanvas.Left="20" 來進行佈局定位,那麼這兩個就是傳說中的附加屬性。

<Canvas>
    <Button Canvas.Top="20" Canvas.Left="20" Content="Knights Warrior!"/>
</Canvas>

  在最前面的小節中,我們是使用DependencyProperty.Register來註冊一個依賴屬性,同時依賴屬性本身也對外提供了 DependencyProperty.RegisterAttached方法來註冊附加屬性。這個RegisterAttached的引數和 Register是完全一致的,那麼Attached(附加)這個概念又從何而來呢?

  其實我們使用依賴屬性,一直在Attached(附加)。我們註冊(構造)一個依賴屬性,然後在DependencyObject中通過 GetValue和SetValue來操作這個依賴屬性,也就是把這個依賴屬性通過這樣的方法關聯到了這個DependencyObject上,只不過是通過封裝CLR屬性來達到的。那麼RegisterAttached又是怎樣的呢?

下面我們來看一個最簡單的應用:首先我們註冊(構造)一個附加屬性

public class AttachedPropertyChildAdder
{
    //通過使用RegisterAttached來註冊一個附加屬性
    public static readonly DependencyProperty IsAttachedProperty =
        DependencyProperty.RegisterAttached("IsAttached", typeof(bool), typeof(AttachedPropertyChildAdder),
            new FrameworkPropertyMetadata((bool)false));

    //通過靜態方法的形式暴露讀的操作
    public static bool GetIsAttached(DependencyObject dpo)
    {
        return (bool)dpo.GetValue(IsAttachedProperty);
    }

    //通過靜態方法的形式暴露寫的操作
    public static void SetIsAttached(DependencyObject dpo, bool value)
    {
        dpo.SetValue(IsAttachedProperty, value);
    }
}

在XAML中就可以使用剛才註冊(構造)的附加屬性了:

  在上面的例子中,AttachedPropertyChildAdder 中並沒有對IsAttached採用CLR屬性形式進行封裝,而是使用了靜態SetIsAttached方法和GetIsAttached方法來存取IsAttached值,當然如果你瞭解它內部原理,你就會看到實際上還是呼叫的SetValue與GetValue來進行操作(只不過擁有者不同而已)。這裡我們不繼續深入下去,詳細在後面的內容會揭開謎底。

十. 清除本地值

  在很多時候,由於我們的業務邏輯和UI操作比較複雜,所以一個龐大的頁面會進行很多諸如動畫、3D、多模板及樣式的操作,這個時候頁面的值已經都被改變了,如果我們想讓它返回預設值,可以用ClearValue 來清除本地值,但是遺憾的是,很多時候由於WPF依賴屬性本身的設計,它往往會不盡如人意(詳細就是依賴屬性的優先順序以及依賴屬性EffectiveValueEntry 的影響)。ClearValue 方法為在元素上設定的依賴項屬性中清除任何本地應用的值提供了一個介面。但是,呼叫 ClearValue 並不能保證註冊屬性時在元資料中指定的預設值就是新的有效值。值優先順序中的所有其他參與者仍然有效。只有在本地設定的值才會從優先順序序列中移除。例如,如果您對同時也由主題樣式設定的屬性呼叫 ClearValue,主題值將作為新值而不是基於元資料的預設值進行應用。如果您希望取消過程中的所有屬性值,而將值設定為註冊的元資料預設值,則可以通過查詢依賴項屬性的元資料來最終獲得預設值,然後使用該預設值在本地設定屬性並呼叫 SetValue來實現,這裡我們得感得PropertyMetadata類為我們提供了諸如DefaultValue這樣的外部可訪問的屬性。

上面講了這麼多,現在我們就簡單用一個例子來說明上面的原理(例子很直觀,相信大家能很容易看懂)

XAML中程式碼如下:

<Window  x:Class="WpfApplication1.DPClearValue"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="Window1" Height="400" Width="400">
    <StackPanel Name="root">
        <StackPanel.Resources>
            <Style TargetType="Button">
                <Setter Property="Height" Value="20"/>
                <Setter Property="Width" Value="250"/>
                <Setter Property="HorizontalAlignment" Value="Left"/>
            </Style>
            <Style TargetType="Ellipse">
                <Setter Property="Height" Value="50"/>
                <Setter Property="Width" Value="50"/>
                <Setter Property="Fill" Value="LightBlue"/>
            </Style>
            <Style TargetType="Rectangle">
                <Setter Property="Height" Value="50"/>
                <Setter Property="Width" Value="50"/>
                <Setter Property="Fill" Value="MediumBlue"/>
            </Style>
            <Style x:Key="ShapeStyle" TargetType="Shape">
                <Setter Property="Fill" Value="Azure"/>
            </Style>
        </StackPanel.Resources>
        <DockPanel Name="myDockPanel">
            <Ellipse Height="100"   Width="100" Style="{StaticResource ShapeStyle}"/>
            <Rectangle Height="100" Width="100"  Style="{StaticResource ShapeStyle}" />
        </DockPanel>
        <Button Name="RedButton" Click="MakeEverythingAzure" Height="39" Width="193">改變所有的值</Button>
        <Button Name="ClearButton" Click="RestoreDefaultProperties" Height="34" Width="192"> 清除本地值</Button>
    </StackPanel>
</Window>

後臺程式碼:

public partial class DPClearValue
 {
     //清除本地值,還原到預設值
     void RestoreDefaultProperties(object sender, RoutedEventArgs e)
     {
         UIElementCollection uic = myDockPanel.Children;
         foreach (Shape uie in uic)
         {
             LocalValueEnumerator locallySetProperties = uie.GetLocalValueEnumerator();
             while (locallySetProperties.MoveNext())
             {
                 DependencyProperty propertyToClear = (DependencyProperty)locallySetProperties.Current.Property;
                 if (!propertyToClear.ReadOnly) { uie.ClearValue(propertyToClear); }
             }
         }
     }

     //修改本地值
     void MakeEverythingAzure(object sender, RoutedEventArgs e)
     {
         UIElementCollection uic = myDockPanel.Children;
         foreach (Shape uie in uic) { uie.Fill = new SolidColorBrush(Colors.Azure); }
     }
 }

當按下”改變所有的值“按鈕的時候,就會把之前的值都進行修改了,這個時候按下”清除本地值“就會使原來的所有預設值生效。

ClearLocallySetValues

十一. 依賴屬性元資料

前面我們看到一個依賴屬性的註冊最全的形式是下面這樣子的:

public static DependencyProperty Register(string name, 
                                          Type propertyType,
                                          Type ownerType, 
                                          PropertyMetadata typeMetadata,
                                          ValidateValueCallback validateValueC