程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> .NET網頁編程 >> 關於.NET >> 基礎內容 - 依賴關系屬性和通知

基礎內容 - 依賴關系屬性和通知

編輯:關於.NET

目錄

依賴關系屬性基礎知識

綁定源與目標

使用通知自定義集合

依賴關 系屬性和事件

動態綁定技術

Freezable 差異

使用 DependencyPropertyDescriptor

DependencyObjects 集合

這些天來,對象似乎已經忙得暈 頭轉向了。每個人都希望它們做這做那。Windows® Presentation Foundation (WPF) 應用程序中的 典型對象會接到各種各樣不同的請求:有要求綁定到數據的、有要求更改樣式的、有要求從可見父項繼承 的,甚至還有要求來點動畫讓大家高興一下的。

對象怎麼才能建立起邊界和優先級呢?WPF 的回 答是一種稱為依賴關系屬性的功能。通過為 WPF 類提供結構化方法來響應由數據綁定、樣式、繼承和其 他來源更改帶來的變化,依賴關系屬性已變得十分重要,其程度不亞於事件和事件處理對早期 .NET 程序 員的重要性。

當然,依賴關系屬性不是萬能的,它可能無法為某些傳統任務提供您所需要的所有 功能。假設您可以訪問具有依賴關系屬性的對象,並且希望在其中某個屬性發生變化時得到通知。這對於 事件處理程序來說好像並不是什麼難事——但您要知道實際上根本不存在這樣的事件!

當您使用對象集合時,這個問題更加突出。在某些情況下,您可能希望當集合中對象的某些特定 依賴關系屬性發生更改時得到通知。現有唯一的解決方案是 FreezableCollection<T>,它能夠告 訴您何時發生變化——但不能告訴您是什麼發生了變化。

換句話說,依賴關系屬性並 不總能與其他各方良好協作。本專欄主要講述的就是如何彌補它們在通知方面的不足。

依賴關系 屬性基礎知識

假設您正在設計名為 PopArt 的 WPF 類,並且希望定義類型為 Brush 的屬性 SwirlyBrush。如果 PopArt 繼承自 DependencyObject,您就可以將 SwirlyBrush 定義為 DependencyProperty。第一步是公共靜態只讀字段:

public static readonly 

DependencyProperty SwirlyBrushProperty;

依賴關系屬性與該屬性具有相同的名稱,但附加了 "Property" 字樣。它是字段聲明或靜態構造函數的一部分,您可以隨後注冊依賴關系屬性:

SwirlyBrushProperty = DependencyProperty.Register("SwirlyBrush",
 typeof(Brush), typeof(PopArt),
 new PropertyMetadata(OnSwirlyBrushChanged));

您還需要能夠提供對該屬性進行正常訪問 的傳統屬性定義(有時稱為 CLR 屬性):

public Brush SwirlyBrush {
 set { SetValue(SwirlyBrushProperty, value); }
 get { return (Brush) GetValue(SwirlyBrushProperty); }
}

SetValue 和 GetValue 方法由 DependencyObject 定義,這就是所有定義依賴關系屬性的類 必需從該類派生的原因。除調用這兩種方法外,CLR 屬性不應當包含任何其他代碼。CLR 屬性通常被認為 是由依賴關系屬性支持的。

依賴關系屬性注冊中引用的 OnSwirlyBrushChanged 方法是一種回調 方法,任何時候只要 SwirlyBrush 屬性發生變化便會調用該方法。因為該方法與靜態字段關聯,所以它 自身也必須是靜態方法:

static void OnSwirlyBrushChanged(DependencyObject obj,
 DependencyPropertyChangedEventArgs args) {
 ...
}

第一個參數是屬性發生改變的類的特定實例。如果已經在名為 PopArt 的類中定義過此依賴關 系屬性,則第一個參數始終是類型為 PopArt 的對象。我喜歡使用與靜態方法相同的名稱定義實例方法。 靜態方法隨後按照如下方式調用實例方法:

static void OnSwirlyBrushChanged

(DependencyObject obj,
 DependencyPropertyChangedEventArgs args) {
 (obj as PopArt).OnSwirlyBrushChanged(args);
}
void OnSwirlyBrushChanged(DependencyPropertyChangedEventArgs args) {
 ...
}

該實例方法中包含用於應對 SwirlyBrush 值更改所需的所有項目。

在代碼中,您可以 采用正常方式在 PopArt 的實例中設置 SwirlyBrush 屬性的值:

popart.SwirlyBrush = new 

SolidColorBrush(Colors.AliceBlue);

但是,還需要注意 PopArt 類中存在名為 PopArt.SwirlyBrushProperty 的公共靜態字段。該字段是類型為 DependencyProperty 的有效對象,獨 立於所有類型為 PopArt 的對象存在。這使您能夠在創建具有該屬性的對象之前引用類的特定屬性。

由 DependencyObject 定義的 SetValue 方法也是公共的,因此您可以采用如下方式設置 SwirlyBrush 屬性:

popart.SetValue(PopArt.SwirlyBrushProperty,
 new SolidColorBrush(Colors.AliceBlue));

此外,如果具有如圖 1 所示的三個對象,您可以使用以下這段完全通用的代碼設置屬性:

target.SetValue(property, value);

圖 1 對象

對象 說明 目標 DependencyObject 派生類的實例。 屬性 DependencyProperty 類型的對象。 值 包含正確類型的依賴關系屬性的對象。

如果值類型與 DependencyProperty 關聯的類型(本例中為 Brush)不一致,則會拋出異常。但 DependencyProperty 定義的 IsValidType 方法可以幫助避免此類問題。

使用 DependencyProperty 對象與 SetValue 和 GetValue 方法引用和設置屬性的過程,比早期通過 "SwirlyBrush" 等字符串引用屬性更為簡單明了。采用這種方法指定的屬性需要反射來實際設置屬性。

綁定源和目標

依賴關系屬性在 WPF 中的廣泛使用在設置 XAML 形式的數據綁定、樣式和動畫時並不明顯,但當您在 代碼中完成這些任務時卻是顯而易見的。BindingOperations 和 FrameworkElement 定義的 SetBinding 方法需要類型為 DependencyProperty 的對象作為綁定目標。WPF 樣式中使用的 Setter 類需要 DependencyProperty 對象。BeginAnimation 方法還需要使用 DependencyProperty 對象作為動畫目標。

這些目標必須都是 DependencyProperty 對象才能使 WPF 施加合適的優先權規則。例如,動畫設置的 屬性值比樣式設置的屬性值優先權高。(如果需要了解有哪些源負責特定的屬性值,您可以使用 DependencyPropertyHelper 類。)

盡管數據綁定目標必須是依賴關系屬性,但綁定源可以不必是依賴關系屬性。很顯然,如果綁定需要 能夠成功監控源中的變化,則綁定源必須采納某種通知機制,而 WPF 實際上允許三種不同類型的源通知 。並且這幾種類型是互斥的。

第一種(同時也是首選)方法是對源和目標均使用依賴關系屬性。如相同的屬性在不同上下文中分別 擔當綁定源和目標兩種角色,那麼這是一種不錯的解決方案。

第二種方法包括根據屬性名稱定義事件。舉例來說,如果某個類定義了名為 Flavor 的屬性,則它還 將定義名為 FlavorChanged 的事件。顧名思義,該類將在 Flavor 的值發生變化時觸發此事件。這種類 型的通知在 Windows 窗體中廣泛使用(例如,由 Control 定義的 Enabled 屬性具有相應的 EnabledChanged 事件)。但應該避免在 WPF 代碼中使用這種方法,原因在此不再贅述。

最後一種可行的方法是實現 INotifyPropertyChanged 接口,它要求類根據 PropertyChangedEventHandler 委托定義名為 PropertyChanged 的事件。相關聯的 PropertyChangedEventArgs 對象具有名為 PropertyName 的屬性,它代表發生更改的屬性的名稱字符串 。

例如,如果類具有名為 Flavor 的屬性和名為 flavor 的私有字段,則該屬性的 set 訪問器如下所示 :

flavor = value;
if (PropertyChanged != null)
 PropertyChanged(this, new PropertyChangedEventArgs("Flavor");

對 Flavor 屬性變化感興趣的類只需訂閱 PropertyChanged 事件,即可在該屬性(或該類中任何其他 屬性)發生變化時得到通知。

INotifyPropertyChanged 接口並不能做到萬無一失。該接口所能控制的僅是名為 PropertyChanged 的事件。並且不能保證該類一定會觸發此事件。在許多情況下,類會為某些公共屬性觸發該事件,但並不 一定會為所有屬性觸發該事件。如果您無法訪問源代碼,那沒有什麼很好的方法能夠事先了解哪些屬性會 觸發 PropertyChanged,而哪些屬性不會觸發該事件。

無論如何,INotifyPropertyChanged 對於那些不會成為 WPF 數據綁定、樣式或動畫目標的屬性來說 仍然是一種優秀、簡單的解決方案,但它必須提供對其他類的更改通知。

使用通知自定義集合

有時可能需要使用對象集合,並且需要在其中某個對象的屬性發生變化時得到通知。最有價值的通知 能夠准確告訴您集合中哪一個項目發生了變化,並且同時還能指出該項中哪個屬性發生了變化。

如集合中的所有對象都實現了 INotifyPropertyChanged 接口且集合本身實現了 InotifyCollectionChanged,這項任務相對就比較簡單。當新項目添加到集合,或者刪除現有項目時,實 現該接口的集合會觸發 CollectionChanged 事件。CollectionChanged 事件可以提供這些添加或刪除項 目的列表。或許泛型 ObservableCollection<T> 類是實現了 INotifyCollectionChanged 的最流 行的集合對象。

讓我們創建一個派生自 ObservableCollection<T> 的自定義集合類,並實現名為 ItemPropertyChanged 的新事件。集合中任何項目的屬性發生變化均會觸發該事件,並且伴隨該事件的參 數還包括項和發生變化的屬性。圖 2 顯示派生自 PropertyChangedEventArgs 的 ItemPropertyChangedEventArgs 類,它包含類型為對象、名稱為 Item 的新屬性。圖 2 還顯示了 ItemPropertyChangedEventHandler 委托。

圖 2 ItemPropertyChangedEventArgs

public class ItemPropertyChangedEventArgs : PropertyChangedEventArgs {
 object item;
 public ItemPropertyChangedEventArgs(object item,
  string propertyName) : base(propertyName) {
  this.item = item;
 }
 public object Item {
  get { return item; }
 }
}
public delegate void ItemPropertyChangedEventHandler(object sender,
 ItemPropertyChangedEventArgs args);

ObservableNotifiableCollection<T> 派生自 ObservableCollection<T>,並且定義了 如圖 3 所示的 ItemPropertyChanged 事件。請注意,將類型參數限制為 INotifyPropertyChanged 類型 的對象。在 OnCollectionChanged 重寫過程中,該類將為每個添加到集合的項目注冊一個 PropertyChanged 事件,並在從集合中刪除項目時刪除該事件。在項目的 PropertyChanged 事件中,集 合將觸發 ItemPropertyChanged 事件。PropertyChanged 事件的調用方對象(即屬性發生變化的項目) 將成為 ItemPropertyChangedEventArgs 的 Item 屬性。

圖 3 ObservableNotifiableCollection<T> 類

class ObservableNotifiableCollection<T> :
 ObservableCollection<T> where T : INotifyPropertyChanged {
 public ItemPropertyChangedEventHandler ItemPropertyChanged;
 protected override void OnCollectionChanged(
  NotifyCollectionChangedEventArgs args) {
  base.OnCollectionChanged(args);
  if (args.NewItems != null)
   foreach (INotifyPropertyChanged item in args.NewItems)
    item.PropertyChanged += OnItemPropertyChanged;
  if (args.OldItems != null)
   foreach (INotifyPropertyChanged item in args.OldItems)
    item.PropertyChanged -= OnItemPropertyChanged;
 }
 void OnItemPropertyChanged(object sender,
  PropertyChangedEventArgs args) {
  if (ItemPropertyChanged != null)
   ItemPropertyChanged(this,
    new ItemPropertyChangedEventArgs(sender,
    args.PropertyName));
 }
}

圖 2 和 3 中所示的代碼是項目 ObservableNotifiableCollectionDemo 的一部分,本專欄的可下載 代碼中包含該項目。它還包括一個小型演示程序。

依賴關系屬性和事件

盡管依賴關系屬性能夠很好地產生綁定源,但它不能以公共事件方式提供常規通知機制。第一次搜索 該事件時,這項令人不快的事實常常會讓您感到有點震驚。

但是,DependencyObject 確實定義了受保護的 OnPropertyChanged 方法。該方法是 DependencyObject 工作方式的基礎。根據我的經驗判斷,OnPropertyChanged 作為調用 SetValue 的結 果而被調用,並且調用與單個依賴關系屬性相關聯的回調方法的是 OnPropertyChanged。 OnPropertyChanged 的文檔中包含大量重寫此方法的危險警告。

如果正在編寫實現依賴關系屬性的類,可以想象:類的用戶希望在特定依賴關系屬性更改時觸發事件 。在這種情況下,可以顯式提供這些事件。可以將事件命名為屬性名後加單詞 "Changed"(類似於在 Windows 窗體中的事件命名方式),但使用 DependencyPropertyChangedEventHandler 委托定義事件。 作為模型,您會發現 UIElement 中的許多事件都與依賴關系屬性相關聯,其中包括 FocusableChanged 和 IsEnabledChanged。

對於前面顯示的 PopArt 示例,您可以將 SwirlyBrushChanged 事件定義為如下所示:

public event DependencyPropertyChangedEventHandler
 SwirlyBrushChanged;

在前面顯示的 OnSwirlyBrushChanged 方法實例中,采用如下方式觸發事件:

if (SwirlyBrushChanged != null)
 SwirlyBrushChanged(this, args);

這段代碼非常短小,但令人吃驚的是其他依賴關系屬性並沒有與之關聯的事件。

如果您需要使用的類沒有所需的事件,那麼您必須尋找另一種方式以便在特定依賴關系屬性更改時收 到通知。令人欣慰的是,至少有三種解決方案能應對這一挑戰:一種比較奇特,一種比較強硬,而最後一 種既不奇特,也不強硬。

動態綁定技術

比較奇特的方法是動態創建依賴關系屬性和數據綁定,以此接收依賴關系屬性更改的通知。如果正在 編寫繼承自 DependencyObject 的類,同時可以訪問包含依賴關系屬性的對象,並且需要在其中某個依賴 關系屬性更改時得到通知,那您可以創建依賴關系屬性和動態綁定以便得到所需的信息。

例如,假設您的類可以訪問名為 txtblk 的 TextBlock 類型的對象,並且您希望知道 Text 屬性何時 發生更改。Text 屬性由名為 TextProperty 的 DependencyProperty 提供支持,但 TextBlock 並未定義 TextChanged 事件。為修正這一問題,您需要首先注冊一個相同類型的 DependencyProperty 作為希望監 控的屬性:

DependencyProperty MyTextProperty =
 DependencyProperty.Register("MyText", typeof(string), GetType(),
 new PropertyMetadata(OnMyTextChanged));

該 DependencyProperty 對象不需要是公共靜態字段。如此處所示,可以在實例方法內部注冊該對象 。OnMyTextChanged 回調也可以作為實例方法。

當您注冊完 DependencyProperty 後,可以定義從 TextBlock 對象到該屬性的綁定:

Binding binding = new Binding("Text");
binding.Source = txtblk;
BindingOperations.SetBinding(this, MyTextProperty, binding);

現在對 txtblk 對象 Text 屬性的任何更改都將調用您的 OnMyTextChanged 回調。

您甚至可以在對所監控的 DependencyObject 和 DependencyProperty 一無所知的情況下注冊該 DependencyProperty 並定義回調。假設您僅知道需要跟蹤 DependencyObject 派生類 obj 中名為 Property 的 DependencyProperty 變量,代碼如下:

DependencyProperty OnTheFlyProperty =
 DependencyProperty.Register("OnTheFly",
 property.PropertyType,
 GetType(),
 new PropertyMetadata(OnTheFlyChanged));
Binding binding = new Binding(property.Name);
binding.Source = obj;
BindingOperations.SetBinding(this, OnTheFlyProperty, binding);

此解決方案最適合單個對象的一個或多個屬性。當處理相同類型的多個對象(例如集合中的對象)時 ,您需要為每個對象的每個屬性創建唯一的動態依賴關系屬性和綁定。這樣整個架構很快就會變得無法控 制。

Freezable 差異

Freezable 類是接收有關依賴關系對象更改的較為強硬的方法。它確實可以完成工作,但您無法實際 指出這一強硬措施針對的目標是什麼。

Freezable 替代由 DependencyObject 定義的 OnPropertyChanged 方法,並且定義了名為 Changed 的新屬性,根據文檔說明,該屬性將在“修改此 Freezable 或其所包含的任何對象”(增加的強調)時 觸發。在這段話中,只要將“其所包含的任何對象”理解為由依賴關系屬性支持的 Freezable 類型的子 屬性,那麼結果確實是正確的。

例如,假設名為 A 的 Freezable 類包含 B 類型名為 B 的屬性。B 類同樣派生自 Freezable 並且包 含 C 類型名為 C 的屬性。C 類也派生自 Freezable 並包含 double 類型名為 D 的屬性。所有三個屬性 都由依賴關系屬性支持。創建所有這些類型的對象;更改屬性 D;則類 A、B 和 C 都會觸發 Changed 事 件。(請注意,本專欄源代碼下載中包含的 NestedFreezableNotificationsDemo 項目可以演示此情形。 )

主要問題在於該 Changed 事件基於 EventHandler 委托,並且無法准確通告 Freezable 對象中哪個 屬性(或子屬性)發生了變化。並且這些通知的開銷較大,而這正是 Freezable 類因其無法更改且使所 有通知消失而得名的原因。

Freezable 在 WPF 圖形系統中使用廣泛。Brush、Pen、Geometry 和 Drawing 類都派生自 Freezable 。有幾種集合也派生自 Freezable,其中包括 DoubleCollection、PointCollection、VectorCollection 和 Int32Collection。這些集合還實現了 Changed 事件,當集合發生更改時將觸發該事件。其他由 Freezable 對象組成的 Freezable 集合(例如 GeometryCollection 和 TransformCollection)會在集 合中某個項目的任何屬性或子屬性發生更改時觸發 Changed 事件。

例如,PathGeometry 派生自 Freezable。它包含同樣派生自 Freezable 的 PathFigureCollection 類型的 Figures 屬性。PathFigureCollection 包含同樣派生自 Freezable 的 PathFigure 類型的對象 。PathFigure 包含名為 Segments 的屬性,它屬於 PathSegmentCollection 類型,也派生自 Freezable 。PathSegment 派生自 Freezable,並且是 PolyLineSegment 和 PolyBezierSegment 等類的基類。這些 類包含名為 Points 的屬性,它同樣派生自 Freezable,類型為 PointCollection。

總體效果:如任何單獨的點發生變化,更改通知將沿對象層次結構向上傳遞,並最終引起圖形對象完 全重繪。

PathGeometry 是否嘗試准確指出哪個點發生了變化,並僅更改相應部分的圖形?換句話說,它是否執 行增量更新?從其可以獲得的信息來判斷,它不可能做到這一點。PathGeometry 所知道的僅是某個點發 生了變化,需要一切從頭再來。

如果這樣(僅通知嵌套子屬性集合中的某個屬性發生更改)能夠滿足您的要求,那 Freezable 類將會 是非常理想的選擇。還有一個很有用的泛型 FreezableCollection<T>。類型參數可以不必是 Freezable 類型。它只需是 DependencyObject 即可。但如果是 Freezable 類型,集合項目的屬性或 Freezable 子屬性的更改會使集合觸發 Changed 事件。

請記住 Freezable 類存在某些限制。如果從 FreezableCollection<T> 派生類,您必須重寫 CreateInstanceCore(只需調用類的構造函數並返回該對象),否則您可能會得到令您抓狂的奇怪錯誤。

使用 DependencyPropertyDescriptor

第三種從依賴關系屬性獲取通知事件的技術是采用 DependencyPropertyDescriptor 類。這確實不是 顯而易見的解決方案,因為在文檔中介紹這種技術“主要供應用程序設計人員使用”,但它的確有效,而 這才是最重要的。

再次假設您有類型為 TextBlock 的對象,並且您希望知道 Text 屬性何時發生更改。首先您需要創建 類型為 DependencyPropertyDescriptor 的對象:

DepedencyPropertyDescriptor descriptor =
 DependencyPropertyDescriptor.FromProperty(
 TextBlock.TextProperty, typeof(TextBlock));

第一個參數是 DependencyProperty 對象,第二個參數是擁有該依賴關系屬性或繼承它的類。請注意 ,DependencyPropertyDescriptor 並未與任何特定的 TextBlock 對象相關聯。

然後您可以注冊事件處理程序,以檢測特定 TextBlock 對象(例如名為 txtblk 的對象)中 Text 屬 性的變化:

descriptor.AddValueChanged(txtblk, OnTextChanged);

請記住還需要 RemoveValueChanged 方法以便刪除事件處理程序。

OnTextChanged 事件處理程序基於簡單的 EventHandler 委托簽名,並很可能采用如下方式定義:

void OnTextChanged(object sender, EventArgs args) {
 ...
}

請注意,EventArgs 參數並不提供任何信息。但是,傳遞給事件處理程序的第一個參數是 Text 值發 生變化的 TextBlock 對象。

您可能不希望在多個依賴關系屬性間共享這些事件處理程序——您希望為每個屬性提供單獨的事件處 理程序。但您可以輕松在多個對象之間共享這些事件處理程序,因為可以通過傳遞給事件處理程序的第一 個參數區分對象。

現在萬事俱備,最後一步就是構建集合,它在集合中的項目依賴關系屬性發生變化時會觸發通知。

DependencyObjects 集合

理論上講,如果有派生自 DependencyObject 類型的對象集合,您應該能夠為特定依賴關系屬性創建 DependencyPropertyDescriptor 對象,以便在集合中任何項目的這些屬性發生變化時得到通知。

處理來自 DependencyPropertyDescriptor 的通知的方法會有一些妨礙。您不希望在多個依賴關系屬 性間共享一個方法,因為這樣您將無法確定哪個依賴關系屬性發生了更改。您希望為每個依賴關系屬性創 建單獨的方法。通常,無法在運行之前確定所需的方法個數。雖然可以在運行時動態創建方法,但這會產 生中間語言。相對簡單的方法是:定義一個包含專門處理這些通知的方法的類,然後為每個希望監控的依 賴關系屬性創建一個該類的實例。

我創建了 ObservableDependencyObjectCollection<T> 集合類用來完成這項工作,它派生自 ObservableCollection<T>。請注意,此集合中的項目必須是派生自 DependencyObject 的類的實 例。集合類為每個由該類型參數定義或繼承的 DependencyProperty,或者為選定的 DependencyProperty 對象列表創建 DependencyPropertyDescriptor 對象。

ObservableDependencyObjectCollection<T> 定義了名為 ItemDependencyPropertyChanged 的 新事件。圖 4 顯示該事件參數的定義和對此事件的委托。ItemDependencyPropertyChangedEventArgs 類 似於正常的 DependencyPropertyChangedEventArgs,不同之處在於它包含 Item 屬性而沒有 OldValue 屬性。

圖 4 ItemDependencyPropertyChangedEventArgs

public struct ItemDependencyPropertyChangedEventArgs {
  DependencyObject item;
  DependencyProperty property;
  object newValue;
  public ItemDependencyPropertyChangedEventArgs(
    DependencyObject item,
    DependencyProperty property,
    object newValue) {
    this.item = item;
    this.property = property;
    this.newValue = newValue;
  }
  public DependencyObject Item {
    get { return item; }
  }
  public DependencyProperty Property {
    get { return property; }
  }
  public object NewValue {
    get { return newValue; }
  }
}
public delegate void ItemDependencyPropertyChangedEventHandler(
  Object sender,
  ItemDependencyPropertyChangedEventArgs args);

我將分三個部分為您介紹實際的 ObservableDependencyObjectCollection<T> 類。類的第一部 分如圖 5 所示。盡管該類派生自 ObservableCollection<T>,但它將類型參數限制為 DependencyObject。ObservableCollection<T> 有兩個構造函數:一個不帶參數,而另一個使用泛 型 List<T> 對象初始化內容。新類不僅包括這兩個構造函數,而且它還添加了另外兩個構造函數 用於指定希望監控的 DependencyProperty 對象列表。

例如,假設您希望監控 Button 對象集合,但您只希望在兩個與字體相關的屬性發生變化時得到通知 。此構造函數如下:

ObservableDependencyObjectCollection<Button> buttons =
new ObservableDependencyObjectCollection(
Button.FontSize, Button.FontFamily);

您可以隨後為該集合安裝事件處理程序:

buttons.ItemDependencyPropertyChanged +=
OnItemDependencyPropertyChanged;

圖 5 中的所有構造函數最終都會調用 GetExplicitDependencyProperties 或 GetAllDependencyProperties。第一種方法僅為構造函數參數中所列出的每個 DependencyProperty 對象 調用 CreateDescriptor。第二種方法使用反射獲取由類型參數及其祖先(即 BindingFlags.FlattenHierarchy 的用意)共同定義的 DependencyProperty 類型的所有公共字段,並隨 後為每個對象調用 CreateDescriptor。

圖 5 ObservableDependencyObjectCollection 構造函數

public class ObservableDependencyObjectCollection<T> :
  ObservableCollection<T> where T : DependencyObject {
  public event ItemDependencyPropertyChangedEventHandler
    ItemDependencyPropertyChanged;
  List<DescriptorWrapper> descriptors = new List<DescriptorWrapper>();
  public ObservableDependencyObjectCollection() {
    GetAllDependencyProperties();
  }
  public ObservableDependencyObjectCollection(List<T> list) : base(list) {
    GetAllDependencyProperties();
  }
  public ObservableDependencyObjectCollection(
    params DependencyProperty[] properties) {
    GetExplicitDependencyProperties(properties);
  }
  public ObservableDependencyObjectCollection(List<T> list,
    params DependencyProperty[] properties) : base(list) {
    GetExplicitDependencyProperties(properties);
  }
  void GetExplicitDependencyProperties(
    params DependencyProperty[] properties) {
    foreach (DependencyProperty property in properties)
      CreateDescriptor(property);
  }
  void GetAllDependencyProperties() {
    FieldInfo[] fieldInfos =
      typeof(T).GetFields(BindingFlags.Public |
                          BindingFlags.Static |
                BindingFlags.FlattenHierarchy);
    foreach (FieldInfo fieldInfo in fieldInfos)
      if (fieldInfo.FieldType == typeof(DependencyProperty))
        CreateDescriptor(fieldInfo.GetValue(null) as DependencyProperty);
  }
  void CreateDescriptor(DependencyProperty property) {
    DescriptorWrapper descriptor =
      new DescriptorWrapper(typeof(T), property,
      OnItemDependencyPropertyChanged);
      descriptors.Add(descriptor);
  }

對 CreateDescriptor 方法的每次調用都將創建一個 DescriptorWrapper 類型的新對象,並向該對象 傳遞集合中項目的類型、要監控的 DependencyProperty,以及名為 OnItemDependencyPropertyChanged 的回調方法。

圖 6 中所示的 DescriptorWrapper 類位於 ObservableDependencyObjectCollection<T> 的內 部,實際封裝 DependencyPropertyDescriptor 對象。構造函數保存與 DependencyPropertyDescriptor 和回調方法相關聯的特定 DependencyProperty。

圖 6 DescriptorWrapper 類

class DescriptorWrapper {
  DependencyPropertyChangedEventHandler
    OnItemDependencyPropertyChanged;
  DependencyPropertyDescriptor desc;
  DependencyProperty dependencyProperty;
  public DescriptorWrapper(Type targetType,
    DependencyProperty dependencyProperty,
    DependencyPropertyChangedEventHandler
    OnItemDependencyPropertyChanged) {
    desc = DependencyPropertyDescriptor.FromProperty(
      dependencyProperty, targetType);
    this.dependencyProperty = dependencyProperty;
    this.OnItemDependencyPropertyChanged =
      OnItemDependencyPropertyChanged;
  }
  public void AddValueChanged(DependencyObject dependencyObject) {
    desc.AddValueChanged(dependencyObject, OnValueChanged);
  }
  public void RemoveValueChanged(DependencyObject dependencyObject) {
    desc.RemoveValueChanged(dependencyObject, OnValueChanged);
  }
  void OnValueChanged(object sender, EventArgs args) {
    OnItemDependencyPropertyChanged(sender,
      new DependencyPropertyChangedEventArgs(
      dependencyProperty,
      null,
      (sender as DependencyObject).GetValue(
      dependencyProperty)));
  }
}

該類還包括兩種分別名為 AddValueChanged 和 RemoveValueChanged 的方法,它們會調用 DependencyPropertyDescriptor 對象中的相應方法。OnValueChanged 事件處理程序將篩選返回集合類的 信息。

ObservableDependencyObjectCollection<T> 類的最後一部分如圖 7 所示。 OnCollectionChanged 的替代方法負責遍歷所有添加到集合的項目(以及每個 DescriptorWrapper),並 調用 AddValueChanged。每個從集合中刪除的項目都會刪除相應的事件處理程序。

圖 7 ObservableDependencyObjectCollection

protected override void OnCollectionChanged(
  NotifyCollectionChangedEventArgs args) {
  base.OnCollectionChanged(args);
  if (args.NewItems != null)
    foreach (DependencyObject obj in args.NewItems)
      foreach (DescriptorWrapper descriptor in descriptors)
        descriptor.AddValueChanged(obj);
  if (args.OldItems != null)
    foreach (DependencyObject obj in args.OldItems)
      foreach (DescriptorWrapper descriptor in descriptors)
        descriptor.RemoveValueChanged(obj);
}
protected void OnItemDependencyPropertyChanged(object item,
  DependencyPropertyChangedEventArgs args) {
  if (ItemDependencyPropertyChanged != null)
    ItemDependencyPropertyChanged(this,
      new ItemDependencyPropertyChangedEventArgs(
      item as DependencyObject,
      args.Property, args.NewValue));
}

ObservableDependencyObjectCollection<T> 類的最後一個 OnItemDependencyPropertyChanged 方法從 DescriptorWrapper 中調用,並會觸發 ItemDependencyPropertyChanged 事件,它是整個練習的目的。

ObservableDependencyObjectCollection<T> 類是結構化的類,因此它將在根據類型參數構造 集合時創建所有必需的 DescriptorWrapper 對象。如果將類型參數指定為 DependencyObject 並使用無 參數的構造函數,則不會創建任何 DescriptorWrapper 對象,因為 DependencyObject 不會定義任何其 自身的依賴關系屬性。但是,可以將類型參數指定為 DependencyObject 並包括 DependencyProperty 對 象的顯式列表,這樣通知也能正常工作。

在實際情形中,您可能希望將 ObservableDependencyObjectCollection<T> 的類型參數設置為 FrameworkElement,並使用各種 FrameworkElement 的派生對象(例如 TextBlock 和 Button)填充集合。集合將僅報告 UIElement 和 FrameworkElement 定義的依賴關系屬性的更改,但不報告由派生類定義的屬性更改。

為提高靈活性, 必須基於這些項目的類型和由這些類型定義的依賴關系屬性編寫集合,從而能在添加項目時創建新的 DescriptorWrapper 對象。因為不希望創建重復的 DescriptorWrapper 對象,所以您需要首先檢查是否 已經為每種特殊依賴關系屬性創建過 DescriptorWrapper。

當從集合中刪除項目時,您需要刪除不再 需要的 DescriptorWrapper 對象。這意味著應該為每個 DescriptorWrapper 連接使用計數,並且當使用 計數減為零時,應該從 ObservableDependencyObjectCollection<T> 的描述符集合中刪除 DescriptorWrapper。

也可以創建非常靈活且監控所有項目屬性的集合類,而不必考慮項目是否定義了 依賴關系屬性或是否實現了 INotifyPropertyChanged 接口。

如您所見,只要正確處理,即使是依賴 關系屬性也可以像 Microsoft.NET Framework 中的前輩們一樣學會生成事件。

請將您想詢問的問題和 提出的意見發送至 [email protected]

Charles Petzold 是《MSDN 雜志》的特約編輯。他的最新著作是《The Annotated Turing:A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine》。

本文配套源碼:http://www.bianceng.net/dotnet/201212/741.htm

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved