WPF ValidationRule предотвращает установку последнего значения

Я использую правило проверки для TextBox для проверки строки ввода пользователя. Текст привязывается к свойству с плавающей запятой в модели представления, и механизм привязки WPF достаточно умен, чтобы автоматически преобразовать строку в число с плавающей запятой для меня.

Однако, когда проверка не удалась, привязка, кажется, считывает старое значение. Это приводит к тому, что я получаю красную рамку вокруг текстового поля, хотя текст вернулся к последнему приемлемому значению с плавающей запятой.

Вопрос. Как убедиться, что ошибочный вводимый текст не перезаписывается механизмом привязки автоматически при сбое проверки? Привязка должна быть двусторонней.

Я должен упомянуть, что я делаю небольшой трюк в своем ValidationRule, где я позволяю ему находить текущую модель представления из локатора модели представления и использую подход INotifyDataErrorInfo к модели представления. Я нашел это отличным решением, так как это означает, что ViewModel HasError соберет для меня все ошибки проверки (и это позволит мне применить проверку в правилах проверки или в модели представления при установке свойства). Преимущество разрешения проверки Правило применения проверки с использованием INotifyDataErrorInfo в модели представления заключается в том, что проверка может быть применена до автоматического преобразования строки в число с плавающей запятой, обеспечивая выполнение проверки даже тогда, когда пользователь вводит «Hello World», что приводит к исключению (проглоченному механизм привязки) во время автоматического преобразования в число с плавающей запятой. Это позволяет мне сохранить тип плавающего свойства на виртуальной машине и при этом выполнить проверку.

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>

Правило проверки

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

person Johan    schedule 26.09.2018    source источник
comment
@ mm8 пожалуйста, смотрите мой ответ ниже. Мне удалось это решить. Стоит взглянуть на этот подход. Он имеет достаточное количество плюсов по сравнению с использованием ValidationRule обычным способом или использованием только проверки в установщиках свойств виртуальной машины, и позволяет свойствам виртуальной машины быть запрошенного типа (нет необходимости использовать дополнительный слой строк для принудительной проверки иметь место также для ввода, который вызовет исключение привязки)   -  person Johan    schedule 26.09.2018


Ответы (1)


Мне удалось ее решить! Я нашел подсказку от Джоша, которая привлекла мое внимание в правильном направлении.

С помощью конвертера можно установить Binding.DoNothing. Я преобразовал его в преобразователь, который проверяет 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>

ИМО, это отличное решение, о котором стоит помнить.

Плюсы

  • Проверка выполняется на виртуальной машине с использованием INotifyDataErrorInfo.
  • Элементы представления могут напрямую связываться с INotifyDataErrorInfo HasError.
  • Поддержка нескольких ValdiationResult (отказов)/свойства.
  • Поддерживает перекрестную проверку свойств.
  • Проверка может быть выполнена с помощью ValidationRule для RawProposedValue (строка), нет необходимости добавлять дополнительный уровень строк в виртуальной машине.
  • Когда нет необходимости выполнять проверку RawProposedValue, можно выполнить проверку в установщике свойств в ViewModel.
  • Последний пункт подразумевает, что проверка может быть выполнена до того, как автоматическое преобразование (в данном случае из строки в число с плавающей запятой) завершится ошибкой с исключением, перехваченным механизмом привязки WPF, что обычно предотвращает выполнение проверки и предотвращает обновление привязки элементов к HasError. их состояние.
  • Неверное значение (в данном случае строка) не будет перезаписано в представлении при сбое проверки.
person Johan    schedule 26.09.2018