Passing state of WPF ValidationRule to View Model in MVVM

前端 未结 9 1713
难免孤独
难免孤独 2021-01-01 20:10

I am stuck in a seemingly common requirement. I have a WPF Prism (for MVVM) application. My model implements the IDataErrorInfo for validation. The

相关标签:
9条回答
  • 2021-01-01 20:37

    For MVVM I prefer to use Attached Properties for this type of thing because they are reusable and it keeps the view models clean.

    In order to bind to the Validation.HasError property to your view model you have to create an attached property which has a CoerceValueCallback that synchronizes the value of your attached property with the Validation.HasError property on the control you are validating user input on.

    This article explains how to use this technique to solve the problem of notifying the view model of WPF ValidationRule errors. The code was in VB so I ported it over to C# if you're not a VB person.

    The Attached Property

    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
    }
    

    Using The Attached Property in 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 >
    

    Now the property on your view model will be synchronized with Validation.HasError on your textbox.

    0 讨论(0)
  • 2021-01-01 20:40

    Someone's solved it here (unfortunately its in VB) by creating a dependency property HasError in the VM which seems to be bound to the Validation.HasError. I don't completely understand it yet but it may help you:

    http://wpfglue.wordpress.com/2009/12/03/forwarding-the-result-of-wpf-validation-in-mvvm/

    0 讨论(0)
  • 2021-01-01 20:41

    If you provide a custom ValidationRule implementation you can store the value it received, as well as storing the last result. PseudoCode:

    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 );
      }
    }
    
    0 讨论(0)
  • 2021-01-01 20:42

    I have the same problem with you, but I solve in another way, I use the Triggers to disable the button when the input is invalid. Meanwhile, the textbox binding should use 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>
    

    0 讨论(0)
  • 2021-01-01 20:44

    Since .NET 4.5, ValidationRule has an overload of the Validate method:

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

    You can override it and get the view model this way:

    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;
    }
    
    0 讨论(0)
  • 2021-01-01 20:51

    Nirvan

    The simplest way to solve this particular issue is to use a numeric textbox, which prevents the user from entering an invalid value (you can either do this via a Third Party Vendor, or find an open source solution, such as a class derived from Textbox that suppresses non numeric input).

    The second way to handle this in MVVM without doing the above, is to define another field in you ViewModel which is a string, and bind that field to your textbox. Then, in the setter of your string field you can set the Integer, and assign a value to your numeric field:

    Here is a rough example: (NOTE I did not test it, but it should give you the idea)

    // 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
    
    0 讨论(0)
提交回复
热议问题