1. Banding基础
WPF中的数据绑定提供了很强大的功能。与普通的WinForm程序相比,其绑定功能为我们提供了很多便利,例如Binding对象的自动通知/刷新,Converter,Validation Rules,Two Way Binding等功能,省去了很多维护的繁琐工作。另外对于WPF中提供的数据模板功能,让我们可以轻松定制可以被复用的控制呈现的模块—但这是以数据绑定为前提来做到轻松易用的效果的。数据提供者例如XmlDataProvider和ObjectDataProvider更是简化了将对象以特定方式绑定并呈现的过程。可以说,数据绑定是WPF中让我们真正能够开始体现其便利性的特征之一,而对以数据驱动的应用来讲,其重要性不言而喻。
数据绑定的关键是System.Windows.Data.Binding对象,它会把两个对象(UI对象与UI对象之间,UI对象与.NET数据对象之间)按照指定的方式粘合在一起,并在他们之间建立一条通信通道,绑定一旦建立,接下来的应用生命周期中它可以自己独立完成所有的同步工作。根据其应用场合的不同我们将在本文中从以下几个部分分别讨论:
· 对象间的绑定
· 绑定到集合
· 数据模板
· 向绑定添加规则和转换器
1. UI对象间的绑定
UI对象间的绑定,也是最基本的形式,通常是将源对象Source的某个属性值绑定 (拷贝) 到目标对象Destination的某个属性上。源属性可以是任意类型,但目标属性必须是依赖属性(Dependency Property)。通常情况下我们对于UI对象间的绑定源属性和目标属性都是依赖属性 (有些属性不是) ,因为依赖属性有垂直的内嵌变更通知机制,WPF可以保持目标属性和源属性的同步。
看个简单的例子是如何在XAML中实现数据绑定的:
<Window x:Class="Allan.WpfBinding.Demo.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" Title="Basic Bindings" Height="400" Width="700" Style="{StaticResource windowStyle}"> <Grid> <Grid.RowDefinitions> <RowDefinition Height="40" /> <RowDefinition Height="*" /> <RowDefinition Height="40" /> </Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="5" HorizontalAlignment="Right"> <Button x:Name="btnBasicBinding" Content="Basic" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnCollectionBinding" Content="Collection" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnDataTemplate" Content="Data Template" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnAdvanceBindings" Content="Advance" Style="{StaticResource buttonStyle}"></Button> <Button x:Name="btnExit" Content="Exit" Style="{StaticResource buttonStyle}"></Button> </StackPanel>
<StackPanel Grid.Row="1" HorizontalAlignment="Left"> <TextBox x:Name="txtName" Margin="5" Width="400" BorderThickness="0" Height="50" Text="Source Element"></TextBox> <TextBlock x:Name="tbShowMessage" Margin="5" Width="400" Height="50" Text="{BindingElementName=txtName,Path=Text }" /> </StackPanel> </Grid> </Window> |
· XAML绑定语法:
上边的代码我们将名为txtName的对象的Text属性作为源对象分别绑定给了两个TextBlock的Text属性。这里我们用了Binding关键字并指定了ElementName和Path,这两个就是指定源对象(Source)和源属性(Source Property). 通常我们在设定绑定时都用与StaticResource标记类似的语法{Binding… }并设置ElementName和Path属性:
Text=”{Binding ElementName=SourceObjectName, Path=SourceProperty}” |
· 用Coding(C#)添加Binding
而对于C#里和绑定相关的代码,则看起来会罗嗦很多。但它们都同样的使用了Binding对象,然后指定PropertyPath的一个实例为源属性,然后可以有两个方法来加载绑定规则:
1. 调用FrameworkElement 或FrameworkContentElement对象的SetBinding方法
2. 调用BindingOperations.SetBinding静态方法
以下代码实现了和上边XAML文件类似的功能:
Binding binding = new Binding(); //设置源对象 binding.Source = txtName; //设置源属性 binding.Path = new PropertyPath("Text"); //添加到目标属性 this.tbShowMessage.SetBinding(TextBlock.TextProperty, binding); //or //BindingOperations.SetBinding(tbShowMessage, TextBlock.TextProperty, binding); |
· 用Coding(C#)移除Binding
当你在应用程序中某个地方添加了绑定,而在某个时候又不想这个绑定在接下来继续有效时,你可以有两种方式来断开这个绑定:
1. 用BindingOperations.ClearBinding静态方法。
例如BindingOperations.ClearBinding(currentTextBlock, TextBlock.TextProperty); BindingOperations同时还提供了ClearAllBindings方法,只需要传入要清除绑定的目标对象的名称,它就会将所有这个对象的绑定移除。
2. 简单的将目标属性设置为一个新的值。
这个简单的方法同样有效,可以断开与前边设置的binding的连接。简单的设置为任何值即可:如:currentTextBlock.Text = “it’s a new value.”;
· Binding对象的属性
Property |
Description |
Converter |
转换器 |
ElementName |
绑定的源对象 |
FallbackValue |
绑定无法返回有效值时的默认显示。 |
Mode |
绑定方式 |
Path |
属性 |
RelativeSource |
常用于自身绑定或者数据模板中来指定绑定的源对象。 |
Source |
源对象 |
StringFormat |
格式化表达式 |
UpdateSourceTrigger |
Sets the events on which binding will occur. |
ValidationRules |
验证规则 |
总结:对于对象间的绑定,绑定源为ElementName,Path为绑定源属性。ElementName必须为以下可选项之一:
DataContext |
DataContext是WPF最后才试图查找的源。一旦RelativeSource和Source对象都没有被设置,则会在逻辑树种向上搜寻。 |
RelativeSource |
用来标识和当前控件关联的对象,通常用于自我引用或数据模板。 |
Source |
数据提供者/对象 |
2. 绑定到集合
·利用ItemsSource来绑定数据源
常用标记:{Binding Path =””} ItemSource DisplayMemberPath
通常来说这是我们在做以数据驱动为主的应用时最经常用到的绑定方式。WPF支持任何类型的.NET对象作为数据源绑定到WPF对象。对于所有的ItemsControl对象都有一个ItemsSource依赖属性,这是专门为数据绑定而准备的。ItemsSource的类型是IEnumerable,所以对于我们几乎所有的集合类型我们都可以轻易的改变成ItemsSource的源对象。通过以下语句我们可以将一个名为photos的集合赋予ListBox对象,并以显示Name属性的值:
<ListBox x:Name=”pictureBox” DisplayMemberPath=”Name” ItemsSource=”(Binding {DynamicResource photos}” |
我们知道,依赖属性内建的垂直通知功能让UI对象间的绑定可以自己负责同步处理,但是对于.NET集合/对象来讲,它不具备这样的能力。为了让目标属性与源集合的更改保持同步,源集合必须实现一个叫INotifyCollectionChanged的接口,但通常我们只需要将集合类继承于ObservableCollection类即可。因为ObservableCollection实现了INotifyPropertyChanged和INotifyCollectionChanged接口。示例代码中我们这么去定义Photos集合类:
public class Photos : ObservableCollection<Photo> |
· 利用DataContext来作为共享数据源
常用标记:{Binding Path=””} DataContext
顾名思义,DataContext就是数据上下文对象,它是为了避免多个对象共享一个数据源时重复的对所有对象显式地用binding标记每个Source/RelativeSource/ElementName,而把同一个数据源在上下文对象的某个范围内共享,这样当一个绑定没有显式的源对象时,WPF会便利逻辑数找到一个非空的DataContext为止。
例如我们可以通过以下代码给ListBox和Title设置绑定:
<StackPanel Orentation=”Vertical” Margin=”5” DataContext=”{DynamicResource photos}”> <Label x:Name=”TitleLabel” Content=”{Binding Path=Count}” DockPanel.Dock=”Bottom” /> <ListBox x:Name=”pictureBox” DisplayMemeberPath=”Name” ItemSource=”{Binding}” /> </StackPanel> |
对于这些简单的绑定我们可以很灵活的组合他们的应用来达到我们的要求,这也是我们通常使用的方法。例如:
<Window.Resources> <local:Employee x:Key="MyEmployee" EmployeeNumber="123" FirstName="John" LastName="Doe" Department="Product Development" Title="QA Manager" /> </Window.Resources> <Grid DataContext="{StaticResource MyEmployee}"> <TextBox Text="{Binding Path=EmployeeNumber}"></TextBox> <TextBox Text="{Binding Path=FirstName}"></TextBox> <TextBox Text="{Binding Path=LastName}" /> <TextBox Text="{Binding Path=Title}"></TextBox> <TextBox Text="{Binding Path=Department}" /> </Grid> |
总结:对于集合的绑定,通常会需要用到以下几个标记:
DisplayMemberPath |
指定源对象中被显示的属性。ToString()方法会被默认调用。 |
ItemsSource |
指定要显示的数据源 |
ItemsTemplate |
指定以什么样的格式来显示数据(类似于符合控件,可以在数据模板中利用多种控件来控制展现方式) |
Path |
数据源对象中的属性—控制显示 |
DataContext |
共享数据源 |
3. 数据模板 – Data Template
当源属性和目标属性为兼容的数据类型,且源所显示的东西正是你需要显示的东西时,数据绑定确实很简单,你只需要向Section 1中讲的来匹配对象关系即可。而通常情况下我们对数据绑定都要做一些定制,特别对于.NET对象的绑定,你需要将数据源按照不同的方式分割显示。Data Template就负责来完成这样的功能:按照预想的数据展现模式将数据源的不同部分显示,而其作为可以被复用的独立结构,一旦定义可以被添加到一个对象内部,将会创建一个全新的可视树。
数据模板通常会被应用到以下几类控件来填充其类型为DataTemplate的属性:
· 内容控件(Content Control):ContentTemplate属性,控制Content的显示
· 项控件(Items Control) : ItemTemplate属性,应用于每个显示的项
· 头控件(Header Content Control) : HeaderTemplate属性,控制Header的展现。
每个数据模板的定义都是类似的方式,你可以像设计普通的窗体一样来设计其展现的方式,而且他们共享数据模板父空间所赋予的绑定源。例如下边的代码我们用一个图片来替代ListBox中的每一项:
<ListBox x:Name="pictureBox" ItemsSource="{Binding}"ScrollViewer.HorizontalScrollBarVisibility="Disabled"> <ListBox.ItemTemplate> <DataTemplate> <Image Source="{Binding Path=FullPath}" Margin="3,8" Height="35"> <Image.LayoutTransform> <StaticResource ResourceKey="st"/> </Image.LayoutTransform> <Image.ToolTip> <StackPanel> <TextBlock Text="{Binding Path=Name}"/> <TextBlock Text="{Binding Path=DateTime}"/> </StackPanel> </Image.ToolTip> </Image> </DataTemplate> </ListBox.ItemTemplate> </ListBox> |
最终的ListBox中每一项的展现将按照我们在数据模板中设定的样式以图片来显示:
通常数据模板是不需要被内联声明的,它可以被定义成一个资源存放在Application.Resources这样的全局资源辞典中,或者单独的Resource Dictionary中在多个元素间共享。
4. 向绑定添加规则和转换器
· 使用值转换器Value Converter
无论你的绑定XAML写得多么漂亮,所有的绑定值毫无疑问你都可以得到,但是它不总是可以满足你不经过任何程序变化显示出来就能满足要求的。例如对于本文示例代码的照片总数的显示,我们还想显示得更为智能一些:对于一些符合某种要求的数据我们将其背景显示为黄色,而对于有多于一条记录时我们显示15 Items,仅有一条时显示1 Item。这时Value Converter就派上用场了。
要定义一个Value Converter需要声明一个类让其继承于System.Windows.Data.IValueConverter接口,并实现其中的两个方法Convert和ConvertBack方法。
public class RawCountToDescriptionConverter : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { // Let Parse throw an exception if the input is bad int num = int.Parse(value.ToString()); return num + (num == 1 ? " item" : " items"); }
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { throw new NotSupportedException(); } } |
在XAML中声明资源,然后将其通过静态资源引用的方式赋予Binding对象的Converter属性。
<Window.Resources> <local:CountToBackgroundConverter x:Key="myConverter"/> <local:RawCountToDescriptionConverter x:Key="myConverter2"/> </Window.Resources> <TextBlock x:Name="filePath" DockPanel.Dock="Top" Style="{StaticResource titleStyle}" Text="{Binding Count, Converter={StaticResource myConverter2}}"></TextBlock> |
同样,我们可以对输入进行转换。如果数据的输入是被验证规则(如果有的话)标记为有效的,那么值转换器将会被调用,来对输入进行转换后反应出来。 (参考附件代码中的BindingConverter窗体)
· 向绑定添加规则
每个Binding对象都有一个ValidationRules属性,可以被设置为一个或多个派生自ValidationRule的对象,每个规则都会检查特定的条件并更具结果来标记数据的有效性。就像我们在ASP.NET中应用RequiredValidator, CustomValidator一样,你只需要定义自己的规则,WPF会在每次调用数据时(通常是TextBox等输入控件失去焦点)会调用验证检查。这些是在值转换器之前发生的,如果数据无效,它会标记此次更新无效,并将数据标记为无效—这是通过设置目标元素的Validation.HasError属性为true并触发Validation.Error事件(ValidationResult会被返回,并且其IsValid属性为false)。我们可以通过一个触发器来设定当数据无效时对用户的提示。例如下边的代码我们就通过定义一个JpgValidationRule,当数据无效时通过tooltip来提示用户输入无效。
public class JpgValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { string filename = value.ToString();
// Reject nonexistent files: if (!File.Exists(filename)) { return new ValidationResult(false, "Value is not a valid file."); }
// Reject files that don’t end in .jpg: if (!filename.EndsWith(".jpg", StringComparison.InvariantCultureIgnoreCase)) { return new ValidationResult(false, "Value is not a .jpg file."); } else { return new ValidationResult(true, null); } } } |
上边的代码定义了我们验证的规则。接下来在XAML中来应用这个规则。我们将这个规则用来检测输入框中的数据是否合法:
<TextBox Style="{StaticResource validateTextBoxStyle}"> <TextBox.Text> <Binding UpdateSourceTrigger="PropertyChanged" Path="Department"> <Binding.ValidationRules> <local:JpgValidationRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> |
当数据不合法时我们以什么样的方式来告诉用户呢?这里有两个方法可以做,一个是定义你自己的ErrorTemplate,另外一个是根据Trigger来设置一些可见信息。通常我们都可以来自己定义一些Error Provider和可以复用的ErrorTemplate,这个话题我们会在下一篇文章中讲。这里我们只让背景做改变并用tooltip来提示用户—显示的是ValidationRule返回的出错信息。因为都是控制显示的,所以定义成共用的Style:
<Style x:Key="validateTextBoxStyle" TargetType="{x:Type TextBox}"> <Setter Property="Width" Value="300" /> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="Red"/> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> |
总的来说,对于验证,我们常用一下几个属性来定义错误验证规则和错误展现方式:
· Errors – 错误信息集合
· HasError – 是否有错误出现.
· ErrorTemplate – 错误提示的展现方式.
· Binding.ValidationRules 绑定验证规则
Coming Next:
本文我们了解了有关Binding以及和绑定有关的附加验证规则,转换器等。附加验证规则我们将在下一篇中了解更多自定义Error Provider,Error Template等。附加的Demo里提供了所有本文中的实例。在下一篇中我们会了解以下几个问题:
· Validation Rules
· Triggers
2. 验证
在上一篇文章中我们讨论了有关WPF绑定的知识点,现在我们可以很容易的将业务数据作为源绑定到WPF控件并可以通过创建不同的Data Template后来以特定的样式展现。而作为这个最常用的功能我们可以通过Two Way的绑定模式与界面交互,而在这个时候我们就需要类似于ASP.NET中Validator一样的东西来限制或者说校验数据有效性。ValidationRule就是为这样的应用而生的。本文将详细讨论如何应用验证规则和自定义错误模板。
我们首先来看看WPF中有关数据验证规则相关的几个常用类:
· ValidationRule : 所有自定义验证规则的基类。提供了让用户定义验证规则的入口。
· ExceptionValidation :表示一个规则,该规则检查在绑定源属性更新过程中引发的异常。它是一个内置的规则,它检查在绑定源属性更新过程中引发的异常。
· ValidationResult : 数据验证结果的表现方式。ValidationRule对象的Validate方法执行完毕后通过ValidationResult来表示验证的结果。这里包含了错误信息—ErrorContent,数据是否有效—IsValid。ValidResult 为 ValidationResult 的有效实例。
· ValidationError :表示一个验证错误,该错误在 ValidationRule 报告验证错误时由绑定引擎创建。
对于WPF中绑定的验证和转换值我们需要注意:
1. 在将值从目标属性传输到源属性时,数据绑定引擎首先移除可能已添加到所绑定元素的 Validation.Errors 附加属性的任何 ValidationError。然后,数据绑定引擎检查是否为该 Binding 定义了自定义验证规则;如果定义了自定义验证规则,那么它将调用每个 ValidationRule 上的 Validate方法,直到其中一个规则失败或者全部规则都通过为止。如果某个自定义规则未通过,则绑定引擎会创建一个 ValidationError 对象,并将该对象添加到绑定元素的 Validation.Errors 集合。如果 Validation.Errors 不为空,则元素的 Validation.HasError 附加属性被设置为 true。此外,如果Binding 的 NotifyOnValidationError 属性设置为 true,则绑定引擎将引发该元素上的 Validation.Error 附加事件。
2. 如果所有规则都通过,则绑定引擎会调用转换器(如果存在的话)。
3. 如果转换器通过,则绑定引擎会调用源属性的 setter。
4. 如果绑定具有与其关联的 ExceptionValidationRule,并且在步骤 3 或 4 中引发异常,则绑定引擎将检查是否存在 UpdateSourceExceptionFilter。使用UpdateSourceExceptionFilter 回调可以提供用于处理异常的自定义处理程序。如果未对 Binding 指定 UpdateSourceExceptionFilter,则绑定引擎将对异常创建ValidationError 并将其添加到绑定元素的 Validation.Errors 集合中。
任何方向(目标到源或源到目标)的有效值传输操作都将清除 Validation.Errors 附加属性。
· 简单验证:使用ExceptionValidationRule
对于大多数验证来说我们都是在验证用户输入。ExceptionValidateRule作为WPF内置的简单验证器可以捕捉在绑定上发生的任何异常。我们可以用ExceptionValidateRule来作为一个笼统的错误收集器,来暴露出内部数据验证规则的异常信息。
模拟一个Employee信息更改的窗体。假设我们定义了一个Employee实体类,在实体类中我们对数据的有效性都做了简单的验证。
public string Title { get { return strTitle; } set { strTitle = value; if (String.IsNullOrEmpty(strTitle)) { throw new ApplicationException("Please input Title."); } } }
public DateTime Birthday { get { return objBirthday; } set { objBirthday =value; if (objBirthday.Year >= DateTime.Now.Year) { throw new ApplicationException("Please enter a valid date."); } } } |
在XAML中添加对字段的绑定,并对各个单独的控件的Text属性设置Bindg.ValidationRules. 这样当这个绑定的实体对象对应的属性出现验证错误时,错误信息会被ExceptionValidationRule抛出来。其默认行为是在当前控件LostFocus时触发验证,并将出错源控件加亮。
<TextBox Grid.Column="1" Grid.Row="2" Width="200" HorizontalAlignment="Left"> <TextBox.Text> <Binding Path="Title"> <Binding.ValidationRules> <ExceptionValidationRule /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> |
上边的验证是正确了,但是我们并不知道具体的错误信息。这对于一个String类型的字段来说可能是可以理解的,我可以试出来。但对于特殊的字段来说,我们根本无法得知到底发生了什么,我怎么填写数据才能正确。那么如何去控制显示呢?
· 自定义错误验证规则和定制显示
我们在上一篇里边提到过,所有的验证其实是有一个System.Windows.Controls.Validation类来完成的,对于这个类我们可以看到它有几个附加属性和一个附加事件Error.所谓附加事件/属性就是说可以被加载到另外的控件上来触发某个对象的事件或操作其属性,类似于我们有一个接口,通过这个接口你可以操作别的对象。在这里我们着重需要注意下边几个属性:
Gets the collection of all active ValidationError objects on the bound element. |
|
Gets a value that indicates whether any binding on the binding target element has a ValidationError. |
|
Gets or sets the ControlTemplate used to generate validation error feedback on the adorner layer. |
回想一下上一篇的最后部分,我们用到了触发器,用触发器来判断HasError属性,然后设置其错误模板。很容易我们可以做到这一点,这也是我们经常用到的办法。为了其复用性,我们可以将它定义为一个公用的Style。
<Style TargetType="{x:Type TextBox}">
<Style.Triggers> <Trigger Property="Validation.HasError" Value="true"> <Setter Property="Validation.ErrorTemplate"> <Setter.Value> <ControlTemplate> <DockPanel LastChildFill="True"> <TextBlock DockPanel.Dock="Right" Foreground="Red" FontSize="12pt" Text="{Binding ElementName=MyAdorner, Path=AdornedElement.(Validation.Errors)[0].ErrorContent}"> </TextBlock> <Border BorderBrush="Red" BorderThickness="1"> <AdornedElementPlaceholder Name="MyAdorner" /> </Border> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Trigger> </Style.Triggers> </Style> |
样式属于一个implicit key的样式,表明它针对的是所有的TargetType所指明的类型,这里是TextBox. 有个问题是,我们想在这里边的值改变时就需要触发验证而不是LostFocus,怎么改变呢?Binding对象有一个属性叫UpdateSourceTrigger,这是一个枚举值来指定什么时候触发数据源更新,更新的时候才会调用验证。
Members |
Description |
Default |
The default UpdateSourceTrigger value of the binding target property. The default value for most dependency properties is PropertyChanged, while the Text property has a default value of LostFocus. |
PropertyChanged |
Updates the binding source immediately whenever the binding target property changes. |
LostFocus |
Updates the binding source whenever the binding target element loses focus. |
Explicit |
Updates the binding source only when you call the UpdateSource method. |
<Binding Path="Title" UpdateSourceTrigger="PropertyChanged">就可以帮我们完成任务。
接下来我们可以定义一个对象化一些的错误处理类了。定义自己的规则来验证数据是ValidationRule所处理的主要任务,扩展这些规则就要使我们的类继承于这个父类:
public class EnumValidationRule : ValidationRule { private string _enumClass; private string _errorMessage;
public string EnumClass { get { return "Allan.WPFBinding.ValidationDemo." + _enumClass; } set { _enumClass = value; } }
public string ErrorMessage { get { return _errorMessage; } set { _errorMessage = value; } }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) { ValidationResult result = new ValidationResult(true, null); string inputString = (value ?? string.Empty).ToString();
Type type = Type.GetType(EnumClass);
if (string.IsNullOrEmpty(inputString) || !ValidateInput(type,inputString)) { result = new ValidationResult(false, this.ErrorMessage); } return result; } } |
更改绑定参数设置:将验证规则换成EnumValidationRule并设置相关参数。
<Binding Path="Title" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:EnumValidationRule EnumClass="TitleEnum" ErrorMessage="输入值不存在。请重新输入。" />
</Binding.ValidationRules>
</Binding>
· 用BindingGroups实现对List的验证
到目前为止我们所有看到的都是针对每个单个的控件输入的验证,那么有没有办法对比如某个ListView的每个item进行绑定验证呢?用上边的方法是不可以的,否则我们也没必要针对每个空间都去那么繁琐的设置各种信息啊。随同.NET Framework SP1发布了新的功能点,那就是BindingGroups。BindingGroups是对list列表中的绑定之间的关系进行了封装,使得你在获得一个上下文对象时可以和BIdingGroups关联来得到与当前上下文对象相关联的所有绑定。而这个关系是你需要显式去设定的—通过BindingGroupName。
FrameworkElement和FrameworkContentElement有一个叫做BindingGroup的依赖属性,而ItemsControl继承于FrameworkElement,所以其具有BindingGroup属性。而对于BindingBase对象添加了BindingGroupName属性。
public class BindingBase { public string BindingGroupName { get; set; } } |
我们刚才讲过,BindingGroup是需要显式的去声明,并且对于一个BindingGroup来说,拿到它就相当于拿到了当前对象相关上下文的所有与其有关系的绑定。而它依然本质上又回到了基本的ValidationRule。
在上边的类图中我们可以看到几个很有意思的东西:
· BindingExpressions: 保存了与当前上下文有关的绑定对象。我们给DateContext中绑定了四个字段的值(对应四个控件),BindingExpressions就保存了所有归属到当前BindingGroup的所有Binding对象。例如你有四个字段,那就有四个BidingExpression,有5个字段绑定,你这里就有5个BindingExpression,即便他们很可能都是一样的。
· Items :这个属性保存了与当前上下文对象有关的绑定。例如,我们在示例程序中只绑定了一个单独的Employee对象,那么BindingExpressions里就保留了这个上下文对象。换句话讲,当我们按照对象的方式来多个验证的时候(因为Item在ListView, DataGrid等控件里就代表一个单个对象),我们所操作的对象传递到验证规则时是以BindingExpression的target来表示的。这时,你得到了行的源,就可以做你想做的事情了。
· ValidationRules : 关联到我们给bindingGroup加载的验证规则。
· BeginEdit, CancelEdit, CommitEdit : 实际上这些是和IEditableCollectionView关联的一些事件,表明对数据视图的操作。这里我们可以简单认为对数据进行更新和提交。
· UpdateSources : 这是一个方法,表明我们要调用ValidationRule来验证数据。
· ValidateWithoutUpdate : 方法:验证数据而不更新数据。
现在我们来应用BindingGroup给基于一个Form的绑定来做验证吧。首先是定义BindingGroup.
<Grid.BindingGroup> <BindingGroup> <BindingGroup.ValidationRules> <local:EmployeeValidationRule ValidationStep="ConvertedProposedValue" /> </BindingGroup.ValidationRules> </BindingGroup> </Grid.BindingGroup> |
注意这里应用了ValidationStep,和前边的UpdateSourceTrigger很相似,它是用来表示在什么时候来应用验证规则的。是个枚举值:
· RawProposedValue = 0, 在没有变化的原数据上验证
· ConvertedProposedValue = 1,用户更改过数据后验证 default
· UpdatedValue = 2, 在将数据提交给Source时验证
· CommittedValue = 3 在将数据提交到数据库后验证
接下来定义我们的规则。现在的规则是针对每个对象而不是单个属性的,所以与前边的有些许不同,但很容易,因为你可以拿到绑定到当前项的对象。
public override ValidationResult Validate(object value, CultureInfo cultureInfo) { BindingGroup bindingGroup = (BindingGroup)value; Employee emp = (Employee)bindingGroup.Items[0];
object startDateObj = bindingGroup.GetValue(emp, "StartDate"); DateTime? startDate = (DateTime)startDateObj;
object endDateObj = bindingGroup.GetValue(emp, "EndDate"); DateTime? endDate = (DateTime)endDateObj;
// check start and end date together if (startDate.Value > endDate.Value) { return new ValidationResult(false, string.Format("StartDate: {0}, cannot be greater than EndDate: {1}",startDate,endDate)); } else { return new ValidationResult(true, null); } } |
Ok了,你可以再去定义一下出错时的ErrorTempalte,然后去享受这样的成果了:)
3. 使用触发器
WPF提供了很重要的一个东西就是绑定Binding, 它帮助我们做了很多事情,这个我们在WPF学习之绑定这篇里边有讲过。对于Binding我们可以设置其绑定对象,关系,并通过某种规则去验证输入,或者转换值等等,这一切的背后是省去了很多我们需要自己去处理的代码。而对于WPF最主要表现的东西—渲染UI,当然是我们必须去了解和把握的了。美工设计了很多效果,并把其设计成样式展现(很大程度上我们应该认为Style也是一种资源),而作为程序员的我们不应该只是简单的拿来这些拼凑的效果,根据程序的逻辑和用户的操作来动态的展现效果才是我们能发挥它对界面渲染的更好途径。Trigger就给我们提供了很好的途径去结合这些元素。
触发器,从某种意义上来说它也是一种Style,因为它包含有一个Setter集合,并根据一个或多个条件执行Setter中的属性改变。因为复用的缘故,Styles是放置触发器的最好位置。但对于每个FrameworkElement来说都有Triggers集合,你也可以放在Triggers集合里。触发器有三种类型:
· 属性触发器Property Trigger:当Dependency Property的值发生改变时触发。
· 数据触发器Data Trigger: 当普通.NET属性的值发生改变时触发。
· 事件触发器Event Trigger: 当路由时间被触发时调用。
1. 属性触发器(Property Trigger)
属性触发器是WPF中最常用的触发器类型,因为我们前边说过依赖属性具有垂直变更通知的功能,所以在使用属性触发器时会很方便,而且因为WPF中每个控件超过2/3的属性都是依赖属性,所以它用到的场合更多。属性触发器是在当某个依赖属性的值发生变化时触发执行一个Setter的集合,当属性失去这个值时,这些被处罚执行的Setter集合会自动被撤销。
例如,下边的例子设置了当鼠标放置于按钮之上悬停时,按钮的外表会发生变化。注意,属性触发器是用Trigger标识的。
<Style x:Key="buttonMouseOver" TargetType="{x:Type Button}"> <Style.Triggers> <Trigger Property="IsMouseOver" Value="True"> <Setter Property="RenderTransform"> <Setter.Value> <RotateTransform Angle="10"></RotateTransform> </Setter.Value> </Setter> <Setter Property="RenderTransformOrigin" Value="0.5,0.5"></Setter> <Setter Property="Background" Value="#FF0CC030" /> </Trigger> </Style.Triggers> </Style> |
属性触发器还经常被用在做数据验证时用来显示验证错误信息。在WPF学习之绑定里的Validation部分我们附有用属性触发器来判断是否有验证错误并显示相应验证错误信息的示例。
<TextBox Style="{StaticResource validateTextBoxStyle}"> <TextBox.Text> <Binding UpdateSourceTrigger="PropertyChanged" Path="Department"> <Binding.ValidationRules> <local:JpgValidationRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> …..
<Style x:Key="validateTextBoxStyle" TargetType="{x:Type TextBox}"> <Setter Property="Width" Value="300" /> <Style.Triggers> <Trigger Property="Validation.HasError" Value="True"> <Setter Property="Background" Value="Red"/> <Setter Property="ToolTip" Value="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.Errors)[0].ErrorContent}"/> </Trigger> </Style.Triggers> </Style> |
2. 数据触发器Data Trigger
数据触发器和属性触发器除了面对的对象类型不一样外完全相同。数据触发器是来检测非依赖属性------也就是用户自定义的.NET属性-----的值发生变化时来触发并调用符合条件的一系列Setter集合。
下边的示例演示了在绑定的ListBox里如果某个User对象符合某种特点(Role=Admin),则以突出方式显示这个对象。这里就用了DataTrigger,因为我们需要检测的是User对象的属性Role,这个对象是自定义的非可视化对象并且其属性为普通.NET属性。
<Page.Resources> <clr:Users x:Key="myUsers" /> <DataTemplate DataType="{x:Type clr:User}"> <TextBlock Text="{Binding Path=Name}"/> </DataTemplate> ... </Page.Resources> <StackPanel> <ListBox Width="200" ItemsSource="{Binding Source={StaticResource myUsers}}" /> </StackPanel> |
主要的部分定义在了Style中,其针对的是每个ListBox的项,当其被绑定的数据的属性Role为Admin时,突出显示:
<Style TargetType="{x:Type ListBoxItem}"> <Style.Triggers> <DataTrigger Binding="{Binding Path=Role}" Value="Admin"> <Setter Property="Foreground" Value="Red" /> </DataTrigger> </Style.Triggers> </Style> |
3. 事件触发器Event Trigger
事件触发器,顾名思义是在某个事件被触发时来调用这个触发器的相关操作。因为WPF提供了用XAML来标记对象,事件等,所以其提供了一些在普通.NET开发中看似没用的属性例如IsMouseOver, IsPressed等,这是为了XAML来用的,使其可以很方便的通过某个属性来判断状态,也方便了Property Trigger的应用。而作为事件触发器来说,它所做的事情和Property Trigger类似,不过是它的内部不能是简单的Setter集合,而必须是TriggerAction的实例。
以下示例演示了如何应用Event Trigger当鼠标点击按钮时,让按钮的阴影效果发生变化。
<Button Margin="15" Width="200" Name="myButton"> Click Me to Animate Drop Shadow! <Button.BitmapEffect>
<!-- This BitmapEffect is targeted by the animation. --> <DropShadowBitmapEffect x:Name="myDropShadowBitmapEffect" Color="Black" ShadowDepth="0" /> </Button.BitmapEffect> <Button.Triggers> <EventTrigger RoutedEvent="Button.Click"> <BeginStoryboard> <Storyboard>
<!-- Animate the movement of the button. --> <ThicknessAnimation Storyboard.TargetProperty="Margin" Duration="0:0:0.5" From="50,50,50,50" To="0,0,50,50" AutoReverse="True" />
<!-- Animate shadow depth of the effect. --> <DoubleAnimation Storyboard.TargetName="myDropShadowBitmapEffect" Storyboard.TargetProperty="ShadowDepth" From="0" To="30" Duration="0:0:0.5" AutoReverse="True" />
<!-- Animate shadow softness of the effect. As the Button appears to get farther from the shadow, the shadow gets softer. --> <DoubleAnimation Storyboard.TargetName="myDropShadowBitmapEffect" Storyboard.TargetProperty="Softness" From="0" To="1" Duration="0:0:0.5" AutoReverse="True" /> </Storyboard> </BeginStoryboard> </EventTrigger> </Button.Triggers> </Button> |
4. MultiDataTrigger & MultiTrigger
截至目前我们讨论的都是针对单个条件的触发器,也就是说当某一个条件满足时就会触发。而现实中我们可能需要满足很多个条件时才触发一系列操作,这个时候就需要用到MultiDataTrigger或MultiTrigger。MutliDataTrigger和MultiTrigger都具有一个Conditions集合用来存放一些触发条件,这里的Condition之间是and的关系,当所有条件都满足时,Setter集合才会被调用。根据名字就可以看清楚:MultiDataTrigger用来实现多个数据触发器(只用于普通.NET属性)满足条件时调用;MultiTrigger用来实现多个属性触发器(用于依赖属性)满足条件时调用。
以下示例仅当按钮的IsEenabled属性为true,并且可见时(Visibility=Visible)会以醒目的方式显示,否则当IsEnabled属性为false时将以灰色显示。
<Style TargetType="{x:Type Button}" x:Key="highlightStyle"> <Style.Triggers> <Trigger Property="IsEnabled" Value="false"> <Setter Property="Background" Value="#EEEEEE" /> </Trigger>
<MultiTrigger> <MultiTrigger.Conditions> <Condition Property="Visibility " Value="Visible" /> <Condition Property="IsEnabled" Value="true" /> </MultiTrigger.Conditions> <Setter Property="BorderBrush" Value="Red"/> <Setter Property="FontSize" Value="14" /> <Setter Property="FontWeight" Value="Bold" /> <Setter Property="Foreground" Value="Red" /> </MultiTrigger> </Style.Triggers> </Style> … <Button Style="{StaticResource highlightStyle}" Content="Hight Value" x:Name="btnVisible" Click="Button_Click" /> |
给按钮添加单击事件用来改变IsEnabled属性:
private void Button_Click(object sender, RoutedEventArgs e) { this.btnVisible.IsEnabled = !this.btnVisible.IsEnabled; } |
看看效果(左边为不单击后不满足条件时的样式):
同样的,你也可以用MultiDataTrigger来对自定义的属性进行多条件的与关系操作。
5. 在触发器中执行用户代码
DependencyProperty.RegisterAttached方法允许用户给控件/窗体等定义自己的依赖属性,其包含的CallBack参数可以允许执行某个特定方法。这允许我们在Trigger中去调用特定的事件处理。其实严格的说这和Trigger不太有关系,因为这相当于我们给某个对象添加了自定义属性并执行某些事件。但trigger可以恰恰利用这个好处来简介的执行业务逻辑:
public static readonly DependencyProperty SomethingHappenedProperty = DependencyProperty.RegisterAttached("SomethingHappened", typeof(bool), typeof(Window1), new PropertyMetadata(false, new PropertyChangedCallback(SomethingHappened))); public bool GetSomethingHappened(DependencyObject d) { return (bool)d.GetValue(SomethingHappenedProperty); } public void SetSomethingHappened(DependencyObject d, bool value) { d.SetValue(SomethingHappenedProperty, value); } public static void SomethingHappened(DependencyObject d, DependencyPropertyChangedEventArgs e) { //do something here } |