样式提供了重用一组属性设置的实用方法。它们为帮助构建一致的、组织良好的界面迈出了重要的第一步——但是它们也是有许多限制。
问题是在典型的应用程序中,属性设置仅是用户界面基础结构的一小部分。甚至最基本的程序通常也需要大量的用户界面代码,这些代码与应用程序的功能无关。在许多程序中,用于用户界面任务的代码(如驱动动画、实现平滑效果、维护用户界面状态,以及支持诸如拖放、缩放以及停靠等用户界面特性)无论是在数量山还是复杂性上都超出了业务代码。许多这类代码是通用的,这意味着在创建的每个WPF对象中需要编写相同的内容。所有这些工作几乎都单调乏味的。
为回应这一挑战,Expression Blend创作者开发了称为行为(behavior)的特征。其思想很简单:创建封装了一些通用用户界面功能的行为。这一功能可以是基本功能(如启动故事板或导航到超链接),也可以是复杂功能(如处理多点触摸交互,或构建使用实时物理引擎的碰撞模型)。一旦构建功能,就可将它们添加到任意应用程序的另一个控件中,具体方法是将该控件链接到适当的行为并设置行为的属性。在Expression Blend中,只通过拖放操作就可以使用行为。
一、获取行为支持
重用用户界面的代码通用块得基础结构不是WPF的一部分。反而,它被捆绑到Expression Blend。这是因为行为开始时作为Expression Blend的设计时特性引入的。但这并不意味着行为只能用于Expression Blend。只需要付出很少的努就可以在Visual Studio应用程序中的创建和使用行为。只需要手动缩写标记,而不是使用工具箱。
为了获得支持行为的程序集,有两种选择:
- 可按照Expression Blend 3、Expression Blend 4 或Visual Studio 2012以上版本。所有这些版本都包含Visual Studio中的行为功能所需的程序集。
- 可安装Expression Blend SDK。
无论是使用Expression Blend的哪个版本,都可以在文件夹中看到所需的两个相同的重要程序集:
- System.Windows.Interactivity.dll。这个程序集定义了支持行为的基本类。它是行为特征的基础。
- Microsoft.Expression.Interactions.dll。这个程序集通过添加可选的以核心行为类为基础的动作和触发器类,增加了一些有用的扩展。
二、理解行为模型
行为特性具有两个版本:一个版本是旨在Silverlight添加行为支持,Silverlight是Microsoft的针对浏览器的富客户端插件;而另一个版本是针对WPF设计的。尽管这两个版本提供了相同的特性,但行为特性和Silverlight领域更吻合,因为它弥补了更大的鸿沟。与WPF不同,Silverlight不支持触发器,所以实现行为的程序集也实现触发器更合理。然而,WPF支持触发器,行为特性包含自己的触发器系统,而触发器系统与WPF模型不匹配,这确实令人感到有些困惑。
问题在于具有类似名称的这两个特性有部分重合但不完全相同。在WPF中,触发器最重要的角色是构建灵活的样式和控件模板。在触发器的帮助下,样式和模板变得更加智能;例如,当一些熟悉发生变化时可应用可视化效果。然而,Expression Blend中的触发器系统具有不同的目的。通过使用可视化设计工具,允许为应用程序添加简单功能。换句话说,WPF触发器支持更加强大的样式和控件模板。而Expression Blend触发器支持快速的不需要代码的应用程序设计。
那么,对于使用WPF的普遍开发人员来说所有这些意味着什么呢?下面是几条指导原则:
- 行为模型不是WPF的核心部分,所以行为不像样式和模板那样确定。换句话说,可编写不使用行为的WPF应用程序,但如果不使用样式和模板,就不能创建比“Hello World”演示更复杂的WPF应用程序。
- 如果在Expression Blend上耗费大量时间,或希望为其他Expression Blend用户开发组件,可能会对Expression Blend中的触发器特性感兴趣。尽管和WPF中的触发器系统使用相同的名称,但它们不能相互重叠,可以同时使用这两者。
- 如果不使用Expression Blend,可完全略过触发器特性——但仍应分析Expression Blend提供的功能完整的行为类。这是因为行为比Expression Blend的触发器更强大也更常用。最终,将准备查找那些提供了可在自己的应用程序中使用的整洁美观行为的第三方组件。
三、创建行为
行为旨在封装一些UI功能,从而可以不比编写代码就能够将其应用到元素上。从另一个角度看,每个行为都为元素提供了一个服务。该服务通常涉及监听几个不同的事件并执行几个相关的操作。
为更好地理解行为,最好自己创建一个行为。设想希望为任意元素提供使用鼠标在Canvas面板上拖动元素的功能。对于单个元素实现该功能的基本步骤是非常简单的——代码监听鼠标事件并修改设置相应Canvas坐标的附加属性。但通过付出更多一点的努力,可将该代码转换为可重用的行为,该行为可为Canvas面板上的所有元素提供拖动支持。
在继续之前,创建一个WPF类库程序集。在该程序集中,添加System.Windows.Interactivity.dll程序集的引用。然后,创建一个继承自Behavior基类的类。Behavior是通用类,该类使用一个类型参数。可使用该类型参数将行为限制到特定的元素,或使用UIElement或FrameworkElement将它们都包含进来。如下所示:
public class DragInCanvasBehavior : Behavior<UIElement> { }
在任何行为中,第一步要覆盖OnAttached()和OnDetaching()方法。当调用OnAttached()方法时,可通过AssociatedObject属性访问放置行为的元素,并可关联事件处理程序。当调用OnDetaching()方法时,移除事件处理程序。
下面是DragInCanvasBehavior类用于监视MouseLeftButtonDown、MouseMove以及MouseLeftButtonUp事件代码:
protected override void OnAttached() { base.OnAttached(); // Hook up event handlers. this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseMove += AssociatedObject_MouseMove; this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp; } protected override void OnDetaching() { base.OnDetaching(); // Detach event handlers. this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove; this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp; }
最后一步是在事件处理程序中运行适当的代码。例如,当用户单击鼠标左键时,DragInCanvasBehavior开始拖动操作、记录元素左上角与鼠标指针之间的偏移并捕获鼠标:
private Canvas canvas; // Keep track of when the element is being dragged. private bool isDragging = false; // When the element is clicked, record the exact position // where the click is made. private Point mouseOffset; private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // Find the canvas. if (canvas == null) canvas = VisualTreeHelper.GetParent(this.AssociatedObject) as Canvas; // Dragging mode begins. isDragging = true; // Get the position of the click relative to the element // (so the top-left corner of the element is (0,0). mouseOffset = e.GetPosition(AssociatedObject); // Capture the mouse. This way you‘ll keep receiveing // the MouseMove event even if the user jerks the mouse // off the element. AssociatedObject.CaptureMouse(); }
当元素处于拖动模式并移动鼠标时,重新定位元素:
private void AssociatedObject_MouseMove(object sender, MouseEventArgs e) { if (isDragging) { // Get the position of the element relative to the Canvas. Point point = e.GetPosition(canvas); // Move the element. AssociatedObject.SetValue(Canvas.TopProperty, point.Y - mouseOffset.Y); AssociatedObject.SetValue(Canvas.LeftProperty, point.X - mouseOffset.X); } }
当释放鼠标键时,结束拖动:
private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (isDragging) { AssociatedObject.ReleaseMouseCapture(); isDragging = false; } }
DragInCanvasBehavior类的完整代码如下所示:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Interactivity; using System.Windows.Media; namespace CustomBehaviorsLibrary { public class DragInCanvasBehavior : Behavior<UIElement> { private Canvas canvas; protected override void OnAttached() { base.OnAttached(); // Hook up event handlers. this.AssociatedObject.MouseLeftButtonDown += AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseMove += AssociatedObject_MouseMove; this.AssociatedObject.MouseLeftButtonUp += AssociatedObject_MouseLeftButtonUp; } protected override void OnDetaching() { base.OnDetaching(); // Detach event handlers. this.AssociatedObject.MouseLeftButtonDown -= AssociatedObject_MouseLeftButtonDown; this.AssociatedObject.MouseMove -= AssociatedObject_MouseMove; this.AssociatedObject.MouseLeftButtonUp -= AssociatedObject_MouseLeftButtonUp; } // Keep track of when the element is being dragged. private bool isDragging = false; // When the element is clicked, record the exact position // where the click is made. private Point mouseOffset; private void AssociatedObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { // Find the canvas. if (canvas == null) canvas = VisualTreeHelper.GetParent(this.AssociatedObject) as Canvas; // Dragging mode begins. isDragging = true; // Get the position of the click relative to the element // (so the top-left corner of the element is (0,0). mouseOffset = e.GetPosition(AssociatedObject); // Capture the mouse. This way you‘ll keep receiveing // the MouseMove event even if the user jerks the mouse // off the element. AssociatedObject.CaptureMouse(); } private void AssociatedObject_MouseMove(object sender, MouseEventArgs e) { if (isDragging) { // Get the position of the element relative to the Canvas. Point point = e.GetPosition(canvas); // Move the element. AssociatedObject.SetValue(Canvas.TopProperty, point.Y - mouseOffset.Y); AssociatedObject.SetValue(Canvas.LeftProperty, point.X - mouseOffset.X); } } private void AssociatedObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e) { if (isDragging) { AssociatedObject.ReleaseMouseCapture(); isDragging = false; } } } }
四、使用行为
为测试行为,创建一个新的WPF应用程序项目。然后添加对定义DragInCanvasBehavior类的类库以及System.Windows.Interactivity.dll程序集得应用。接下来在XML中映射这两个标记名称。假定存储DragInCanvasBehavior类的类库名为CustomBehaviorsLibrary,所需的标记如下所示:
<Window x:Class="BehaviorTest.DragInCanvasTest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:custom="clr-namespace:CustomBehaviorsLibrary;assembly=CustomBehaviorsLibrary" Title="DragInCanvasTest" Height="300" Width="300">
为使用该行为,只需要使用Interaction.Behaviors附加属性在Canvas面板中添加任意元素。下面的标记创建一个具有三个图形的Canvas面板。两个Ellipse元素使用了DragInCanvasBehavior,并能在Canvas面板中拖动。Rectangle元素没有使用DraginCanvasBehavior,因此无法移动。
<Canvas> <Rectangle Canvas.Left="10" Canvas.Top="10" Fill="Yellow" Width="40" Height="60"></Rectangle> <Ellipse Canvas.Left="10" Canvas.Top="70" Fill="Blue" Width="80" Height="60"> <i:Interaction.Behaviors> <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior> </i:Interaction.Behaviors> </Ellipse> <Ellipse Canvas.Left="80" Canvas.Top="70" Fill="OrangeRed" Width="40" Height="70"> <i:Interaction.Behaviors> <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior> </i:Interaction.Behaviors> </Ellipse> </Canvas>
下图显示了该例的运行效果。
DragInCanvasTest完整代码:
<Window x:Class="BehaviorTest.DragInCanvasTest" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity" xmlns:custom="clr-namespace:CustomBehaviorsLibrary;assembly=CustomBehaviorsLibrary" Title="DragInCanvasTest" Height="300" Width="300"> <Canvas> <Rectangle Canvas.Left="10" Canvas.Top="10" Fill="Yellow" Width="40" Height="60"></Rectangle> <Ellipse Canvas.Left="10" Canvas.Top="70" Fill="Blue" Width="80" Height="60"> <i:Interaction.Behaviors> <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior> </i:Interaction.Behaviors> </Ellipse> <Ellipse Canvas.Left="80" Canvas.Top="70" Fill="OrangeRed" Width="40" Height="70"> <i:Interaction.Behaviors> <custom:DragInCanvasBehavior></custom:DragInCanvasBehavior> </i:Interaction.Behaviors> </Ellipse> </Canvas> </Window>
但这并非是全部内容。如果正在使用Expression Blend进行开发,行为甚至提供了更好的设计体验——可以根本不用编写任何标记。