行为是一款重用用户界面代码的更有挑战性的工具。其基本思想是:使用行为封装一些通用UI功能。如果具有适当的行为,可使用一两行XAML标记将其附加到任意元素,从而节省编写和调试代码的工作。
样式提供了重用一组属性设置的实用方法。它们帮助构建一致的、组织良好的界面迈出了重要的第一步------但它们还有许多限制。
问题是在典型的应用程序中,属性设置仅是用户界面基础结构的一小部分。甚至最基本的程序通常也需要大量的用户界面代码,这些代码与应用程序的功能无关。在许多程序中,用于用户界面任务的代码(如驱动动画、实现平滑效果、维护用户界面状态,以及支持诸如拖放、缩放以及停靠等用户急么特性),无论是在数量上还是复杂性上都超出了业务代码。许多这类代码是通用的,这意味着在创建每个WPF对象中需要编写相同的内容。所有这些工作几乎都是单调乏味的。
为回应这一挑战,ExpressionBlend创作者开发了称为行为(Behavior)的特征。其思想很简单:创建封装了一些通用用户界面功能的行为,这一功能可以是基本功能(如启动故事板或导航到超链接),也可以是复杂功能(如处理多点触摸交互,或构件使用实时物理引擎的碰撞模型)。一旦构建功能,就可将他们添加到任意应用程序的另一个控件中,具体方法是将该控件链接到适当的行为并设置行为的属性。
自定义控件时另一个在应用程序中(或在多个应用程序之间)重用用户界面功能的技术。然而,自定义控件必须作为可视化内容和代码的紧密链接包进行创建。尽管自定义控件非常强大,但却不能适应于需要大量具有类似功能的不同控件的情况(例如,为一组不同的元素添加鼠标悬停渲染效果)。因此,样式、行为以及自定义控件都是互补的。
获取行为支持
有一个问题,重用用户界面代码通用块的基础结构不是WPF的一部分。反而,它被捆绑到ExpressionBlend。这是因为行为开始是作为ExpressionBlend的设计时特性引入的。事实上,ExpressionBlend仍是通过将行为拖动到需要行为的控件上来添加行为的唯一工具。但这并不意味着行为只能用于ExpressionBlend,只需要付出很少的努力就可以在VisualStudio应用程序中创建和使用行为。只需要手动编写标记,而不是使用工具箱。
为了获得支持行为的程序集,有两种选择:
1、可安装Expression Blend或Expression Blend For Visual Studio ,所有这些版本都包含Visual Studio中的行为功能所需的程序集,但您只能通关Expression Blend For Visual Studio 在Blend环境中创建和编辑WPF应用程序。
2、可安装 Expression Blend SDK
无论是使用Expression Blend IDE还是SDK,最终要使用的两个程序集是:
System.Windows.Interactivity.dll 这个程序集定义了支持行为的基本类。它是行为特征的基础
Microsoft.Expression.Interactions.dll 这个程序集通过添加可选的以核心行为类为基础的动作和触发器类,增加了一些有用的扩展。
理解行为模型
行为特性具有两个版本,一个版本旨在为Silverlight 添加行为支持,Silverlight 是Microsoft 的针对浏览器的富客户端插件;而另一个版本是针对WPF设计的。尽管这两个版本提供了相同的特性,但行为特性和Silverlight 领域更吻合,因为它弥补了更大的鸿沟。与WPF不同,Silverlight 不支持触发器,所以实现行为的程序集也实现触发器更合理。然而,WPF支持触发器,行为特性包含自己的触发器系统,而触发器系统与WPF模型不匹配,这确实令人感到有些困惑。
问题在于具有类似名称的这两个特性有部分重合但不完全相同。在WPF中,触发器最重要的角色是构建灵活的样式和控件模板。在触发器的帮助下,样式和模板变得更加智能;例如,当一些属性发生变化时可视化效果。然而,ExpressionBlend中的触发器具有不同的目的。通过使用可视化设计工具,允许您为应用程序添加简单功能。换句话说,WPF触发器支持更强大的样式和控件模板。而ExpressionBlend触发器支持快速的不需要代码的应用程序设计。
那么,对于使用WPF的普通开发人员来说所有这些意味着什么呢?下面是几条指导原则:
1、行为模型不是WPF的核心部分,所以行为不像样式和模板那样确定。换句话说,可编写不使用行为的WPF应用程序,但如果不是样式和模板,就不能创建比"Hello World" 演示更复杂的WPF应用程序。
2、如果在ExpressionBlend上耗费大量时间,或希望为其他ExpressionBlend用户开发组件,您可能会对ExpressionBlend中的触发器特性感兴趣。尽管和WPF中的触发器系统使用相同的名称,但它们不相互重叠,您可以同时使用这两者。
3、如果不使用ExpressionBlend,可完全略过其触发器特性------但仍应分析ExpressionBlend提供的功能完整的行为类。这是因为行为比ExpressionBlend的触发器更强大也更常用。
创建行为
行为旨在封装一些UI功能,从而可以不必编写代码就能够将其应用到元素上。从另一个角度看,每个行为都为元素提供了一个服务。该服务通常涉及监听几个不同的事件并执行几个相关的操作。例如,为文本框提供水印,如果文本框为空,并且当前没有焦点,那么会以清淡的字体显示提示信息。当文本框具有焦点时,启动行为并删除水印文本。
为更好的理解行为,最好自己创建一个行为。这里实现一个为任意元素提供使用鼠标在Canvas面板上拖动元素的功能。对于单个元素实现该功能的基本步骤是非常简单的------代码监听鼠标事件并修改设置相应的Canvas坐标的附加属性。但通过付出更多一点的努力,可将该代码转换为可重用的行为,该行为可为Canvas面板上的所有元素提供拖动支持。
在任何行为中,第一步是覆盖OnAttached() 和 OnDetaching() 方法。当调用OnAttached() 方法时,可通过 AssociatedObject 属性访问放置行为的元素,并可关联事件处理程序。当调用 OnDetaching() 方法时,移除事件处理程序。
cs
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;
}
}
}
DragInCanvasBehavior 类在OnAttached()方法中为 MouseLeftButtonDown、MouseMove、MouseLeftButtonUp 事件添加事件处理程序,在OnDetaching()方法中移除这些事件处理程序。这里通过AssociatedObject 获得此行为附加到的对象,并通过可视化树查找附加对象的父元素来获得Canvas对象。
使用行为
为使用该行为,只需要使用 Interaction.Behaviors 附加属性在Canvas面板中添加任意元素。这里添加了三个形状(其中的一个Ellipse没有添加行为)、一个TextBlock、一个Button。测试下来发现除了没有添加行为的Ellipse无法拖动外,Button也无法拖动。这是因为Button中的MouseLeftButtonDown和MouseLeftButtonUp事件无法触发,Button本身响应Click事件,相当于将MouseLeftButtonDown和MouseLeftButtonUp事件抑制了,转换成了Click事件。
cs
<Canvas Height="200">
<Rectangle Canvas.Left="10" Canvas.Top="10" Fill="Yellow" Width="40" Height="60">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</Rectangle>
<Ellipse Canvas.Left="10" Canvas.Top="70" Fill="Blue" Width="80" Height="60"></Ellipse>
<Ellipse Canvas.Left="80" Canvas.Top="70" Fill="OrangeRed" Width="40" Height="70">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</Ellipse>
<TextBlock Text="TestBlock" Canvas.Left="280" Canvas.Top="170" Width="100" Height="30">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</TextBlock>
<Button Content="TestButton" Canvas.Left="180" Canvas.Top="170" Width="100" Height="30">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</Button>
</Canvas>
使用行为触发器
行为触发器通常是继承自 System.Windows.Interactivity.TriggerAction类 或是 System.Windows.Interactivity.TargetedTriggerAction类。主要实现Invoke函数用来响应触发器事件。
cs
public class FadeOutAction : TargetedTriggerAction<UIElement>
{
// The default fade out time is 2 seconds.
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeOutAction), new PropertyMetadata(TimeSpan.FromSeconds(2)));
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(FadeOutAction.DurationProperty); }
set { SetValue(FadeOutAction.DurationProperty, value); }
}
private Storyboard fadeStoryboard = new Storyboard();
private DoubleAnimation fadeAnimation = new DoubleAnimation();
public FadeOutAction()
{
fadeStoryboard.Children.Add(fadeAnimation);
}
protected override void Invoke(object args)
{
// Make sure the storyboard isn't already running.
fadeStoryboard.Stop();
// Set up the storyboard.
Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
Storyboard.SetTarget(fadeAnimation, this.Target);
// Set up the animation.
// It's important to do this at the last possible instant,
// in case the value for the Duration property changes.
fadeAnimation.To = 0;
fadeAnimation.Duration = Duration;
fadeStoryboard.Begin();
}
}
public class FadeInAction : TargetedTriggerAction<UIElement>
{
// The default fade in is 0.5 seconds.
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeInAction), new PropertyMetadata(TimeSpan.FromSeconds(0.5)));
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(FadeInAction.DurationProperty); }
set { SetValue(FadeInAction.DurationProperty, value); }
}
private Storyboard fadeStoryboard = new Storyboard();
private DoubleAnimation fadeAnimation = new DoubleAnimation();
public FadeInAction()
{
fadeStoryboard.Children.Add(fadeAnimation);
}
protected override void Invoke(object args)
{
// Make sure the storyboard isn't already running.
fadeStoryboard.Stop();
// Set up the storyboard.
Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
Storyboard.SetTarget(fadeAnimation, this.Target);
// Set up the animation.
fadeAnimation.To = 1;
fadeAnimation.Duration = Duration;
fadeStoryboard.Begin();
}
}
使用行为触发器则需要使用 Interaction.Triggers 附加属性,在其中添加触发器:
cs
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="3,15">
<Button Content="Click to Fade the Label" Padding="5">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:FadeOutAction TargetName="border" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Content="Click to Show the Label" Padding="5">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:FadeInAction TargetName="border" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
<Border x:Name="border" Background="Orange" BorderBrush="Black" BorderThickness="1" Margin="3,0" Opacity="0.5">
<TextBlock Margin="5" FontSize="17" TextWrapping="Wrap" Text="I'm the target of the FadeOutAction and FadeInAction."></TextBlock>
</Border>
</StackPanel>
完整的测试代码如下:
MainWindow.xaml
cs
<Window x:Class="TestBehavior.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestBehavior"
xmlns:i="clr-namespace:System.Windows.Interactivity;assembly=System.Windows.Interactivity"
mc:Ignorable="d"
Title="MainWindow" Height="500" Width="800">
<StackPanel>
<Canvas Height="200">
<Rectangle Canvas.Left="10" Canvas.Top="10" Fill="Yellow" Width="40" Height="60">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</Rectangle>
<Ellipse Canvas.Left="10" Canvas.Top="70" Fill="Blue" Width="80" Height="60"></Ellipse>
<Ellipse Canvas.Left="80" Canvas.Top="70" Fill="OrangeRed" Width="40" Height="70">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</Ellipse>
<TextBlock Text="TestBlock" Canvas.Left="280" Canvas.Top="170" Width="100" Height="30">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</TextBlock>
<Button Content="TestButton" Canvas.Left="180" Canvas.Top="170" Width="100" Height="30">
<i:Interaction.Behaviors>
<local:DragInCanvasBehavior></local:DragInCanvasBehavior>
</i:Interaction.Behaviors>
</Button>
</Canvas>
<StackPanel>
<StackPanel Orientation="Horizontal" Margin="3,15">
<Button Content="Click to Fade the Label" Padding="5">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:FadeOutAction TargetName="border" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
<Button Content="Click to Show the Label" Padding="5">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:FadeInAction TargetName="border" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
<Border x:Name="border" Background="Orange" BorderBrush="Black" BorderThickness="1" Margin="3,0" Opacity="0.5">
<TextBlock Margin="5" FontSize="17" TextWrapping="Wrap" Text="I'm the target of the FadeOutAction and FadeInAction."></TextBlock>
</Border>
</StackPanel>
<StackPanel>
<Button Content="Click to Play Sound" HorizontalAlignment="Left" Padding="5" Margin="3">
<i:Interaction.Triggers>
<i:EventTrigger EventName="Click">
<local:PlaySoundAction Source="test.mp3" />
</i:EventTrigger>
</i:Interaction.Triggers>
</Button>
</StackPanel>
</StackPanel>
</Window>
MainWindow.xaml.cs
cs
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.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Interactivity;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace TestBehavior;
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;
}
}
}
public class FadeOutAction : TargetedTriggerAction<UIElement>
{
// The default fade out time is 2 seconds.
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeOutAction), new PropertyMetadata(TimeSpan.FromSeconds(2)));
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(FadeOutAction.DurationProperty); }
set { SetValue(FadeOutAction.DurationProperty, value); }
}
private Storyboard fadeStoryboard = new Storyboard();
private DoubleAnimation fadeAnimation = new DoubleAnimation();
public FadeOutAction()
{
fadeStoryboard.Children.Add(fadeAnimation);
}
protected override void Invoke(object args)
{
// Make sure the storyboard isn't already running.
fadeStoryboard.Stop();
// Set up the storyboard.
Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
Storyboard.SetTarget(fadeAnimation, this.Target);
// Set up the animation.
// It's important to do this at the last possible instant,
// in case the value for the Duration property changes.
fadeAnimation.To = 0;
fadeAnimation.Duration = Duration;
fadeStoryboard.Begin();
}
}
public class FadeInAction : TargetedTriggerAction<UIElement>
{
// The default fade in is 0.5 seconds.
public static readonly DependencyProperty DurationProperty =
DependencyProperty.Register("Duration", typeof(TimeSpan), typeof(FadeInAction), new PropertyMetadata(TimeSpan.FromSeconds(0.5)));
public TimeSpan Duration
{
get { return (TimeSpan)GetValue(FadeInAction.DurationProperty); }
set { SetValue(FadeInAction.DurationProperty, value); }
}
private Storyboard fadeStoryboard = new Storyboard();
private DoubleAnimation fadeAnimation = new DoubleAnimation();
public FadeInAction()
{
fadeStoryboard.Children.Add(fadeAnimation);
}
protected override void Invoke(object args)
{
// Make sure the storyboard isn't already running.
fadeStoryboard.Stop();
// Set up the storyboard.
Storyboard.SetTargetProperty(fadeAnimation, new PropertyPath("Opacity"));
Storyboard.SetTarget(fadeAnimation, this.Target);
// Set up the animation.
fadeAnimation.To = 1;
fadeAnimation.Duration = Duration;
fadeStoryboard.Begin();
}
}
[DefaultTrigger(typeof(ButtonBase), typeof(System.Windows.Interactivity.EventTrigger), new object[] { "Click" })]
[DefaultTrigger(typeof(Shape), typeof(System.Windows.Interactivity.EventTrigger), new object[] { "MouseEnter" })]
[DefaultTrigger(typeof(UIElement), typeof(System.Windows.Interactivity.EventTrigger), new object[] { "MouseLeftButtonDown" })]
public class PlaySoundAction : TriggerAction<FrameworkElement>
{
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register("Source", typeof(Uri), typeof(PlaySoundAction), new PropertyMetadata(null));
public Uri Source
{
get { return (Uri)GetValue(PlaySoundAction.SourceProperty); }
set { SetValue(PlaySoundAction.SourceProperty, value); }
}
protected override void Invoke(object args)
{
// Find a place to insert the MediaElement.
Panel? container = FindContainer();
if (container != null)
{
// Create and configure the MediaElement.
MediaElement media = new MediaElement();
media.Source = this.Source;
// Hook up handlers that will clean up when playback finishes.
media.MediaEnded += delegate
{
container.Children.Remove(media);
};
media.MediaFailed += delegate
{
container.Children.Remove(media);
};
// Add the MediaElement and begin playback.
container.Children.Add(media);
}
}
private Panel? FindContainer()
{
FrameworkElement? element = this.AssociatedObject;
// Search for some sort of panel where the MediaElement can be inserted.
while (element != null)
{
if (element is Panel)
return (Panel)element;
element = VisualTreeHelper.GetParent(element) as FrameworkElement;
}
return null;
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}