一旦你挑选好一个基类,你将要为你的控件设计一个API。大部分WPF元素提供属性暴露了多数功能,事件,命令,因为他们从框架中获取广泛的支持,以及易于使用XAML。WPF框架对routed event和命令提供了自动支持,它的依赖属性系统提供了数据半岛和动画支持。当然,你也可以写方法——对于某一种功能,方法是最好的途径。(例如,ListBox有一个ScrollIntoView方法,保证了一个特定的项目是可见的。这时从代码中能够做的方便的事情。)但是,我更喜欢在合理的地方使用属性,事件以及命令。
9.3.1属性
.NET类型系统提供了一个标准的方式为一个对象定义属性。它指定了一个协定:提供了get和set的方法访问器,但是对于这些的实现,以及属性值的存储方式,都留给了开发者。在WPF中,元素通常使用依赖属性系统。.NET提供了代表性的样式属性访问器,但是这些仅仅是对依赖属性(DP)的包装,增加了便利。
DP系统添加了大量的特色——并不有标准.NET属性提供。例如,DP从父元素中继承了它的值。这与OO意义上的继承不同,DP是从其基类继承其特征(虽然DP也支持OO意义的继承性)。属性值的继承性是一个更动态的特征,允许在一个单元素上设置属性,以及自动传播到它的所有子元素。例如,所有的元素有一个Cursor属性用来控制鼠标指针。这个属性使用了值的继承性,意味着一旦你在元素上看到Cursor,所有的子元素将自动得到同样的Cursor属性值。(如果你使用Windows Forms,你将熟悉这个概念,这里任意的元素都具有相同的特征。)
DP在别处也自动获取它们的值。DP支持数据绑定和样式,它们提供一个定义默认值的机制。动画系统也依赖于DP,它使用了DP结构来即时的调整属性值。
通过实现你的元素属性,如DP,你不仅可以自动得到这些特征,而且DP系统还为你管理着值的存储。你不必为定义任何字段实例来存储属性值。
存储管理器看起来是件小事情,毕竟,为类添加一个字段是多么的困难?尽管如此,这种特征能提供令人惊讶的有意义的内存存储。
简单的继承于Control,你的元素可以支持多于40个属性(加上任何附属属性)来改变复杂性,其中大部分看起来都具有一个默认值对于大多数对象而言。如果每个元素都有自己的一组字段存储这些值,每个元素将占用数百字节。一个复杂的用户界面可能需要成千的字节(即使UI有一个相当简单的结构,可视化树可以显著地增加元素的数量。)
多数元素的大部分属性或者继承与它们的父类或者设置为它们的默认值,然后使用元素按字段存储这些值,这将浪费成百上千的内存。更加高级的存储方式暴露了这样的事实:多数未设置的属性是有效的。而且随着内存的便宜,在CPU中移入移出数据是昂贵的。CPU可以比数据转换主内存, 更快的执行代码。只有内存缓存可以相当快的跟上处理器,而且大多数现代化的处理器典型地只有成百上千字节的缓存。甚至高端系统仅有几个兆字节的缓存。保存成百上千字节能够显著的提高性能。
采取DP系统,我们可以让它更加有效地处理信息——通过仅对显示设定的属性值排序。
最后,DP系统跟踪了值的改变。这意味着一旦任何感兴趣的部分想知道一个属性值何时改变,它能使用DP系统注册通知。(数据绑定取决于次。)我们不需要写任何特殊的代码使之发生。DP系统管理者我们属性值的存储,因此它知道何时属性改变。
任何你创建的WPF自定义元素,将会自动地支持DP的一切,因为FrameworkElement间接派生于DependencyObject基类。为了在我们的自定义元素上定义一个新的属性,我们必须在元素的静态构造函数中创建一个新的DependencyObject对象。作为惯例,我们暴露了对象属性,通过在我们的类中,按一个公有的静态字段排序,正如示例9-1所示。
示例9-1
public static DependencyProperty FooProperty;
static MyCustomControl( ) {
PropertyMetadata fooMetadata = new PropertyMetadata(Brushes.Green);
FooProperty = DependencyProperty.Register("Foo", typeof(Brush),
typeof(MyCustomControl), fooMetadata);
}
public Brush Foo {
get { return (Brush) GetValueBase(FooProperty); }
set { SetValueBase(FooProperty, value); }
}
}
自定义控件定义了一个单独的名为Foo的DP,类型为Brush。当注册一个属性时,传递Brushes.Green这个默认值在PropertyMetadata对象中。
#你可能想知道为什么WPF发明了新类型来表现属性和关联元数据,当反射API已经提供了PropertyInfo类以及一个扩展机制以自定义属性的形式。不幸的是,反射API不能提供WPF需要的弹性和性能的联合。这是为什么在DP元数据和反射之间有交叠的原因。
示例9-1还提供了一个标准的.NET属性——成对的get和set。这些不是确实需要。你可以使用公有的继承自DependencyObject的GetValue和SetValue方法访问属性,如下:
尽管如此,在大多数.NET语言中,使用正规的CLR属性将是很容易的,因此你通常要提供一个恰当的包装,如示例9-1。正如你看到的,访问器简单的使用继承自DependencyObject基类的GetValueBase和SetValueBase方法。这些方法被特殊的定义用来被属性访问器调用。
示例9-2显示了在xaml中如何使用自定义属性。(这里假设命名空间包含了这个被关联到XML命名空间前缀local的控件。参见附录A获取更多关于.NET命名空间和XML命名空间之间关系的信息)
示例9-2
注意到因为我们的属性是Brush,我们可以使用同样的文字速记格式,用来表示我们在第7章看到的笔刷。示例9-2用此来创建一个垂直渐变的笔刷。
9.3.1.1附属属性
如果你希望定义一个附属属性,一种是将其应用到元素而不是定义你使用DP系统注册的元素,通过一个不同的调用:RegisterAttached。正如示例9-3显示,这个方法的调用方式与Register方法一样。
示例9-3
public static DependencyProperty IsBarProperty;
static ControlWithAttachedProp ( ) {
PropertyMetadata isbarMetadata = new PropertyMetadata(false);
IsBarProperty = DependencyProperty.RegisterAttached("IsBar", typeof(bool),
typeof(ControlWithAttachedProp), isbarMetadata);
}
public static bool GetIsBar(DependencyObject target) {
return (bool) target.GetValueBase(IsBarProperty);
}
public static void SetIsBar(DependencyObject target, bool value) {
target.SetValueBase(IsBarProperty, value);
}
}
注意到,访问器看上去不太一样。.NET并未定义一个标准的暴露属性的方式,这些属性由一个类型定义,但是可以应用到另一种类型。XAML和WPF承认示例9-3中的约定语法,其中我们定义了一对静态方法GetPropname和SetPropname。这些方法都是将目标对象传递给要应用到的属性。
示例9-4展示了如何在xaml中应有一个自定义附属属性到一个Button元素。
示例9-4
9.3.1.2 值改变的通知机制
你不能总是使用方法访问器设置属性,例如,数据绑定和动画使用了DP系统直接修改属性值。如果你需要知道属性值什么时候改变,你应该依赖于被调用的访问器,因为它们不会经常改变。取代的,你应该注册无效的通知,在属性注册期间,通过传递一个回调到PropertyMetadata。这将以同样的方式工作在正常属性和附属属性上。示例9-5显示了对示例9-3的改动,从而当属性改变时收到通知。
示例9-5
static ControlWithAttachedProp ( ) {
PropertyInvalidatedCallback isBarInvalidated =
new PropertyInvalidatedCallback(OnIsBarChanged);
PropertyMetadata isbarMetadata = new PropertyMetadata(false, isBarInvalidated);
IsBarProperty = DependencyProperty.RegisterAttached("IsBar", typeof(bool),
typeof(ControlWithAttachedProp), isbarMetadata);
}
static void OnIsBarChanged(DependencyObject target) {
Debug.WriteLine("IsBar just changed: " + GetIsBar(target));
}
无论何时属性被改动,处理改动的函数都将被调用,不论是在示例9-3调用静态的SetIsBar方法来改变,还是直接使用DP系统在代码中改变。
9.3.2事件
让我们看一下第三章中routed事件的处理。如果你希望为内容定义自定义事件,将它们实现为routed事件就是有意义的。不仅使你的元素与其它WPF元素保持一致,而且你可以恰当地利用同样的bubbing和tunnel路由策略。
创建自定义的路由事件有点像创建自定义属性。你简单的创建它们在类的静态构造函数中。方便起见,你还可以添加一个.NET样式对底层的路由事件处理进行包装。这些技术被示范在示例9-6中。
示例9-6
public static RoutedEvent BarEvent;
public static RoutedEvent PreviewBarEvent;
static MyCustomControl( ) {
BarEvent = EventManager.RegisterRoutedEvent("Bar",
RoutingStrategy.Bubble, typeof(EventHandler), typeof(MyCustomControl));
PreviewBarEvent = EventManager.RegisterRoutedEvent("PreviewBar",
RoutingStrategy.Tunnel, typeof(RoutedEventHandler),
typeof(MyCustomControl));
}
public event RoutedEventHandler Bar {
add { AddHandler(BarEvent, value); }
remove { RemoveHandler(BarEvent, value); }
}
public event RoutedEventHandler PreviewBar {
add { AddHandler(PreviewBarEvent, value); }
remove { RemoveHandler(PreviewBarEvent, value); }
}
protected virtual void OnBar( ) {
RoutedEventArgs args = new RoutedEventArgs( );
args.RoutedEvent = PreviewBarEvent;
RaiseEvent(args);
if (!args.Handled) {
args = new RoutedEventArgs( );
args.RoutedEvent = BarEvent;
RaiseEvent(args);
}
}
}
示例9-6显示了定义一对事件:一个PreviewBar(对应tunneling)事件和一个Bar(对应bubbling)事件。这样就提供给.NET事件成员以便利——推迟到基类中的AddHandler和RemoveHandler方法。
这个事例还提供了OnBar方法来激发事件。这将激发preview事件,而且如果没有标记为已处理,将会继续激发主要的Bar事件。RaiseEvent方法由使用routed事件的基类提供,会调用。注意到正如标准的CLR事件,由异步RaiseEvent激发的routed事件,将会顺序的调用事件句柄,直到全部执行完毕才会返回。
9.3.2.1附属属性
正如一些属性可以被附属到类型上——而不是直接定义的类型,事件也是这样。不同于依赖属性,routed事件不需要以一种不同的方式注册来作为附属事件工作。例如,你可以为示例9-6定义的MyCustomControl.Bar事件附属一个句柄到一个Button上,如示例9-7所示。
示例9-7
myButton.AddHandler(MyCustomControl.BarEvent, handler);
这个示例提及的MyCustomControl是一个事件句柄方法,将会被调用,当这个按钮上的Bar事件被激活的时候。当然,这个按钮并不知道Bar事件,因此我们需要写一些代码来激活事件,如示例9-8。
示例9-8
re.RoutedEvent = MyCustomControl.BarQuuxEvent;
myButton.RaiseEvent(re);
附属事件支持你将你自己的事件引进到UI树中,而不需要担心树中的元素是否知道这些事件。
9.3.3命令
我们在第3章看到了WPF的RoutedCommand类表示用户的一个特定的动作,这个动作可能被任意数量的不同输入所调用。一个自定义控件有两种办法想和命令系统进行交互:可能定义新的命令类型;或者处理定义在别处的命令。
示例9-9显示了如何注册一个自定义命令。
示例9-9
public static RoutedCommand FooCommand;
static MyControl( ) {
InputGestureCollection fooInputs = new InputGestureCollection( );
fooInputs.Add(new KeyGesture(Key.F,
ModifierKeys.Control|ModifierKeys.Shift));
FooCommand = new RoutedCommand("Foo", typeof(MyControl), fooInputs);
}
}
代表性的,你想要制作自己的控件处理任意自定义命令。你可能还向处理一个已有的命令。例如,你可能希望响应一些由CommandLibrary提供的标准命令。在第三章,我们看到通过添加一个CommandBinding到你的自定义控件的CommandBindings集合,可以达到这个目的。然而,对于自定义控件,这通常不是一个恰当的技术。通常你想要你的控件的所有实例都按照同样的方式响应命令,而且你可能为每一个实例设立命令绑定,最好是注册一个类的处理器。这使你在静态构造函数中一次性设立一个命令处理联合,这将会为你的自定义元素的所有实例工作。示例9-10显示了如何去做。
示例9-10
static MyCustomControl( ) {
CommandBinding copyCommandBinding = new CommandBinding(
CommandLibrary.Copy,
HandleCopyCommand);
CommandManager.RegisterClassCommandBinding(typeof(MyCustomControl),
copyCommandBinding);
}
private static void HandleCopyCommand(object target, ExecuteEventArgs e) {
MyCustomControl myControl = (MyCustomControl) target;
}
}
注意到,处理器必须是静态的方法。当你的静态构造函数执行时,还没有一个自定义控件的实例。除此之外,处理器将会代表所有实例注册一次,因此将其放在一个实例方法中是没有意义的。当一个命令被调用时,处理器将传递一个引用到目标元素作为它的第一个参数。
实例9-11显示了在xaml中配置一个Button,当点击的时候会调用这个自定义命令。
实例9-11