这些天来,对象似乎已经忙得晕头转向了。每个人都希望它们做这做那。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<T> 构造函数
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<T>
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 中的前辈们一样学会生成事件。