WPF ValidationRule阻止设置最后一个值

时间:2018-09-26 09:28:03

标签: c# .net wpf mvvm-light validationrules

我在TextBox上使用验证规则来验证用户输入字符串。 文本绑定到视图模型上的float属性,WPF绑定引擎非常热衷,可以自动将字符串转换为float。

但是,当验证失败时,绑定似乎会回读旧值。这导致即使文本已恢复为最后一个可接受的浮点值,我在文本框周围仍显示红色边框。

问题:如何确保验证失败时绑定引擎不会自动覆盖错误的输入文本? 绑定需要双向。

我应该提到,我在ValidationRule中做了一些小技巧,让我从视图模型定位器中查找当前视图模型,并在视图模型上使用INotifyDataErrorInfo方法。我发现这是一个很好的解决方案,因为这意味着ViewModel HasError将为我收集所有验证错误(并且在设置属性时让我在验证规则或视图模型中应用验证)。在视图模型上使用INotifyDataErrorInfo进行验证的规则是,验证可以在从字符串到浮点的自动转换之前应用,即使在用户键入“ Hello World”导致异常时,也要确保执行验证自动转换为浮动期间的绑定引擎)。这使我可以将属性的类型保留在vm上,并仍然执行验证。

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

1 个答案:

答案 0 :(得分:0)

我设法解决了! 我从乔希(Josh)找到了提示,使我的注意力转向了正确的方向。

使用转换器可以设置Binding.DoNothing。我将其修改为一个转换器,用于检查VM上的HasError。如果出现HasError,我将返回Binding.DoNothing,否则我将转发该值。

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;
    }
}
}

我必须将XAML更改为此

  <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这是一个非常值得记住的伟大解决方案。

专业人士

  • 使用INotifyDataErrorInfo在VM上进行验证
  • 视图元素可以直接绑定到INotifyDataErrorInfo HasError。
  • 支持多个ValdiationResult(失败)/属性。
  • 支持交叉属性验证。
  • 可以使用RawProposedValue(字符串)上的ValidationRule完成验证,而无需在VM中添加额外的字符串层。
  • 当不需要对RawProposedValue执行验证时,可以在ViewModel的属性设置器中进行验证。
  • 最后一点表示,在WPF绑定引擎捕获到异常的情况下,可以在自动转换(在这种情况下从字符串转换为浮点)失败之前执行验证,这通常会阻止验证不执行并阻止元素绑定到HasError不更新其状态。
  • 验证失败时,错误的值(在这种情况下为字符串)不会在视图中被覆盖。