前文介绍了自定义或系统自带的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,则可以确保邮政编码对于地址所在国家/地区有效。

设置 FrameworkElementFrameworkContentElement 上的 BindingGroup 属性。正如任何其他可继承属性一样,子元素从其父元素继承 BindingGroup。如果发生以下情况之一,则会将子代元素上的绑定添加到 BindingGroup:

在地址示例中,假定将 PanelDataContext 设置为 Address 类型的对象。每个 TextBox 的绑定均添加到面板的 BindingGroup 中。

ValidationRule 对象添加到 BindingGroup 中。在运行 ValidationRule 时,将 BindingGroup 作为 Validate 方法的第一个参数传递。可以使用该 BindingGroup 上的 TryGetValueGetValue(Object, String) 方法获取对象的建议值,使用 Items 属性获取绑定的源。

BindingGroup 在同一时间更新绑定的源,而不是分别更新每个绑定。在调用任一方法(ValidateWithoutUpdateUpdateSourcesCommitEdit)验证数据时,将验证并可能会更新示例中的每个 TextBox 的绑定。当绑定是 BindingGroup 的一部分时,除非显式设置 UpdateSourceTrigger 属性,否则在对 BindingGroup 调用 UpdateSourcesCommitEdit 之前,不会更新绑定的源。

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 类型的属性。
如果 BindingPath 为 Address.ZipCode 属性,则 Address 会添加到 Items 属性中。
 
NotifyOnValidationError:获取或设置在 ValidationRule 的状态更改时是否发生 Validation.Error 事件。
 
BeginEdit:开始编辑事务。
 
CommitEdit:运行所有的Rule,如果成功,则保存更改,更新源。
 
CancelEdit:取消更改。
 
以上三个,如果源对应的类继承自IEditableObject, 会调用IEditableObject中的相应方法。
 
UpdateSources:运行所有ValidationStep设置为RawProposedValueConvertedProposedValueUpdatedValue的Rule。如果成功,更新源。此方法不会挂起事务并结束事务,也就是说调用完该方法后事务还是处于运行中。
 
ValidateWithoutUpdate:如同UpdateSources,但是不会更新源。
 
所以有三个方法可以用作验证:CommitEdit,UpdateSources,ValidateWithoutUpdate。  

先看看验证实体类的示例:

image

<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所在区域与客服代表所在区域一致。
 
image 
<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());
            }
        }
    }