ValidationRule with ValidationStep="UpdatedValue" is called with BindingExpression instead of updated value

data-binding, validation, wpf

Solution

I have solved the problem of extracting the value from the `BindingExpression`, with a minor limitation.

First, some more complete XAML:

<Window x:Class="ValidationRuleTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:ValidationRuleTest"
        Title="MainWindow" Height="100" Width="525">
    <Window.DataContext>
        <local:MainWindowViewModel/>
    </Window.DataContext>
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="50"/>
            <ColumnDefinition Width="*"/>
        </Grid.ColumnDefinitions>
        <TextBlock Text="String 1"/>
        <TextBox Grid.Column="1">
            <TextBox.Text>
                <Binding Path="String1" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:RequiredRule ValidationStep="RawProposedValue"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBlock Text="String 2" Grid.Row="1"/>
        <TextBox Grid.Column="1" Grid.Row="1">
            <TextBox.Text>
                <Binding Path="String2" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:RequiredRule ValidationStep="UpdatedValue"/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
    </Grid>
</Window>

Note that the first TextBox uses `ValidationStep="RawProposedValue"` (the default), while the second one uses `ValidationStep="UpdatedValue"`, but both use the same validation rule.

A simple ViewModel (neglecting INPC and other useful stuff):

class MainWindowViewModel
{
    public string String1
    { get; set; }

    public string String2
    { get; set; }
}

And finally, the new RequiredRule:

class RequiredRule : ValidationRule
{
    public override ValidationResult Validate(object value,
        System.Globalization.CultureInfo cultureInfo)
    {
        // Get and convert the value
        string stringValue = GetBoundValue(value) as string;

        // Specific ValidationRule implementation...
        if (String.IsNullOrWhiteSpace(stringValue))
        {
            return new ValidationResult(false, "Must not be empty"); 
        }
        else
        {
            return new ValidationResult(true, null); 
        }
    }

    private object GetBoundValue(object value)
    {
        if (value is BindingExpression)
        {
            // ValidationStep was UpdatedValue or CommittedValue (Validate after setting)
            // Need to pull the value out of the BindingExpression.
            BindingExpression binding = (BindingExpression)value;

            // Get the bound object and name of the property
            object dataItem = binding.DataItem;
            string propertyName = binding.ParentBinding.Path.Path;

            // Extract the value of the property.
            object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null);

            // This is what we want.
            return propertyValue;
        }
        else
        {
            // ValidationStep was RawProposedValue or ConvertedProposedValue
            // The argument is already what we want!
            return value;
        }
    }
}

The `GetBoundValue()` method will dig out the value I care about if it gets a BindingExpression, or simply kick back the argument if it's not. The real key was finding the "Path", and then using that to get the property and its value.

The limitation: In my original question, my binding had `Path="Identity.Name"`, as I was digging into sub-objects of my ViewModel. This will not work, as the code above expects the path to be directly to a property on the bound object. Fortunately, I have already flattened my ViewModel so this is no longer the case, but a workaround could be to set the control's datacontext to be the sub-object, first.

I'd like to give some credit to Eduardo Brites, as his answer and discussion got me back to digging on this, and did provide a piece to his puzzle. Also, while I was about to ditch the ValidationRules entirely and use IDataErrorInfo instead, I like his suggestion on using them together for different types and complexities of validation.

Problem

I am getting started with using ValidationRules in my WPF application, but quite confused. I have the following simple rule: ``` class RequiredRule : ValidationRule { public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo) { if (String.IsNullOrWhiteSpace(value as string)) { return new ValidationResult(false, "Must not be empty"); } else { return new ValidationResult(true, null); } } } ``` Used in XAML as follows: ``` <TextBox> <TextBox.Text> <Binding Path="Identity.Name"> <Binding.ValidationRules> <validation:RequiredRule/> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> ``` This mostly works as I would expect. I was surprised to see that my source property (`Identity.Name`) was not being set; I have an undo function that never sees the change, and there is no way to revert the value other than re-type it (not good). Microsoft's Data Binding Overview describes the validation process near the bottom, which explains this behavior very well. Based on this, I would want to have my `ValidationStep` set to `UpdatedValue`. ``` <validation:RequiredRule ValidationStep="UpdatedValue"/> ``` This is where things get weird for me. Instead of Validate() being called with object value being the property value that was set (i.e., a string), I get a `System.Windows.Data.BindingExpression`! I don't see anything in Microsoft's documentation that describes this behavior. In the debugger, I can see the source object (the `DataContext` of the `TextBox`), navigate the path to the property, and see that the value has been set. However, I don't see any good way to get to the right property within the validation rule. Note: With `ValidationStep` as `ConvertedProposedValue`, I get the entered string (I don't have a converter in use), but it also blocks the source property update when validation fails, as expected. With `CommittedValue`, I get the `BindingExpression` instead of the string. There are several questions in here: Why do I get an inconsistent argument type being passed to Validate() based on the ValidationStep setting? How can I get to the actual value from the BindingExpression? Alternately, is there a good way to allow the user to revert the TextBox to the previous (valid) state? (As I mentioned, my own undo function never sees the change.)

Original source

Related problems