前文介绍了自定义或系统自带的ValidationRule进行验证,这种方法对于单个元素的验证不错。很多时候,我们需要对表单(Form)进行验证,也就是对一个实体类进行验证,或者对一个集合的每项进行验证,则显得不尽人意(每次只能验证一次)。WPF3.5中提供了BindingGroup用来验证多个绑定元素,可以对表单(form)和实体类进行验证。另外BindingGroup提供了Transcational的支持,就是说可以让操作回滚(BeginEdit,CancelEdit,CommitEdit)。BindingGroup的验证是同时进行的。可以设置BindingGroupName把一个Binding加入已存在的BindingGroup(就是BindingGroupName指定的)。
MSDN上是这样说的:
BindingGroup 在多个绑定之间创建关系,从而可一起验证和更新这些绑定。例如,假定某应用程序提示用户输入地址。然后该应用程序使用用户提供的值填充 Address 类型的对象,该对象具有 Street、City、ZipCode 和 Country 属性。该应用程序有一个包含四个 TextBox 控件的面板,其中每个控件均数据绑定到对象的属性之一。可以使用 BindingGroup 中的 ValidationRule 验证 Address 对象。如果绑定加入相同的 BindingGroup,则可以确保邮政编码对于地址所在国家/地区有效。
设置 FrameworkElement 或 FrameworkContentElement 上的 BindingGroup 属性。正如任何其他可继承属性一样,子元素从其父元素继承 BindingGroup。如果发生以下情况之一,则会将子代元素上的绑定添加到 BindingGroup:
绑定的源和具有 BindingGroup 的元素的 DataContext 是同一对象,并且未设置 BindingGroupName 属性。
绑定的 BindingGroupName 属性等于 BindingGroup 的 Name,并且它们没有显式设置为 null引用(在 Visual Basic 中为 Nothing)。
在地址示例中,假定将 Panel 的 DataContext 设置为 Address 类型的对象。每个 TextBox 的绑定均添加到面板的 BindingGroup 中。
将 ValidationRule 对象添加到 BindingGroup 中。在运行 ValidationRule 时,将 BindingGroup 作为 Validate 方法的第一个参数传递。可以使用该 BindingGroup 上的 TryGetValue 或 GetValue(Object, String) 方法获取对象的建议值,使用 Items 属性获取绑定的源。
BindingGroup 在同一时间更新绑定的源,而不是分别更新每个绑定。在调用任一方法(ValidateWithoutUpdate、UpdateSources 或 CommitEdit)验证数据时,将验证并可能会更新示例中的每个 TextBox 的绑定。当绑定是 BindingGroup 的一部分时,除非显式设置 UpdateSourceTrigger 属性,否则在对 BindingGroup 调用 UpdateSources 或 CommitEdit 之前,不会更新绑定的源。
BindGroup常用成员:
public class BindingGroup : DependencyObject { public Collection<BindingExpressionBase> BindingExpressions { get; } public bool CanRestoreValues { get; } public IList Items { get; } public string Name { get; set; } public bool NotifyOnValidationError { get; set; } public Collection<ValidationRule> ValidationRules { get; } public void BeginEdit(); public void CancelEdit(); public bool CommitEdit(); public object GetValue(object item, string propertyName); public bool TryGetValue(object item, string propertyName, out object value); public bool UpdateSources(); public bool ValidateWithoutUpdate(); }
Items:BindingGroup 中的绑定对象所使用的源,是个List。所有作为源的对象都会被包含在Items中。通常,Items 中只有一项,即作为使用 BindingGroup 的元素的 DataContext 的对象。
但是,BindingGroup 也可以包含多个源。例如,如果绑定对象共享同一 BindingGroupName 但使用不同的源对象,则用作源的每个对象均在 Items 中。
如果绑定路径可解析为源的嵌套属性,则 Items 中也可有多个对象。例如,假定 TextBox 控件的绑定是 BindingGroup 的一部分,并且其 DataContext 是 Customer 对象,该对象具有 Address 类型的属性。
如果 Binding 的 Path 为 Address.ZipCode 属性,则 Address 会添加到 Items 属性中。
NotifyOnValidationError:获取或设置在 ValidationRule 的状态更改时是否发生 Validation.Error 事件。
BeginEdit:开始编辑事务。
CommitEdit:运行所有的Rule,如果成功,则保存更改,更新源。
CancelEdit:取消更改。
以上三个,如果源对应的类继承自IEditableObject, 会调用IEditableObject中的相应方法。
UpdateSources:运行所有ValidationStep设置为RawProposedValue、ConvertedProposedValue 或 UpdatedValue的Rule。如果成功,更新源。此方法不会挂起事务并结束事务,也就是说调用完该方法后事务还是处于运行中。
ValidateWithoutUpdate:如同UpdateSources,但是不会更新源。
所以有三个方法可以用作验证:CommitEdit,UpdateSources,ValidateWithoutUpdate。
<Window x:Class="ValidateItemSample.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:src="clr-namespace:ValidateItemSample" Title="Validating an Object" Width="400" Height="500" ResizeMode="NoResize"> <StackPanel Name="stackPanel1" Margin="10" Loaded="stackPanel1_Loaded" Validation.Error="ItemError"><!--验证的错误在ItemError中处理,要求NotifyOnValidationError="True"--> <StackPanel.Resources> <Style TargetType="HeaderedContentControl"> <Setter Property="Margin" Value="2"/> <Setter Property="Focusable" Value="False"/> <Setter Property="Template"> <Setter.Value> <ControlTemplate TargetType="HeaderedContentControl"> <DockPanel LastChildFill="False"> <ContentPresenter ContentSource="Header" DockPanel.Dock="Left" Focusable="False" VerticalAlignment="Center"/> <ContentPresenter ContentSource="Content" Margin="5,0,0,0" DockPanel.Dock="Right" VerticalAlignment="Center"/> </DockPanel> </ControlTemplate> </Setter.Value> </Setter> </Style> <Style TargetType="Button"> <Setter Property="Width" Value="100"/> <Setter Property="Margin" Value="10,15,15,15"/> </Style> </StackPanel.Resources>
<!--BindingGroup--> <StackPanel.BindingGroup> <BindingGroup NotifyOnValidationError="True"> <BindingGroup.ValidationRules> <src:ValidateDateAndPrice ValidationStep="ConvertedProposedValue" /> </BindingGroup.ValidationRules> </BindingGroup> </StackPanel.BindingGroup> <TextBlock FontSize="12" TextWrapping="Wrap" Margin="5"> This sample demonstrates how to validate an object by checking multiple properties in a ValidationRule. When a ValidationRule is added to a BindingGroup, the rule can get the properties of the source item in the Validate method. <LineBreak/><LineBreak/> This sample checks that if an item costs more than 100 dollars, the item is available for at least 7 days. </TextBlock> <TextBlock FontSize="14" FontWeight="Bold" Text="Enter an item for sale"/> <HeaderedContentControl Header="Description"> <TextBox Width="150" Text="{Binding Path=Description, Mode=TwoWay}"/> </HeaderedContentControl> <HeaderedContentControl Header="Price"> <TextBox Name="priceField" Width="150"> <TextBox.Text> <Binding Path="Price" Mode="TwoWay" >
<!--自定义的ValidationRule--> <Binding.ValidationRules> <src:PriceIsAPositiveNumber/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </HeaderedContentControl> <HeaderedContentControl Header="Date Offer Ends"> <TextBox Name="dateField" Width="150" > <TextBox.Text> <Binding Path="OfferExpires" StringFormat="d" >
<!--自定义的ValidationRule--> <Binding.ValidationRules> <src:FutureDateRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </HeaderedContentControl> <StackPanel Orientation="Horizontal"> <Button IsDefault="True" Click="Submit_Click">_Submit</Button> <Button IsCancel="True" Click="Cancel_Click">_Cancel</Button> </StackPanel> <HeaderedContentControl Header="Description"> <TextBlock Width="150" Text="{Binding Path=Description}"/> </HeaderedContentControl> <HeaderedContentControl Header="Price"> <TextBlock Width="150" Text="{Binding Path=Price, StringFormat=c}"/> </HeaderedContentControl> <HeaderedContentControl Header="Date Offer Ends"> <TextBlock Width="150" Text="{Binding Path=OfferExpires, StringFormat=d}"/> </HeaderedContentControl> </StackPanel> </Window>
void stackPanel1_Loaded(object sender, RoutedEventArgs e) { // Set the DataContext to a PurchaseItem object. // The BindingGroup and Binding objects use this as // the source. stackPanel1.DataContext = new PurchaseItem(); // Begin an edit transaction that enables // the object to accept or roll back changes. stackPanel1.BindingGroup.BeginEdit(); } private void Submit_Click(object sender, RoutedEventArgs e) { //验证并提交
if (stackPanel1.BindingGroup.CommitEdit()) { MessageBox.Show("Item submitted"); //提交成功后继续接收edit信息
stackPanel1.BindingGroup.BeginEdit(); } } private void Cancel_Click(object sender, RoutedEventArgs e) { // Cancel the pending changes and begin a new edit transaction. stackPanel1.BindingGroup.CancelEdit(); stackPanel1.BindingGroup.BeginEdit(); } // This event occurs when a ValidationRule in the BindingGroup // or in a Binding fails. private void ItemError(object sender, ValidationErrorEventArgs e) { if (e.Action == ValidationErrorEventAction.Added)//描述是添加还是清除了 ValidationError 对象 { MessageBox.Show(e.Error.ErrorContent.ToString()); } }
ValidationRule文件:
public class ValidateDateAndPrice : ValidationRule { // Ensure that an item over $100 is available for at least 7 days. public override ValidationResult Validate(object value, CultureInfo cultureInfo) { BindingGroup bg = value as BindingGroup; // Get the source object. PurchaseItem item = bg.Items[0] as PurchaseItem; object doubleValue; object dateTimeValue; // Get the proposed values for Price and OfferExpires. bool priceResult = bg.TryGetValue(item, "Price", out doubleValue); bool dateResult = bg.TryGetValue(item, "OfferExpires", out dateTimeValue); if (!priceResult || !dateResult) { return new ValidationResult(false, "Properties not found"); } double price = (double)doubleValue; DateTime offerExpires = (DateTime)dateTimeValue; // Check that an item over $100 is available for at least 7 days. if (price > 100) { if (offerExpires < DateTime.Today + new TimeSpan(7, 0, 0, 0)) { return new ValidationResult(false, "Items over $100 must be available for at least 7 days."); } } return ValidationResult.ValidResult; } } //Ensure that the price is positive. public class PriceIsAPositiveNumber : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { try { double price = Convert.ToDouble(value); if (price < 0) { return new ValidationResult(false, "Price must be positive."); } else { return ValidationResult.ValidResult; } } catch (Exception) { // Exception thrown by Conversion - value is not a number. return new ValidationResult(false, "Price must be a number."); } } } // Ensure that the date is in the future. class FutureDateRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { DateTime date; try { date = DateTime.Parse(value.ToString()); } catch (FormatException) { return new ValidationResult(false, "Value is not a valid date."); } if (DateTime.Now.Date > date) { return new ValidationResult(false, "Please enter a date in the future."); } else { return ValidationResult.ValidResult; } } } // PurchaseItem implements INotifyPropertyChanged and IEditableObject // to support edit transactions, which enable users to cancel pending changes. public class PurchaseItem : INotifyPropertyChanged, IEditableObject { struct ItemData { internal string Description; internal double Price; internal DateTime OfferExpires; static internal ItemData NewItem() { ItemData data = new ItemData(); data.Description = "New item"; data.Price = 0; data.OfferExpires = DateTime.Now + new TimeSpan(7, 0, 0, 0); return data; } } ItemData copyData = ItemData.NewItem(); ItemData currentData = ItemData.NewItem(); public PurchaseItem() { } public PurchaseItem(string desc, double price, DateTime endDate) { Description = desc; Price = price; OfferExpires = endDate; } public override string ToString() { return String.Format("{0}, {1:c}, {2:D}", Description, Price, OfferExpires); } public string Description { get { return currentData.Description; } set { if (currentData.Description != value) { currentData.Description = value; NotifyPropertyChanged("Description"); } } } public double Price { get { return currentData.Price; } set { if (currentData.Price != value) { currentData.Price = value; NotifyPropertyChanged("Price"); } } } public DateTime OfferExpires { get { return currentData.OfferExpires; } set { if (value != currentData.OfferExpires) { currentData.OfferExpires = value; NotifyPropertyChanged("OfferExpires"); } } } #region INotifyPropertyChanged Members public event PropertyChangedEventHandler PropertyChanged; private void NotifyPropertyChanged(String info) { if (PropertyChanged != null) { PropertyChanged(this, new PropertyChangedEventArgs(info)); } } #endregion #region IEditableObject Members public void BeginEdit() { copyData = currentData; } public void CancelEdit() { currentData = copyData; NotifyPropertyChanged(""); } public void EndEdit() { copyData = ItemData.NewItem(); } #endregion }
此例中PurchaseItem继承了IEditableObject,那么BindingGroup使用的BeginEdit,CancelEdit, EndEdit会使用IEditableObject中的相应方法。
下例点击Add Customer时,验证通过后会在集合中增加一个Customer对象,要求Customer所在区域与客服代表所在区域一致。
<Window x:Class="ValidateItemInItemsControlSample.Window1" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:sys="clr-namespace:System;assembly=mscorlib" xmlns:src="clr-namespace:ValidateItemInItemsControlSample" xmlns:diag="clr-namespace:System.Diagnostics;assembly=WindowsBase" Title="Window1"> <StackPanel> <StackPanel.Resources> <!--枚举值做数据源,使用ObjectDataProvider,这里有介绍-->
<!--方法原型是Enum.GetValues(Type),返回值是一数组--> <ObjectDataProvider MethodName="GetValues" ObjectType="{x:Type sys:Enum}" x:Key="RegionValues"> <ObjectDataProvider.MethodParameters> <x:Type TypeName="src:Region" /> </ObjectDataProvider.MethodParameters> </ObjectDataProvider>
<!—Representantives是ServiceRep(客服代表)实例的集合--> <src:Representantives x:Key="SaleReps"/> <!—集合中各项的模版-->
<DataTemplate x:Key="ItemTemplate" > <StackPanel Orientation="Horizontal" > <TextBlock Text="Customer Name" Margin="5"/> <TextBox Width="100" Margin="5" Text="{Binding Name}"/> <TextBlock Text="Region" Margin="5"/> <ComboBox ItemsSource="{Binding Source={StaticResource RegionValues}}" SelectedItem="{Binding Location}" Width="100" Margin="5"/> <TextBlock Text="Service Representative" Margin="5"/> <ComboBox ItemsSource="{Binding Source={StaticResource SaleReps}}" SelectedItem="{Binding ServiceRepresentative}" Width="200" Margin="5"/> <Button Content="Save Customer" Click="saveCustomer_Click"/> </StackPanel> </DataTemplate> </StackPanel.Resources> <TextBlock FontSize="14" TextWrapping="Wrap" Margin="5"> This sample demonstrates how to validate an object in an ItemsControl. The ValidationRule assigned to ItemsControl.ItemBindingGroup checks multiple properties in the item. This sample checks that a customer is assigned to a sales representative that serves their area. </TextBlock> <!—设置Itemtemplate和ItemSource-->
<ItemsControl Margin="5" Name="customerList" ItemTemplate="{StaticResource ItemTemplate}" ItemsSource="{Binding}"> <ItemsControl.ItemBindingGroup> <BindingGroup> <BindingGroup.ValidationRules> <src:AreasMatch/> </BindingGroup.ValidationRules> </BindingGroup> </ItemsControl.ItemBindingGroup> <!—获取或设置 Style,它应用于为每个项生成的容器元素。这是一个依赖项属性-->
<ItemsControl.ItemContainerStyle>
<!—ItemsControl里的每项实际是以ContentPresenter作为UI显示的载体--> <Style TargetType="{x:Type ContentPresenter}"> <Setter Property="Validation.ValidationAdornerSite" Value="{Binding ElementName=validationErrorReport}"/> </Style> </ItemsControl.ItemContainerStyle> </ItemsControl> <Label Name="validationErrorReport" Content="{Binding RelativeSource={RelativeSource Self}, Path=(Validation.ValidationAdornerSiteFor).(Validation.Errors)[0].ErrorContent}" Margin="5" Foreground="Red" HorizontalAlignment="Center"/> <Button Content="Add Customer" Click="AddCustomer_Click" HorizontalAlignment="Center"/> </StackPanel> </Window>
这里用了一个Label(validationErrorReport)来显示验证错误信息,验证的错误是以Validation.Errors这个Attached Property作为载体。
通过Validation.ValidationAdornerSite和Validation.ValidationAdornerSiteFor可以设置错误消息源(ItemsControl中的各项)和接收错误的载体(Label)。
但是这种做法是有点问题的,我在另一篇中会讲这个例子的运行效果。其有问题的原因是因为Validation类是静态类,里面的所有成员及方法都是静态的,只能对一个有效。
Backend code:
public partial class Window1 : Window { Customers customerData; BindingGroup bindingGroupInError = null; public Window1() { InitializeComponent(); customerData = new Customers();
// 设置ItemsControl的源 customerList.DataContext = customerData; } void AddCustomer_Click(object sender, RoutedEventArgs e) { if (bindingGroupInError == null) { customerData.Add(new Customer()); } else { MessageBox.Show("Please correct the data in error before adding a new customer."); } } void saveCustomer_Click(object sender, RoutedEventArgs e) { Button btn = sender as Button;
// ItemsControl.ContainerFromElement MSND上是这么说的:返回属于拥有给定元素的当前 ItemsControl 的容器。读起来和念易筋经一样
FrameworkElement container = (FrameworkElement) customerList.ContainerFromElement(btn); // If the user is trying to change an items, when another item has an error, // display a message and cancel the currently edited item. if (bindingGroupInError != null && bindingGroupInError != container.BindingGroup) { MessageBox.Show("Please correct the data in error before changing another customer"); container.BindingGroup.CancelEdit(); return; } if (container.BindingGroup.ValidateWithoutUpdate()) { container.BindingGroup.UpdateSources(); bindingGroupInError = null; MessageBox.Show("Item Saved"); } else { bindingGroupInError = container.BindingGroup; } }
ValicationRule文件:
public class Customers : ObservableCollection<Customer> { public Customers() { Add(new Customer()); } } public enum Region { Africa, Antartica, Australia, Asia, Europe, NorthAmerica, SouthAmerica } public class Customer { public string Name { get; set; } public ServiceRep ServiceRepresentative { get; set; } public Region Location { get; set; } } public class ServiceRep { public string Name { get; set; } public Region Area { get; set; } public ServiceRep() { } public ServiceRep(string name, Region area) { Name = name; Area = area; } public override string ToString() { return Name + " - " + Area.ToString(); } } public class Representantives : ObservableCollection<ServiceRep> { public Representantives() { Add(new ServiceRep("Haluk Kocak", Region.Africa)); Add(new ServiceRep("Reed Koch", Region.Antartica)); Add(new ServiceRep("Christine Koch", Region.Asia)); Add(new ServiceRep("Alisa Lawyer", Region.Australia)); Add(new ServiceRep("Petr Lazecky", Region.Europe)); Add(new ServiceRep("Karina Leal", Region.NorthAmerica)); Add(new ServiceRep("Kelley LeBeau", Region.SouthAmerica)); Add(new ServiceRep("Yoichiro Okada", Region.Africa)); Add(new ServiceRep("T¨¹lin Oktay", Region.Antartica)); Add(new ServiceRep("Preeda Ola", Region.Asia)); Add(new ServiceRep("Carole Poland", Region.Australia)); Add(new ServiceRep("Idan Plonsky", Region.Europe)); Add(new ServiceRep("Josh Pollock", Region.NorthAmerica)); Add(new ServiceRep("Daphna Porath", Region.SouthAmerica)); } } // Check whether the customer and service representative are in the // same area. public class AreasMatch : ValidationRule { public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { BindingGroup bg = value as BindingGroup; Customer cust = bg.Items[0] as Customer; if (cust == null) { return new ValidationResult(false, "Customer is not the source object"); } Region region = (Region)bg.GetValue(cust, "Location"); ServiceRep rep = bg.GetValue(cust, "ServiceRepresentative") as ServiceRep; string customerName = bg.GetValue(cust, "Name") as string; // 相等说明验证通过
if (region == rep.Area) { return ValidationResult.ValidResult; } else { StringBuilder sb = new StringBuilder(); sb.AppendFormat("{0} must be assigned a sales representative that serves the {1} region. \n ", customerName, region); return new ValidationResult(false, sb.ToString()); } } }