Reputation: 512
I use a validation rule on a TextBox to validate the user input string. The Text is binding to a float property on the view model and the WPF binding engine is keen enough to auto convert the string to a float for me.
When the validation fail however, the binding seems to read back the old value. This results in that I get a red border around the textbox even though the text has reverted back to the last acceptable floating point value.
Question: How do I make sure that the faulty input text is not automatically overwritten by the binding engine when the validation failed? The binding needs to be twoway.
I should mention that I do a little trick in my ValidationRule where I let it find the current view model from the view model locator, and uses the INotifyDataErrorInfo approach on the view model. That I found to be a great solution, as it means the ViewModel HasError will collect all the validation errors for me (and it let me apply the validation in the validationrules or in view model when setting the property).The benefit of letting the validation rule apply the validation using the INotifyDataErrorInfo on the view model is that the validation can be applied before the auto conversion from string to float, making sure the validation is performed even when the user types in "Hello World" resulting in an exception (swallowed by the binding engine) during the auto conversion to float. This allows me to keep the type of the property being float on the vm and still get the validation executed.
XAML
<TextBox Grid.Row="2" Grid.Column="2" x:Name="txtPreHeight"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
VerticalAlignment="Center"
Template="{DynamicResource TextBoxBaseControlTemplateMainScreen}">
<TextBox.Text>
<Binding
Path="PreHeight"
ValidatesOnExceptions="False"
NotifyOnValidationError="True"
ValidatesOnNotifyDataErrors="True"
UpdateSourceTrigger="LostFocus"
>
<Binding.ValidationRules>
<validationrules:PreHeightValidationRule ViewModelType="GotoPositionViewModel" Min="0" Max="100" ValidationStep="RawProposedValue"/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
<i:Interaction.Triggers>
<helper:RoutedEventTrigger RoutedEvent="{x:Static Validation.ErrorEvent}">
<cmd:EventToCommand Command="{Binding SetFocusOnValidationErrorCommand}"
PassEventArgsToCommand="True" />
</helper:RoutedEventTrigger>
</i:Interaction.Triggers>
</TextBox>
ValidationRule
class PreHeightValidationRule : ValidationRule
{
private ValidationService validationService_;
private Int32 min_ = Int32.MaxValue;
private Int32 max_ = Int32.MinValue;
private string viewModelType_ = null;
public PreHeightValidationRule()
{
validationService_ = ServiceLocator.Current.GetInstance<Validation.ValidationService>();
}
public Int32 Min
{
get { return min_; }
set { min_ = value; }
}
public Int32 Max
{
get { return max_; }
set { max_ = value; }
}
public string ViewModelType
{
get { return viewModelType_; }
set { viewModelType_ = value; }
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo, BindingExpressionBase owner)
{
ValidationResult result = base.Validate(value, cultureInfo, owner);
ViewModel.ViewModelBaseWithNavigation vm;
System.Reflection.Assembly asm = typeof(ViewModelLocator).Assembly;
Type type = null;
if (type == null)
type = asm.GetType(ViewModelType);
if (type == null)
type = asm.GetType("TeachpendantControl.ViewModel." + ViewModelType);
vm = (ViewModel.ViewModelBaseWithNavigation)ServiceLocator.Current.GetInstance(type);
ICollection<string> validationErrors = new List<string>();
try
{
validationService_.ValidatePreHeight(value.ToString(), ref validationErrors, Min, Max);
}
catch (Exception e)
{
validationErrors.Add("Failed to validate, Exception thrown " + e.Message);
}
finally
{
vm.UpdateValidationForProperty(((BindingExpression)owner).ResolvedSourcePropertyName, validationErrors, validationErrors.Count == 0);
}
return new ValidationResult(validationErrors.Count == 0, validationErrors);
}
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
return new ValidationResult(false, null);
}
}
Upvotes: 0
Views: 1874
Reputation: 512
I managed to solve it! I found a hint from Josh that brought my attention in the right direction.
Using a converter one can set the Binding.DoNothing. I modified it into a converter that checks the HasError on the VM. In case of HasError I return Binding.DoNothing otherwise I just forward the value.
using CommonServiceLocator;
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
namespace Converters
{
class HasErrorToBindingDoNothingConverter : DependencyObject, IValueConverter
{
public static readonly DependencyProperty ViewModelTypeProperty =
DependencyProperty.Register("ViewModelType", typeof(string), typeof(HasErrorToBindingDoNothingConverter), new UIPropertyMetadata(""));
public string ViewModelType
{
get { return (string)GetValue(ViewModelTypeProperty); }
set { SetValue(ViewModelTypeProperty, value); }
}
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
try
{
ViewModel.ViewModelBaseWithNavigation vm;
System.Reflection.Assembly asm = typeof(ViewModelLocator).Assembly;
Type type = null;
if (type == null)
type = asm.GetType(ViewModelType);
if (type == null)
type = asm.GetType("TeachpendantControl.ViewModel." + ViewModelType);
vm = (ViewModel.ViewModelBaseWithNavigation)ServiceLocator.Current.GetInstance(type);
if (vm.HasErrors)
return Binding.DoNothing;
else
return value;
}
catch { return value; }
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
return value;
}
}
}
I had to change the XAML to this
<TextBox.Text>
<Binding
Path="PreHeight"
ValidatesOnExceptions="False"
NotifyOnValidationError="False"
ValidatesOnNotifyDataErrors="False"
UpdateSourceTrigger="PropertyChanged"
Mode="TwoWay"
>
<Binding.ValidationRules>
<validationrules:PreHeightValidationRule ViewModelType="GotoPositionViewModel" Min="0" Max="100" ValidationStep="RawProposedValue"/>
</Binding.ValidationRules>
<Binding.Converter>
<converters:HasErrorToBindingDoNothingConverter ViewModelType="GotoPositionViewModel"/>
</Binding.Converter>
</Binding>
</TextBox.Text>
</TextBox>
IMO this is a great solution well worth to keep in mind.
Pros
Upvotes: 1