Подтвердить что ты не робот

Состояние передачи WPF ValidationRule для просмотра модели в MVVM

Я застрял в, казалось бы, обычном требовании. У меня есть приложение WPF Prism (для MVVM). Моя модель реализует IDataErrorInfo для проверки. IDataErrorInfo отлично работает для нечисловых свойств. Однако для числовых свойств, если пользователь вводит недопустимые символы (которые не являются числовыми), тогда данные даже не достигают модели, потому что wpf не может преобразовать ее в числовой тип.

Итак, я должен был использовать WPF ValidationRule для предоставления пользователю значимого сообщения для недопустимых числовых записей. Все кнопки в представлении привязаны к DelegateCommand призмы (в модели просмотра), а включение/отключение кнопок выполняется в самой модели просмотра.

Теперь, если wpf ValidationRule не работает для некоторого TextBox, как передать эту информацию в View Model, чтобы она могла соответствующим образом отключить кнопки в представлении?

4b9b3361

Ответ 1

Нирван

Самый простой способ решить эту проблему - использовать числовое текстовое поле, которое запрещает пользователю вводить недопустимое значение (вы можете сделать это через стороннего поставщика или найти открытый исходное решение, например класс, полученный из текстового поля, который подавляет не числовой ввод).

Второй способ справиться с этим в MVVM без выполнения вышеизложенного - определить другое поле в вашем представлении ViewModel, которое является строкой, и привязать это поле к вашему текстовому полю. Затем в установщике вашего строкового поля вы можете установить Integer и присвоить значение вашему числовому полю:

Вот пример: (ПРИМЕЧАНИЕ, я не тестировал его, но он должен дать вам идею)

// original field
private int _age;
int Age 
{
   get { return _age; }
   set { 
     _age = value; 
     RaisePropertyChanged("Age");
   }
}


private string _ageStr;
string AgeStr
{
   get { return _ageStr; }
   set { 
     _ageStr = value; 
     RaisePropertyChanged("AgeStr");
     if (!String.IsNullOrEmpty(AgeStr) && IsNumeric(AgeStr) )
         Age = intVal;
    }
} 

private bool IsNumeric(string numStr)
{
   int intVal;
   return int.TryParse(AgeStr, out intVal);
}

#region IDataErrorInfo Members

    public string this[string columnName]
    {
        get
        {

            if (columnName == "AgeStr" && !IsNumeric(AgeStr)
               return "Age must be numeric";
        }
    }

    #endregion

Ответ 2

Для MVVM я предпочитаю использовать Attached Properties для этого типа вещей, потому что они многократно используются и сохраняют чистоту моделей.

Чтобы привязать свойство Validation.HasError к вашей модели представления, вам необходимо создать прикрепленное свойство, которое имеет CoerceValueCallback, которое синхронизирует значение вашего вложенного свойства с свойством Validation.HasError на элементе управления, который вы проверяете на ввод пользователя на.

В этой статье описывается, как использовать эту технику для решения проблемы уведомления модели представления ошибок ValidationRule WPF. Код был в VB, поэтому я перенес его на С#, если вы не являетесь человеком VB.

Прикрепленное свойство

public static class ValidationBehavior
{
    #region Attached Properties

    public static readonly DependencyProperty HasErrorProperty = DependencyProperty.RegisterAttached(
        "HasError",
        typeof(bool),
        typeof(ValidationBehavior),
        new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, CoerceHasError));

    private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached(
        "HasErrorDescriptor",
        typeof(DependencyPropertyDescriptor),
        typeof(ValidationBehavior));

    #endregion

    private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
    {
        return (DependencyPropertyDescriptor)d.GetValue(HasErrorDescriptorProperty);
    }

    private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
    {
        d.SetValue(HasErrorDescriptorProperty, value);
    }

    #region Attached Property Getters and setters

    public static bool GetHasError(DependencyObject d)
    {
        return (bool)d.GetValue(HasErrorProperty);
    }

    public static void SetHasError(DependencyObject d, bool value)
    {
        d.SetValue(HasErrorProperty, value);
    }

    #endregion

    #region CallBacks

    private static object CoerceHasError(DependencyObject d, object baseValue)
    {
        var result = (bool)baseValue;
        if (BindingOperations.IsDataBound(d, HasErrorProperty))
        {
            if (GetHasErrorDescriptor(d) == null)
            {
                var desc = DependencyPropertyDescriptor.FromProperty(System.Windows.Controls.Validation.HasErrorProperty, d.GetType());
                desc.AddValueChanged(d, OnHasErrorChanged);
                SetHasErrorDescriptor(d, desc);
                result = System.Windows.Controls.Validation.GetHasError(d);
            }
        }
        else
        {
            if (GetHasErrorDescriptor(d) != null)
            {
                var desc = GetHasErrorDescriptor(d);
                desc.RemoveValueChanged(d, OnHasErrorChanged);
                SetHasErrorDescriptor(d, null);
            }
        }
        return result;
    }
    private static void OnHasErrorChanged(object sender, EventArgs e)
    {
        var d = sender as DependencyObject;
        if (d != null)
        {
            d.SetValue(HasErrorProperty, d.GetValue(System.Windows.Controls.Validation.HasErrorProperty));
        }
    }

    #endregion
}

Использование прикрепленного свойства в XAML

<Window
  x:Class="MySolution.MyProject.MainWindow"
  xmlns:v="clr-namespace:MyNamespace;assembly=MyAssembly">  
    <TextBox
      v:ValidationBehavior.HasError="{Binding MyPropertyOnMyViewModel}">
      <TextBox.Text>
        <Binding
          Path="ValidationText"
          UpdateSourceTrigger="PropertyChanged">
          <Binding.ValidationRules>
            <v:SomeValidationRuleInMyNamespace/>
          </Binding.ValidationRules>
        </Binding>
     </TextBox.Text>
  </TextBox>
</ Window >

Теперь свойство на вашей модели просмотра будет синхронизировано с Validation.HasError в вашем текстовом поле.

Ответ 3

Начиная с .NET 4.5, ValidationRule имеет перегрузку метода Validate:

public ValidationResult Validate(object value, CultureInfo cultureInfo,
    BindingExpressionBase owner)

Вы можете переопределить его и получить модель представления следующим образом:

public override ValidationResult Validate(object value, 
    CultureInfo cultureInfo, BindingExpressionBase owner)
{
    ValidationResult result = base.Validate(value, cultureInfo, owner);
    var vm = (YourViewModel)((BindingExpression)owner).DataItem;
    // ...
    return result;
}

Ответ 4

Вы должны указать свойство зависимости типа привязки пользователя custome. Например, если ваше свойство является типом int, вы должны установить элемент управления, который не допускает другого значения, кроме типа intenger.

Логику, которую вы можете поместить в PreviewTextInput = "NumberValidationTextBox".

private void NumberValidationTextBox(object sender, TextCompositionEventArgs e)
    { 
        Regex regex = new Regex("[^0-9]+");
        e.Handled = regex.IsMatch(e.Text);
    }

просто вставьте свою логику или установите элемент управления custome, и все готово.

Непременно нужно также выполнить проверку mvvm.

Ответ 5

У меня с вами такая же проблема, но я решаю по-другому, я использую Триггеры, чтобы отключить кнопку, когда вход недействителен. Между тем, привязка текстового поля должна использовать ValidatesOnExceptions=true

<Style TargetType="{x:Type Button}">
<Style.Triggers>
    <DataTrigger Binding="{Binding ElementName=tbInput1, Path=(Validation.HasError)}" Value="True">
        <Setter Property="IsEnabled" Value="False"></Setter>
    </DataTrigger>

    <DataTrigger Binding="{Binding ElementName=tbInput2, Path=(Validation.HasError)}" Value="True">
        <Setter Property="IsEnabled" Value="False"></Setter>
    </DataTrigger>
</Style.Triggers>

Ответ 6

Если вы предоставляете пользовательскую реализацию ValidationRule, вы можете сохранить полученное значение, а также сохранить последний результат. Псевдокод:

public class IsInteger : ValidationRule
{
  private int parsedValue;

  public IsInteger() { }

  public string LastValue{ get; private set; }

  public bool LastParseSuccesfull{ get; private set; }

  public int ParsedValue{ get{ return parsedValue; } }

  public override ValidationResult Validate( object value, CultureInfo cultureInfo )
  {
    LastValue = (string) value;
    LastParseSuccesfull = Int32.TryParse( LastValue, cultureInfo, ref parsedValue );
    return new ValidationResult( LastParseSuccesfull, LastParseSuccesfull ? "not a valid number" : null );
  }
}

Ответ 8

  • Внедрите IDataErrorInfo в вашей модели или в режиме просмотра в зависимости от модели привязки. Вы можете реализовать в обоих классах.

  • Внедрите это также в свой базовый класс проверки. Здесь валидация вызывается, когда привязка IDataErrorInfo не работает.

    public virtual bool HasError
    {
        get { return _hasError; } 
        set
        {
            // if (value.Equals(_hasError)) return;
            _hasError = value;
            RaisePropertyChanged(() => HasError);
        }
    }
    
  • Затем добавьте глобальный класс

    public class ProtocolSettingsLayout
    {
        public static readonly DependencyProperty MVVMHasErrorProperty = DependencyProperty.RegisterAttached("MVVMHasError", typeof(bool), typeof(ProtocolSettingsLayout), new FrameworkPropertyMetadata(false, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, null, CoerceMVVMHasError));
    
        public static bool GetMVVMHasError(DependencyObject d)
        {
            return (bool)d.GetValue(MVVMHasErrorProperty);
        }
    
        public static void SetMVVMHasError(DependencyObject d, bool value)
        {
            d.SetValue(MVVMHasErrorProperty, value);
        }
    
        private static object CoerceMVVMHasError(DependencyObject d, Object baseValue)
        {
            bool ret = (bool)baseValue;
    
            if (BindingOperations.IsDataBound(d, MVVMHasErrorProperty))
            {
                if (GetHasErrorDescriptor(d) == null)
                {
                    DependencyPropertyDescriptor desc = DependencyPropertyDescriptor.FromProperty(Validation.HasErrorProperty, d.GetType());
                    desc.AddValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, desc);
                    ret = System.Windows.Controls.Validation.GetHasError(d);
                }
            }
            else
            {
                if (GetHasErrorDescriptor(d) != null)
                {
                    DependencyPropertyDescriptor desc = GetHasErrorDescriptor(d);
                    desc.RemoveValueChanged(d, OnHasErrorChanged);
                    SetHasErrorDescriptor(d, null);
                }
            }
            return ret;
        }
    
        private static readonly DependencyProperty HasErrorDescriptorProperty = DependencyProperty.RegisterAttached("HasErrorDescriptor",
                                                                                typeof(DependencyPropertyDescriptor),
                                                                                typeof(ProtocolSettingsLayout));
    
        private static DependencyPropertyDescriptor GetHasErrorDescriptor(DependencyObject d)
        {
            var ret = d.GetValue(HasErrorDescriptorProperty);
            return ret as DependencyPropertyDescriptor;
        }
    
        private static void OnHasErrorChanged(object sender, EventArgs e)
        {
            DependencyObject d = sender as DependencyObject;
    
            if (d != null)
            {
                d.SetValue(MVVMHasErrorProperty, d.GetValue(Validation.HasErrorProperty));
            }
        }
    
        private static void SetHasErrorDescriptor(DependencyObject d, DependencyPropertyDescriptor value)
        {
            var ret = d.GetValue(HasErrorDescriptorProperty);
            d.SetValue(HasErrorDescriptorProperty, value);
        }
    }
    
  • xaml

    <TextBox  PreviewTextInput="NumValidationTextBox" Text="{Binding ESec, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged, ValidatesOnDataErrors=true, NotifyOnValidationError=true, ValidatesOnExceptions=True, NotifyOnSourceUpdated=True, NotifyOnTargetUpdated=True, TargetNullValue='0', FallbackValue='0' }" Validation.ErrorTemplate="{StaticResource validationTemplate}" viewmodels:ProtocolSettingsLayout.MVVMHasError="{Binding Path=HasError}" />    
    

Ответ 9

Я столкнулся с той же проблемой и решил ее с помощью трюка. См. Преобразователь ниже:

public class IntValidationConverter : IValueConverter
{
    static string[] AllValuse = new string[100000];
    static int index = 1;
    public static int StartOfErrorCodeIndex = -2000000000;
    public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return null;
        if (value.ToString() == "") return null;

        int iValue = (int)(value);

        if (iValue == int.MinValue) return null;

        if (iValue >= StartOfErrorCodeIndex) return value;
        if ((iValue < IntValidationConverter.StartOfErrorCodeIndex) && (iValue > int.MinValue)) return AllValuse[StartOfErrorCodeIndex - iValue];

        return null;
    }

    public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
    {
        if (value == null) return int.MinValue;
        if (value.ToString() == "") return int.MinValue;

        int result;
        bool success = int.TryParse(value.ToString(), out result);
        if (success) return result;

        index++;
        AllValuse[index] = value.ToString();
        return StartOfErrorCodeIndex - index;
    }
}