:WPF 3.5 SP1 Feature: BindingGroups with Item-level Validation

原文:http://blogs.msdn.com/vinsibal/archive/2008/08/11/wpf-3-5-sp1-feature-bindinggroups-with-item-level-validation.aspx

Motivation

Before 3.5 SP1, the binding validation system worked with only one binding at a time.  What made this difficult in some scenarios was when validation was required on a set of bound objects.  Some typical examples that come to mind are a form of data items that are submitted all at once or a ListView or DataGrid row that is committed all at once.  You can put a validation rule on each bound object and validate their values individually but there was no readily available way in the binding validation system to validate multiple objects together.  Now in 3.5 SP1 a solution has been created for this, Binding Groups.


BindingGroup 在多个绑定之间创建关系,从而可一起验证和更新这些绑定。

 
What is it?

A BindingGroup encapsulates and has access to a set of related bindings.  These related bindings are bindings that share the same data context as the BindingGroup and/or bindings that have explicitly declared membership to the BindingGroup.  With this set of bindings, the BindingGroup can now provide services such as validation of all the encapsulated bindings together and transactional editing. 

  How do I define a BindingGroup?

We will use an example to illustrate how to define a BindingGroup.  Let’s say this is the data context that I will use (I just went canoeing recently which influence my choice of data):      

public class BoatRentalCustomer

{

  public string FirstName { get; set; }

  public string LastName { get; set; }

  public DateTime? DateOfBirth { get; set; }

  public int BoatCheckOutID { get; set; }

  public DateTime? StartDate { get; set; }

  public DateTime? EndDate { get; set; }

}  

 

With this data I want to use it in a form to be filled in by the customer.  Here is the xaml (I only put the relevant information for this example):

<Grid>

  <StackPanel>

    <TextBox Text="{Binding Path=FirstName}" />

    <TextBox Text="{Binding Path=LastName}" />

    <TextBox Text="{Binding Path=DateOfBirth}" />

    <TextBox Text="{Binding Path=BoatCheckOutID}" />

    <TextBox Text="{Binding Path=StartDate}" />

    <TextBox Text="{Binding Path=EndDate}" />

    <Button Click="submit_Click">Submit</Button>

  </StackPanel>

</Grid>  

 

As I said in the Motivation section, we can add validation to each binding and validate them individually, but there wasn’t a built in mechanism to validate them as a group.  To do that, we can add a BindingGroup to the Grid:

<Grid>

  <Grid.BindingGroup>

    <BindingGroup>

      <BindingGroup.ValidationRules>

        ...

      </BindingGroup.ValidationRules>

    </BindingGroup>

  </Grid.BindingGroup>

  <StackPanel>

    <TextBox Text="{Binding Path=FirstName}" />

    <TextBox Text="{Binding Path=LastName}" />

    <TextBox Text="{Binding Path=DateOfBirth}" />

    <TextBox Text="{Binding Path=BoatCheckOutID}" />

    <TextBox Text="{Binding Path=StartDate}" />

    <TextBox Text="{Binding Path=EndDate}" />

    <Button Click="submit_Click">Submit</Button>

  </StackPanel>

</Grid>  

 

Assuming that I’ve set the Grid.DataContext to be an instance of BoatRentalCustomer, the BindingGroup I set on the Grid will have access to all the bindings I set within the Grid.  This example shows how the BindingGroup gets bindings from the same DataContext but it is also possible to get bindings through explicit membership declaration like this slightly modified example below:

<Grid>

  <Grid.BindingGroup>

    <BindingGroup Name="FormBindingGroup">

      <BindingGroup.ValidationRules>

        ...

      </BindingGroup.ValidationRules>

    </BindingGroup>

  </Grid.BindingGroup>

  <StackPanel>

    <Slider Name="sliderFontSize" Minimum="1" Maximum="100" Value="10" />

    <TextBox Text="{Binding Path=FirstName}"

             FontSize="{Binding ElementName=sliderFontSize,

                               Path=Value,

                               BindingGroupName=FormBindingGroup}" />

    <TextBox Text="{Binding Path=LastName}" />

    <TextBox Text="{Binding Path=DateOfBirth}" />

    <TextBox Text="{Binding Path=BoatCheckOutID}" />

    <TextBox Text="{Binding Path=StartDate}" />

    <TextBox Text="{Binding Path=EndDate}" />

    <Button Click="submit_Click">Submit</Button>

  </StackPanel>

</Grid>  

 

 

I named the BindingGroup, “FormBindingGroup”, and added a binding to the FontSize property on one of the TextBox elements.  Notice that in the binding I explicitly declare BindingGroupName to be set to “FormBindingGroup”.  By doing this, the BindingGroup will have access to the bindings under the same DataContext as well as the bindings associated with it by the same BindingGroupName.  The ability to declare explicit membership to a BindingGroup are for scenarios where bindings don’t use DataContext (explicit Source or ElementName like in the example above) and/or bindings that use a different DataContext that want to participate in the same group validation.

注意上面这段话的意思,在Grid中定义了BindingGroup,如果指定了Grid的DataContext是一个BoatRentalCustomer实例,那么在Grid中与这个DataContext相关的绑定都将参与BindingGroup的验证。
但是上例中的FontSize用到的DataContext却是从sliderFontSize得到的,通过显式的指定BindingGroupName,也可以让TextBox的FontSize绑定参与到BindingGroup的验证中来。

From the previous example you saw that Grid had a property named BindingGroup.  You may be wondering where BindingGroup is actually defined.  BindingGroup has been added to FrameworkElement and FrameworkContentElement so it can be associated with a set of bindings on a per element basis.
      

public class Framework[Content]Element

{

  public static readonly DependencyProperty BindingGroupProperty;

  public BindingGroup BindingGroup { get; set; }

}

 

It has also been added to ItemsControl so it can be associated with a set of bindings per each generated container.  The pattern is similar to ItemTemplate.

public class ItemsControl

{   

  public static readonly DependencyProperty ItemBindingGroupProperty;

  public BindingGroup ItemBindingGroup { get; set; }

}  

 

And as you saw the use of BindingGroupName, that property has been defined in BindingBase. 

public class BindingBase

{

    public string BindingGroupName { get; set; }

}  

 

How do I use it? (Part 1)

So now I know where to define a BindingGroup and how bindings get associated with it, but how do I use it?  Let’s first look at the BindingGroup class and then dig into the common APIs and common scenarios.  Here is the BindingGroup class:

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();

}

 

You have access to the associated bindings through the BindingExpressions property and can set group validation rules through the ValidationRules property.  You may have notice all these transactional methods in this class such as BeginEdit, CancelEdit, etc.  Well, as part of group validation you have the ability to control whether the new data to that item is committed as a whole and/or reverted back to its previous state.  The two major areas of BindingGroup that I want to get into are its validation rules and how its methods are used to update the binding sources.  First, let’s discuss validation. 

The ValidationRule class has been updated with a couple APIs to work with BindingGroup.  Here are the new APIs:

public abstract class ValidationRule

{

  public bool ValidatesOnTargetUpdated { get; set; }

  public ValidationStep ValidationStep { get; set; }

}

 

public enum ValidationStep

{

  RawProposedValue = 0,

  ConvertedProposedValue = 1,

  UpdatedValue = 2,

  CommittedValue = 3,

}

 

The ValidationStep property is used to let ValidationRule know when to apply the validation.  RawProposedValue means the rule is applied to the unconverted value.  This is the default step which also was the current behavior for ValidationRule prior to this feature.  ConvertedProposedValue means the rule is applied to the converted value, UpdatedValue means the rule is applied after writing to the source, and CommittedValue means the rule is applied after committing changes.  ValidatesOnTargetUpdated is used to trigger validation when the source is updating the target. 


RawProposedValue在执行convention之前验证
ConvertedProposedValue在执行convention之后验证
UpdatedValue在source被updated之后验证
CommittedValue要看source支持不支持submitting or rolling back 。Runs the System.Windows.Controls.ValidationRule after the value has been
committed to the source if the source supports submitting or rolling back
changes--for example, if the source implements System.ComponentModel.IEditableObject.
If the source does not submitting or rolling back changes, the System.Windows.Controls.ValidationRule
 runs after the source is updated.
ValidatesOnTargetUpdated指示当source更新target的时候要不要触发验证。

So now we have an idea of how validation rule may be used but how does this relate to the transactional methods of BindingGroup?  Note that to use a BindingGroup for transactional editing, you should define IEditableObject on your data item. 

BeginEdit, CancelEdit, and CommitEdit work similar to IEditableCollectionView’s versions where they will call IEditableObject.BeginEdit, IEditableObject.CancelEdit, and IEditableObject.EndEdit respectively on the data item.  In addition to CommitEdit, you also have UpdateSources and ValidateWithoutUpdate.  These three methods (CommitEdit, UpdateSources, and ValidateWithoutUpdate) are the main methods that you will call to validate and update your source.  That brings up a point on how bindings update in a BindingGroup.  UpdateSourceTrigger for bindings that belong to the BindingGroup are set to Explicit by default.  That means you will need to use the BindingGroup APIs to update the bindings (CommitEdit or UpdateSources).  Validation and updating methods work like this:

·         ValidateWithoutUpdate: runs all validation rules marked as RawProposedValue or ConvertedProposedValue.

·         UpdateSources: does the same as ValidateWithoutUpdate, and then if no errors were found, it writes all the values to the data item and runs the validation rules marked as UpdatedValue.

·         CommitEdit: does the same as UpdateSources, and then runs the rules marked as CommittedValue.

How do I use it? (Part 2)

Ok, we made it this far.  So I know where to define my BindingGroup, how to setup when I want my validation rules ran and how to update my bindings.  Let’s look at what kinds of things I can do to actually validate the item inside the ValidationRule.Validate method.

The value input parameter to Validate is the actual BindingGroup.  With the BindingGroup you can query for the data on the item.  The important BindingGroup APIs for the actual validation include Items, GetValue, and TryGetValue.  The Items collection contains the unique data items used by the bindings in the group.  GetValue looks for the binding that uses the given item and propertyName parameter values and returns the value appropriate to the current validation step.  So for example, if the validation step is RawProposedValue or ConvertedProposedValue, the data source has not been updated yet when the Validate method is called, but by querying for the value using GetValue you can get what the value will be (as long as it passes validation).  Here is an example Validate method:

public override ValidationResult Validate(object value, CultureInfo cultureInfo)

{

  BindingGroup bindingGroup = (BindingGroup)value;

  BoatRentalCustomer customer = (BoatRentalCustomer)bindingGroup.Items[0];

 

  object startDateObj = bindingGroup.GetValue(customer, "StartDate");

  DateTime? startDate = DateTime.Parse((string)startDateObj);

 

  object endDateObj = bindingGroup.GetValue(customer, "EndDate");

  DateTime? endDate = DateTime.Parse((string)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);
  }

}


注意这里的value实际上就是BindingGroup,datasource是Items[0],如果BindingGroup的binding来自于多个
datasource这个Items集合就有多个值了。

You’ll notice that I am assuming the data item is Items[0].  If the BindingGroup has bindings from more than one data source then you will have to handle that as the Items collection will be greater than one.  I’ve also been pretty optimistic about getting the proposed values as well as parsing the DateTime.  This example is for demonstration purposes only but in a production level application you will probably want to use TryGetValue and TryParse.  So inside the Validate method I have access to the entire item and in the example I validate the start and end date together.

Where we are…

Hopefully I still have your attention after all that.  I know it’s a lot to go through.  Just to summarize a bit, a BindingGroup enabled you to validate data at the item-level as opposed to the property-level.  This is possible because the BindingGroup has access to all the bindings associated with the data item (assuming you set the BindingGroup that way).  BindingGroup is available on all FrameworkElements, FrameworkContentElements, and ItemsControls.  You set the validation rules on the actual BindingGroup and in the ValidationRule.Validate method you are given the BindingGroup where you can query for all the properties on the item being validated.  I’ve attached a small sample to demonstrate some of the concepts discussed here. 

So what’s left?  Well, there are two more important topics that should be discussed.  One is the new additions to validation feedback and the other is how BindingGroup works in conjunction with IEditableCollectionView.  I will save both these topics for another post as there is enough here to digest.  This does cover about 95% of the new APIs relating to BindingGroup so there isn’t too much more to cover.  = )

However, BindingGroup and IEditableCollectionView can be a bit confusing on when and how to use them so I do want to talk more on that.  Read more on BindingGroup and IEditableCollectionView here.  And you can read on BindingGroup and ValidationFeedback here.

Posted: Monday, August 11, 2008 9:14 PM by vinsibal

BindingGroupSample下载
对BindingGroupSample的一些说明:
1. AdornedElementPlaceholder
用它的时候仅当我们要自定义Validation.ErrorTemplate的时候。例如本例中的ErrorTemplate定义:
Code
 
这里的AdornedElement实际上就是Grid了,因为是更改了它的ErrorTemplate。所以当出错了的时候,这个
Grid就应用上面的ErrorTemplate,并把自己(Grid)放到 AdornedElementPlaceholder 所在的位置(例中
Window1的content就是一个Grid),如下图:

AdornedElementPlaceholder.jpg
 

你可能感兴趣的:(validation)