How to validate two properties which depend on each other? - c#

I have view model with 2 properties: A and B and I want to validate that A < B.
Below is my simplified implementation where I use custom validation rule. Since each property is validated independently, it lead to an anoying issue: if entered A value is invalid, than it stay so even after changing B, since validation of B doesn't know anything about A.
This can be seen on this demo:
A is invalid after entering 11, that's correct since 11 > 2. Changing B to 22 doesn't re-evalute A, I have to edit A to have validation passed.
What I want? I want that after enering 22 into B the red border (validation error) disappears and A = 11, B = 22 would be source values in view model.
How can I in B validation somehow force A validation after new B value is synchronized with source?
View model:
public class ViewModel : INotifyPropertyChanged
{
int _a;
public int A
{
get => _a;
set
{
_a = value;
OnPropertyChanged();
}
}
int _b;
public int B
{
get => _b;
set
{
_b = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
public virtual void OnPropertyChanged([CallerMemberName] string property = "") =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
View:
<StackPanel>
<TextBox Margin="10" Text="{local:MyBinding A}" />
<TextBox Margin="10" Text="{local:MyBinding B}" />
</StackPanel>
View code:
public MainWindow()
{
InitializeComponent();
DataContext = new ViewModel { A = 1, B = 2 };
}
Binding:
public class MyBinding : Binding
{
public MyBinding(string path) : base(path)
{
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged;
ValidationRules.Add(new MyValidationRule());
}
}
Validation rule:
public class MyValidationRule : ValidationRule
{
public MyValidationRule() : base(ValidationStep.ConvertedProposedValue, false) { }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) => ValidationResult.ValidResult; // not used
public override ValidationResult Validate(object value, CultureInfo cultureInfo, BindingExpressionBase owner)
{
var binding = owner as BindingExpression;
var vm = binding?.DataItem as ViewModel;
switch (binding.ResolvedSourcePropertyName)
{
case nameof(vm.A):
if ((int)value >= vm.B)
return new ValidationResult(false, "A should be smaller than B");
break;
case nameof(vm.B):
if ((int)value <= vm.A)
return new ValidationResult(false, "B should be bigger than A");
break;
}
return base.Validate(value, cultureInfo, owner);
}
}

ValidationRules don't support invalidating a property when setting another property.
What you should do is to implement INotifyDataErrorInfo in your view model and raise the ErrorsChanged event whenever you want to refresh the validation status for a property.
There is an example available in the this TechNet article.

Related

How to determine if there are no errors anymore in Validation.ErrorEvent?

I've very simple check if there are validation error somewhere in my window (assuming what all bindings will have NotifyOnValidationError set):
public MainWindow()
{
InitializeComponent();
DataContext = new VM();
AddHandler(Validation.ErrorEvent, new RoutedEventHandler((s, e) =>
{
var args = (ValidationErrorEventArgs)e;
var binding = (BindingExpression)args.Error.BindingInError;
Title = binding.HasError ? $"Error {args.Error.ErrorContent}" : "";
}), true);
}
The event is rised when errors appear/disappear, but for some reasons HasError still true when there are no more errors and ErrorContent contains old error text.
What am I doing wrong?
Below is a simple MCVE with validation that Test should be 0.
Binding errors (entering 0a or empty string) are set/reset correctly. Validation error is set correctly (when entering 1), but is not reset (when entering 0). Why?
Implementing INotifyPropertyChange makes no difference.
xaml:
<TextBox Text="{Binding Test, NotifyOnValidationError=True, UpdateSourceTrigger=PropertyChanged}" />
View Model:
public class VM : INotifyDataErrorInfo
{
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
int _test;
public int Test
{
get => _test;
set
{
_test = value;
_error = value == 0 ? null : "Must be 0";
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(nameof(Test)));
}
}
string _error;
public bool HasErrors => _error != null;
public IEnumerable GetErrors(string propertyName)
{
if (_error != null)
yield return _error;
}
}
If you set the Title from within the handler, I think you need to consider the
ValidationErrorEventArgs.Action Property:
Gets a value that indicates whether the error is a new error or an existing error that has now been cleared.
I am not 100% sure, but I suspect that by the time you check the HasError Property, it has not yet been cleared. (Suspicion based on "Also note that a valid value transfer in either direction (target-to-source or source-to-target) clears the Validation.Errorsattached property." from MSDN)

Stack OverFlow with Custom validator

I have implemented custom validator as following...
public class RquiredFiledValidation:ValidationRule
{
public string ErrorMessage { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (string.IsNullOrWhiteSpace(value.ToString()))
return new ValidationResult(false, ErrorMessage);
else
return new ValidationResult(true, null);
}
}
And Attached this with a text box as following...
<TextBox x:Name="txtLoging" Grid.Column="1" HorizontalAlignment="Stretch" Validation.ErrorTemplate="{x:Null}" VerticalAlignment="Center" Margin="0,40,30,0">
<Binding Path="Text" ElementName="txtLoging" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True">
<Binding.ValidationRules>
<Validate:RquiredFiledValidation ErrorMessage="Please Provide Login Name"></Validate:RquiredFiledValidation>
</Binding.ValidationRules>
</Binding>
</TextBox>
My problem is...
1) When I click directly on login button then the validation doesn't get fired
2) When I put a character in text box validation get fired but produced stack overflow error.
I have solve the first problem from code behind as below txtLoging.GetBindingExpression(TextBox.TextProperty).UpdateS‌​ource(); txtPassword.GetBindingExpression(Infrastructure.AttachedProp‌​erty.PasswordAssiste‌​nt.PasswordValue).Up‌​dateSource(); But how solve the same in MVVM
If you care about the MVVM pattern you should not validate your data using validation rules. Validation rules belong to the view and in an MVVM application the validation logic should be implemented in the view model or the model class.
What you should do is implement the INotifyDataErrorInfo interface: https://msdn.microsoft.com/en-us/library/system.componentmodel.inotifydataerrorinfo%28v=vs.110%29.aspx
Here is an example for you:
public class ViewModel : INotifyDataErrorInfo
{
private string _username;
public string Username
{
get { return _username; }
set
{
_username = value;
ValidateUsername();
}
}
private void ValidateUsername()
{
if (_username == "valid")
{
if (_validationErrors.ContainsKey("Username"))
_validationErrors.Remove(nameof(Username));
}
else if (!_validationErrors.ContainsKey("Username"))
{
_validationErrors.Add("Username", new List<string> { "Invalid username" });
}
RaiseErrorsChanged("Username");
}
private readonly Dictionary<string, ICollection<string>>
_validationErrors = new Dictionary<string, ICollection<string>>();
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
private void RaiseErrorsChanged(string propertyName)
{
if (ErrorsChanged != null)
ErrorsChanged(this, new DataErrorsChangedEventArgs(propertyName));
}
public System.Collections.IEnumerable GetErrors(string propertyName)
{
if (string.IsNullOrEmpty(propertyName)
|| !_validationErrors.ContainsKey(propertyName))
return null;
return _validationErrors[propertyName];
}
public bool HasErrors
{
get { return _validationErrors.Count > 0; }
}
}
<TextBox Text="{Binding Username, UpdateSourceTrigger=PropertyChanged, ValidatesOnNotifyDataErrors=True}" />
Please refer to the following blog post for more information about the broad picture of how data validation in WPF works and some comprehensive samples on how to implement it.
Data validation in WPF: https://blog.magnusmontin.net/2013/08/26/data-validation-in-wpf/

ValidateProperties code

Hi all i am using mvvmcross and portable class libraries , so i cannot use prism or componentmodel data annotations, to validate my classes. basically i have a modelbase that all my models inherit from.
My validate code below is horribly broken, basically im looking for the code that data annotations uses to iterate thru all the properties on my class that is inheriting the base class ,
i have written various attributes that are there own validators inheriting from "validatorBase" which inherits from attribute. i just cannot for the life of me figure out thecode that says ... ok im a class im going to go through all the properties in me that have an attribute of type ValidatorBase and run the validator. my code for these are at the bottom
public class ModelBase
{
private Dictionary<string, IEnumerable<string>> _errors;
public Dictionary<string, IEnumerable<string>> Errors
{
get
{
return _errors;
}
}
protected virtual bool Validate()
{
var propertiesWithChangedErrors = new List<string>();
// Get all the properties decorated with the ValidationAttribute attribute.
var propertiesToValidate = this.GetType().GetRuntimeProperties()
.Where(c => c.GetCustomAttributes(typeof(ValidatorBase)).Any());
foreach (PropertyInfo propertyInfo in propertiesToValidate)
{
var propertyErrors = new List<string>();
TryValidateProperty(propertyInfo, propertyErrors);
// If the errors have changed, save the property name to notify the update at the end of this method.
bool errorsChanged = SetPropertyErrors(propertyInfo.Name, propertyErrors);
if (errorsChanged && !propertiesWithChangedErrors.Contains(propertyInfo.Name))
{
propertiesWithChangedErrors.Add(propertyInfo.Name);
}
}
// Notify each property whose set of errors has changed since the last validation.
foreach (string propertyName in propertiesWithChangedErrors)
{
OnErrorsChanged(propertyName);
OnPropertyChanged(string.Format(CultureInfo.CurrentCulture, "Item[{0}]", propertyName));
}
return _errors.Values.Count == 0;
}
}
here is my validator
public class BooleanRequired : ValidatorBase
{
public override bool Validate(object value)
{
bool retVal = true;
retVal = value != null && (bool)value == true;
var t = this.ErrorMessage;
if (!retVal)
{
ErrorMessage = "Accept is Required";
}
return retVal;
}
}
and here is an example of its usage
[Required(ErrorMessage = "Please enter the Amount")]
public decimal Amount
{
get { return _amount; }
set { _amount = value; }//SetProperty(ref _amount, value); }
}

WPF MVVM Validation Using IDataErrorInfo

I have created a Person MVVM model which needs to be validated. I am using IDataErrorInfo class and validating the user. But when the screen loads the textboxes are already red/validated indicating that the field needs to be filled out. I believe this is because I bind the PersonViewModel in the InitializeComponent. I tried to use LostFocus for updatetriggers but that did not do anything.
Here is my PersonViewModel:
public class PersonViewModel : IDataErrorInfo
{
private string _firstName;
private string _lastName;
public string LastName { get; set; }
public string Error
{
get { throw new NotImplementedException(); }
}
public string FirstName
{
get { return _firstName; }
set { _firstName = value; }
}
public string this[string columnName]
{
get
{
string validationResult = String.Empty;
switch(columnName)
{
case "FirstName":
validationResult = ValidateFirstName();
break;
case "LastName":
validationResult = ValidateLastName();
break;
default:
throw new ApplicationException("Unknown property being validated on the Product");
}
return validationResult;
}
}
private string ValidateLastName()
{
return String.IsNullOrEmpty(LastName) ? "Last Name cannot be empty" : String.Empty;
}
private string ValidateFirstName()
{
return String.IsNullOrEmpty(FirstName) ? "First Name cannot be empty" : String.Empty;
}
}
Here is the XAML:
<StackPanel>
<TextBlock>First Name</TextBlock>
<TextBox Text="{Binding FirstName, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" Background="Gray"></TextBox>
<TextBlock>Last Name</TextBlock>
<TextBox Text="{Binding LastName, ValidatesOnDataErrors=True, UpdateSourceTrigger=LostFocus}" Background="Gray"></TextBox>
</StackPanel>
MainWindow.cs:
public MainWindow()
{
InitializeComponent();
_personViewModel = new PersonViewModel();
this.DataContext = _personViewModel;
}
Am I missing something? I do not want the validation to be fired when the screen loads. I only want it to be fired when the user looses the focus of the textboxes.
Rather than fight the tide of how WPF works by default, consider redefining the UI so that the error display 'fits' the scenario of screen load as well as data entry error. Besides, a user should have some hints on a blank form of what is needed.
Create a method to do your validation, and store the validation results in a dictionary:
private Dictionary<string, string> _validationErrors = new Dictionary<string, string>();
public void Validate(string propertyName)
{
string validationResult = null;
switch(propertyName)
{
case "FirstName":
validationResult = ValidateFirstName();
break;
}
//etc.
}
//Clear dictionary properly here instead (You must also handle when a value becomes valid again)
_validationResults[propertyName] = validationResult;
//Note that in order for WPF to catch this update, you may need to raise the PropertyChanged event if you aren't doing so in the constructor (AFTER validating)
}
Then update your ViewModel to:
Have the indexer return a result from the _validationErrors instead, if present.
Call Validate() in your setters.
Optionally, in Validate(), if the propertyName is null, validate all properties.
WPF will call the indexer to display errors, and since you are returning something, it will think that there are errors. It won't unless you explictly call Validate() with this solution.
EDIT: Please also note that there is now a more efficient way of implementing validation in .NET 4.5 called INotifyDataErrorInfo.

CustomValidation attribute doesn't seem to work

I have a simple test page in my Silverlight 4 application in which I'm trying to get a custom validation rule to fire.
I have a TextBox and a Button, and I am showing the validation results in a TextBlock. My view model has a Name property, which is bound the the Text property of the TextBox. I have two validation attributes on the Name property, [Required] and [CustomValidation].
When I hit the Submit button, the Required validator fires correctly, but the breakpoint inside the validation method of my custom validator never gets hit. I can't see why this is, as I think I have followed MS's example very carefully: http://msdn.microsoft.com/en-us/library/system.componentmodel.dataannotations.customvalidationattribute(v=vs.95).aspx
Here is the code for the view model:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using GalaSoft.MvvmLight.Command;
namespace MyProject
{
// custom validation class
public class StartsCapitalValidator
{
public static ValidationResult IsValid(string value)
{
// this code never gets hit
if (value.Length > 0)
{
var valid = (value[0].ToString() == value[0].ToString().ToUpper());
if (!valid)
return new ValidationResult("Name must start with capital letter");
}
return ValidationResult.Success;
}
}
// my view model
public class ValidationTestViewModel : ViewModelBase
{
// the property to be validated
string _name;
[Required]
[CustomValidation(typeof(StartsCapitalValidator), "IsValid")]
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value, () => Name); }
}
string _result;
public string Result
{
get { return _result; }
private set { SetProperty(ref _result, value, () => Result); }
}
public RelayCommand SubmitCommand { get; private set; }
public ValidationTestViewModel()
{
SubmitCommand = new RelayCommand(Submit);
}
void Submit()
{
// perform validation when the user clicks the Submit button
var errors = new List<ValidationResult>();
if (!Validator.TryValidateObject(this, new ValidationContext(this, null, null), errors))
{
// we only ever get here from the Required validation, never from the CustomValidator
Result = String.Format("{0} error(s):\n{1}",
errors.Count,
String.Join("\n", errors.Select(e => e.ErrorMessage)));
}
else
{
Result = "Valid";
}
}
}
}
Here is the view:
<navigation:Page x:Class="Data.Byldr.Application.Views.ValidationTest"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:navigation="clr-namespace:System.Windows.Controls;assembly=System.Windows.Controls.Navigation">
<Grid Width="400">
<StackPanel>
<TextBox Text="{Binding Name, Mode=TwoWay}" />
<Button Command="{Binding SubmitCommand}" Content="Submit" />
<TextBlock Text="{Binding Result}" />
</StackPanel>
</Grid>
</navigation:Page>
Why don't you create your own Validation attribute like this..
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
public class StartsCapital : ValidationAttribute
{
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
var text = value as string;
if(text == null)
return ValidationResult.Success;
if (text.Length > 0)
{
var valid = (text[0].ToString() == text[0].ToString().ToUpper());
if (!valid)
return new ValidationResult("Name must start with capital letter");
}
return ValidationResult.Success;
}
}
And then use it like
// my view model
public class ValidationTestViewModel : ViewModelBase
{
// the property to be validated
string _name;
[Required]
[StartsCapital]
public string Name
{
get { return _name; }
set { SetProperty(ref _name, value, () => Name); }
}
As stated on the MSDN page for that overload of Validator.TryValidateObject ( http://msdn.microsoft.com/en-us/library/dd411803(v=VS.95).aspx ), only the object-level validations are checked with this method, and RequiredAttribute on properties.
To check property-level validations, use the overload that also takes a bool ( http://msdn.microsoft.com/en-us/library/dd411772(v=VS.95).aspx )
So it should be as simple as passing "true" as an extra parameter to TryValidateObject

Categories

Resources