20

I am stuck in a seemingly common requirement. I have a WPF Prism (for MVVM) application. My model implements the IDataErrorInfo for validation. The IDataErrorInfo works great for non-numeric properties. However, for numeric properties, if the user enters invalid characters (which are not numeric), then the data doesn't even reach the model because wpf cannot convert it to numeric type.

So, I had to use WPF ValidationRule to provide user some meaningful message for invalid numeric entries. All the buttons in the view are bound to DelegateCommand of prism (in view model) and the enabling/disabling of buttons is done in View Model itself.

Now if a wpf ValidationRule fail for some TextBox, how do I pass this information to View Model so that it can appropriately disable buttons in the view ?

John Saunders
  • 160,644
  • 26
  • 247
  • 397
Jatin
  • 4,023
  • 10
  • 60
  • 107

9 Answers9

15

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.

Hugh W
  • 716
  • 2
  • 10
  • 33
Farrah Stark
  • 580
  • 5
  • 15
  • 1
    Works great for basic in-line XAML, but doesn't seem to work for data template elements. The coerce value callback is called initially, but before the data binding seems to be set up (i.e. `IsDataBound()` returns `false`). I can remove the check for `IsDataBound()` and it works, but then I risk a memory leak. Any idea how to get this approach to work in the data template scenario? – Peter Duniho Jun 09 '16 at 07:13
8

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;
}
Maxence
  • 12,868
  • 5
  • 57
  • 69
  • 2
    Maybe I'm missing something, but how does this get called from the view? Setting the validation rule for the binding defaults to using the abstract Validate method, as far as I can tell. – Chris Bush Sep 19 '17 at 20:16
  • The abstract `Validate` method is called by the virtual one. The source code is here: https://referencesource.microsoft.com/#PresentationFramework/Framework/System/Windows/Controls/ValidationRule.cs,8d9199b8be6d367d – Maxence Sep 22 '17 at 13:01
  • 3
    @Maxence this link is not available anymore. I have the same question as Chris. Great potential since the ValidationRule can have the VM performing the validation using the INotifyDataErrorInfo. Please explain in more detail how to force the overload Validate() to run? The benefit with using a ValidationRule over just binding is that one can keep the VM property type (example Integer), and have the ValidationRule applied on the view control type (example string). One could perform validation before exceptions are swallowed by the binding engine so HasError property on the VM gets updated. – Johan Sep 19 '18 at 14:16
7

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
Hugh W
  • 716
  • 2
  • 10
  • 33
Alex Blokh
  • 131
  • 3
  • Alex, Thanks for your answer. I am using the second way suggested by you. Although, for the current issue, this approach is sufficient, I was looking for a generic solution to use ValidationRule in combination with IDateErrorInfo. I came across a problem while solving that and is posted here (http://stackoverflow.com/questions/10629278/strange-order-of-firing-of-validation-error-event-added-fired-before-removed). I would be very grateful for your advice on that issue. Since my current issue is resolved, I will mark this answer of yours as correct one. Cheers. Nirvan – Jatin May 23 '12 at 11:56
  • 1
    I would strongly advise against using Validation rules. They are on the binding level, and were designed to be used in code behind, and are not a good fit for MVVM. You might be able to get them to work together, but that would require connecting your ViewModel to the View and navigating through the logical tree checking bindings and tying it back to ViewModel. I shudder when I think about this. Take a look at this: http://stackoverflow.com/questions/63646/wpf-data-binding-and-validation-rules-best-practices – Alex Blokh May 23 '12 at 21:31
  • For myself, I would always try to follow the path of least resistance, and simply use a control that does not involve invalid input that cannot make it to the ViewModel. I want all the validation code to be in ViewModel, and hence in one place, and under my control. – Alex Blokh May 23 '12 at 21:41
  • Alex, indeed Validation Rules do not fit nicely with MVVM. I think I will follow your suggestion and avoid it altogether. Thanks very much for your advice. – Jatin May 24 '12 at 14:06
3

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>

zzczzc004
  • 131
  • 5
1

You must specify custome user control depending bind type property. For example if your property is int type you must place control that not allow diferent value except intenger type.

The logic you may put in PreviewTextInput="NumberValidationTextBox".

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

just insert your logic or place custome control and you are done.

Defently must implement mvvm validation too.

Pitka
  • 525
  • 5
  • 12
1
  1. Implement IDataErrorInfo in your model or Viewmodel depending logic of binding property. You may implement in both classes.

  2. Implement this too in your base validation class. Here validation will trigger when binding IDataErrorInfo does not work.

    public virtual bool HasError
    {
        get { return _hasError; } 
        set
        {
            // if (value.Equals(_hasError)) return;
            _hasError = value;
            RaisePropertyChanged(() => HasError);
        }
    }
    
  3. Next, add global class

    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);
        }
    }
    
  4. 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}" />    
    
Arnaud
  • 7,259
  • 10
  • 50
  • 71
Pitka
  • 525
  • 5
  • 12
  • 100% it is work. I have test it. Not need to implement validation rules or custome controls. – Pitka Apr 09 '15 at 11:32
0

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 );
  }
}
stijn
  • 34,664
  • 13
  • 111
  • 163
  • 1
    I am not sure if I fully understand what you are trying to explain. My problem is that the invalid value (for a property) never reaches the Model, so I can't use IDataErrorInfo to validate the property. For example suppose there is an textbox for age property. First user enters valid age (say 20). Then later he changes the age textbox to something non numeric (say abc). Now this incorrect value never reaches Model although the ValidationRule is able to detect the same. Model's Age is still the old valid value (20). All by buttons are bound to ViewModel and I can disable them only from there. – Jatin May 15 '12 at 13:01
0

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/

Nix
  • 321
  • 8
  • 20
0

I encountered the same problem and solved it with a trick. See the converter below:

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;
    }
}
Shilo
  • 1
  • 1