다음을 통해 공유


WPF: The Myth of BindingGroup

Do you use BindingGroup often? I bet 90% of WPF Developers would answer this with "nope" but one guy gave it a try. A user asked this month a very interesting question about BindingGroups in WPF. He wanted to know why was his BindingGroup not working as expected. BindingGroups are not used often in WPF and many developers don't even know they exist. That moved me to write this article and hopefully inspire somebody to stop hacking around and instead use the elegant way with BindingGroups.

Here is the link for everybody who whishes to check out the question:
http://social.msdn.microsoft.com/Forums/vstudio/en-US/7ef66977-f792-449c-9a0f-972bd397d213/two-questions-about-bindinggroup?forum=wpf

Lets get started...

Definition of BindingGroup

The very simple explanation would be:

  • Contains a collection of bindings and ValidationRule objects that are used to validate an object.

But lets face the truth nobody would ever guess what are BindingGroups good for just by reading that one sentence so here is a bit detailed info for you guys from MSDN.

  • "A BindingGroup creates a relationship between multiple bindings, which can be validated and updated together. For example, suppose that an application prompts the user to enter an address. The application then populates an object of type Address, which has the properties, Street, City, ZipCode, and Country, with the values that the user provided. The application has a panel that contains four TextBox controls, each of which is data bound to one of the object’s properties. You can use a ValidationRule in a BindingGroup to validate the Address object. If the bindings participate in the same BindingGroup, you can ensure that the zip-code is valid for the country of the address. You set the BindingGroup property on FrameworkElement or FrameworkContentElement. Child elements inherit the BindingGroup from their parent elements, just as with any other inheritable property."

Here is MSDN Link for those who wish to read more:

http://msdn.microsoft.com/en-us/library/system.windows.data.bindinggroup%28v=vs.110%29.aspx

Hands on BindingGroup

public class PersonValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        BindingGroup bindingGroup = (BindingGroup)value;
        Person person = (Person)bindingGroup.Items[0];
 
        // validate the age
        object objValue = null;
        if (!bindingGroup.TryGetValue(person, "Age", out objValue))
        {
            return new ValidationResult(false, "Age is not a whole number");
        }
 
        // if we can retrieve the value - can we parse it to an int?
        int parseResult;
        if (!Int32.TryParse(objValue as string, out parseResult))
        {
            return new ValidationResult(false, string.Format("Age is not a whole number"));
        }
 
        return ValidationResult.ValidResult;
    }
}
 
public class NullValidationRule : ValidationRule
{
    public override ValidationResult Validate(object value, CultureInfo cultureInfo)
    {
        var result = value as string;
        return string.IsNullOrEmpty(result) ? new ValidationResult(false, "Value may not be null or empty") : ValidationResult.ValidResult; 
    }
}

NullValidationRule is gonna be a standalone ValidationRule and it will be called first before the BindingGroup's validation.

Now lets create a Model with IDataErrorInfo.

public class Person : IDataErrorInfo, INotifyPropertyChanged
{
    private int age;
 
    public int Age
    {
        get { return age; }
        set
        {
            age = value;
            RaisePropertyChanged("Age");
        }
    }
 
    private string name;
 
    public string Name
    {
        get { return name; }
        set
        {
            name = value;
            RaisePropertyChanged("Name");
        }
    }
 
    public string Error
    {
        get { return null; }
    }
 
    public string this[string columnName]
    {
        get
        {
            if (columnName == "Age")
            {
                if (Age < 0)
                    return "Age cannot be less than 0.";
                if (Age > 120)
                    return "Age cannot be greater than 120.";
            }
 
            return null;
        }
    }
 
    private void RaisePropertyChanged(string propertyName)
    {
        if (this.PropertyChanged != null)
        {
            this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
 
    public event PropertyChangedEventHandler PropertyChanged;
}

And now the usage of BindingGroups in XAML.. As you can see a BindingGroup should be defined at root level so it may be inherited to its children bindings.

<Window ...>    
   <Window.Resources>
        <DataTemplate x:Key="myDataTemplate1">
            <Grid x:Name="RootElement1" VerticalAlignment="Top">
                <Grid.BindingGroup>
                    <BindingGroup Name="myBindingGroup1">
                        <BindingGroup.ValidationRules>
                            <local:PersonValidationRule/>
                        </BindingGroup.ValidationRules>
                    </BindingGroup>
                </Grid.BindingGroup>
 
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="Auto"/>
                    <ColumnDefinition Width="*"/>
                </Grid.ColumnDefinitions>
 
                <Grid.RowDefinitions>
                    <RowDefinition/>
                    <RowDefinition/>
                    <RowDefinition/>
                </Grid.RowDefinitions>
 
                <Label Content="Name:"/>
                <TextBox Grid.Column="1" LostFocus="TextBox_LostFocus">
                    <TextBox.Text>
                        <Binding Path="Name" BindingGroupName="myBindingGroup1">
                            <Binding.ValidationRules>
                                <local:NullValidationRule/>
                            </Binding.ValidationRules>
                        </Binding>
                    </TextBox.Text>
                </TextBox>
                 
                <Label Grid.Row="1" Content="Age:"/>
                <TextBox Grid.Row="1" Grid.Column="1" LostFocus="TextBox_LostFocus" Text="{Binding Age, ValidatesOnDataErrors=true, BindingGroupName=myBindingGroup1}"/>
                <Label Grid.Row="2" Grid.ColumnSpan="2" Content="{Binding Path=(Validation.Errors)[0].ErrorContent, ElementName=RootElement}"/>
                 
            </Grid>
        </DataTemplate>
    </Window.Resources>
 
    <Grid>
        <TabControl>
            <TabItem x:Name="tabItem1" Header="Example1">
                <ContentControl x:Name="contentControl1" ContentTemplate="{StaticResource myDataTemplate1}"  Content="{Binding}"/>
            </TabItem>
        </TabControl>
    </Grid>
</Window>
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.DataContext = new Person();
    }
 
    private void TextBox_LostFocus(object sender, RoutedEventArgs e)
    {
        ((FrameworkElement)this.contentControl1.ContentTemplate.FindName("RootElement1", (FrameworkElement)VisualTreeHelper.GetChild(this.contentControl1, 0))).BindingGroup.CommitEdit();
    }
}

Summary

BindingGroups allow you to validate the complete object and therefore they need to be placed at parents level (see my example). BindingGroups support property value inheritance and so they will be sticked to each inner Binding and/or after all inner ValidationRules inside their scope. Though if you set UpdateSourceTrigger on a certain Binding explicitly the BindingGroup inheritance will skip that one. Means the list of items will not contain the source of that one. Furthermore the order of items in the list will match the order of all inner Bindings inside BindingGroup scope.

Once user done editing something the changes will be only permitted to the source when the BindingGroup allows it. Means that you need to call CommitEdits() if you want changes to be saved. Else you can call the cancel edits method to throw yours changes away.

That feature of BindingGroup allow you to have the complete control over all the inner Bindings inside the BindingGroup's scope.

I should also mention that once BindingGroup turns to invalid state the ErrorTemplate will not be displayed per TextBox/Binding but instead on owners control which holds the BindingGroup. In my case it is the Grid. However you can use the ValidationAdornerSite property to move the ErrorTemplate on desired control.

Lets take a look inside 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();
}

In cases you do not want the ValidationRules to know about your model classes you can use the GetValue or TryGetValue method and so you will avoid any kind of dependencies between View and ViewModel.

The Rules of ValidationRule explained in 30 seconds
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. 
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 UpdateValue.
  • CommitEdit: does the same as UpdateSources, and then runs the rules marked as CommittedValue."

Here is the link to a great BindingGroup tutorial for those who wish to read more:

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

For any questions about BindingGroups feel free to leave a comment. Also just ask if you wish to know more about property value inheritance and BindingGroups or how BindingGroups know about inner Bindings even though a Binding is not "directly" part of LogicalTree.

Code example given is ment to be used for tutorial purposes. All rights are reserved. :)