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.)
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.
This is an extension to mbmcavoy's answer.
I have modified the GetBoundValue
method in order to remove the limitation for binding paths. The BindingExpression conveniently has the properties ResolvedSource and ResolvedSourcePropertyName, which are visible in the Debugger but not accessible via normal code. To get them via reflection is no problem though and this solution should work with any binding path.
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
string resolvedPropertyName = binding.GetType().GetProperty("ResolvedSourcePropertyName", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null).ToString();
object resolvedSource = binding.GetType().GetProperty("ResolvedSource", BindingFlags.Public | BindingFlags.DeclaredOnly | BindingFlags.Instance).GetValue(binding, null);
// Extract the value of the property
object propertyValue = resolvedSource.GetType().GetProperty(resolvedPropertyName).GetValue(resolvedSource, null);
return propertyValue;
}
else
{
return value;
}
}
This is an alternative extension to mbmcavoy's and adabyron's answer.
In order to remove the limitation for binding paths, I get the property value using such method:
public static object GetPropertyValue(object obj, string propertyName)
{
foreach (String part in propertyName.Split('.'))
{
if (obj == null) { return null; }
Type type = obj.GetType();
PropertyInfo info = type.GetProperty(part);
if (info == null) { return null; }
obj = info.GetValue(obj, null);
}
return obj;
}
Now simply change
object propertyValue = dataItem.GetType().GetProperty(propertyName).GetValue(dataItem, null);
to
object propertyValue = GetPropertyValue(dataItem, propertyName);
Related post: Get property value from string using reflection in C#
In order to answer to your 2 question:
string strVal = (string)((BindingExpression)value).DataItem
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With