WPF基础:依赖关系属性和通知
Alex | Tags: WPF Comments: 0 | 订阅文章 |
这些天来,对象似乎已经忙得晕头转向了。每个人都希望它们做这做那。Windows® Presentation Foundation (WPF) 应用程序中的典型对象会接到各种各样不同的请求:有要求绑定到数据的、有要求更改样式的、有要求从可见父项继承的,甚至还有要求来点动画让大家高兴一下的。
对象怎么才能建立起边界和优先级呢?WPF 的回答是一种称为依赖关系属性的功能。通过为 WPF 类提供结构化方法来响应由数据绑定、样式、继承和其他来源更改带来的变化,依赖关系属性已变得十分重要,其程度不亚于事件和事件处理对早期 .NET 程序员的重要性。
当然,依赖关系属性不是万能的,它可能无法为某些传统任务提供您所需要的所有功能。假设您可以访问具有依赖关系属性的对象,并且希望在其中某个属性发生变化时得到通知。这对于事件处理程序来说好像并不是什么难事——但您要知道实际上根本不存在这样的事件!
当您使用对象集合时,这个问题更加突出。在某些情况下,您可能希望当集合中对象的某些特定依赖关系属性发生更改时得到通知。现有唯一的解决方案是 FreezableCollection<T>,它能够告诉您何时发生变化——但不能告诉您是什么发生了变化。
依赖关系属性基础知识
假 设您正在设计名为 PopArt 的 WPF 类,并且希望定义类型为 Brush 的属性 SwirlyBrush。如果 PopArt 继承自 DependencyObject,您就可以将 SwirlyBrush 定义为 DependencyProperty。第一步是公共静态只读字段:
public static readonly DependencyProperty SwirlyBrushProperty;
SwirlyBrushProperty = DependencyProperty.Register("SwirlyBrush",typeof(Brush),typeof(PopArt),new PropertyMetadata(OnSwirlyBrushChanged));
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 = new SolidColorBrush(Colors.AliceBlue);
但 是,还需要注意 PopArt 类中存在名为 PopArt.SwirlyBrushProperty 的公共静态字段。该字段是类型为 DependencyProperty 的有效对象,独立于所有类型为 PopArt 的对象存在。这使您能够在创建具有该属性的对象之前引用类的特定属性。
popart.SetValue(PopArt.SwirlyBrushProperty,new SolidColorBrush(Colors.AliceBlue));
target.SetValue(property,value);
如果值类型与 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 = value;
if (PropertyChanged != null)
PropertyChanged(this,new PropertyChangedEventArgs("Flavor");
INotifyPropertyChanged 接口并不能做到万无一失。该接口所能控制的仅是名为 PropertyChanged 的事件。并且不能保证该类一定会触发此事件。在许多情况下,类会为某些公共属性触发该事件,但并不一定会为所有属性触发该事件。如果您无法访问源代码,那 没有什么很好的方法能够事先了解哪些属性会触发 PropertyChanged,而哪些属性不会触发该事件。
@H_301_303@
无论如何,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));
}
}
依赖关系属性和事件
但 是,DependencyObject 确实定义了受保护的 OnPropertyChanged 方法。该方法是 DependencyObject 工作方式的基础。根据我的经验判断,OnPropertyChanged 作为调用 SetValue 的结果而被调用,并且调用与单个依赖关系属性相关联的回调方法的是 OnPropertyChanged。OnPropertyChanged 的文档中包含大量重写此方法的危险警告。
如 果正在编写实现依赖关系属性的类,可以想象:类的用户希望在特定依赖关系属性更改时触发事件。在这种情况下,可以显式提供这些事件。可以将事件命名为属性 名后加单词 "Changed"(类似于在 Windows 窗体中的事件命名方式),但使用 DependencyPropertyChangedEventHandler 委托定义事件。作为模型,您会发现 UIElement 中的许多事件都与依赖关系属性相关联,其中包括 FocusableChanged 和 IsEnabledChanged。
@H_303_403@
对于前面显示的 PopArt 示例,您可以将 SwirlyBrushChanged 事件定义为如下所示: