WPF/MVVM Validation - c#

Here's the XAML code representing a TextBox used as input for the IdCard
<TextBox.Text>
<Binding Mode="TwoWay"
Path="IdCardNumber"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<v:AlphaNumValidationRule ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
The validation :
public class AlphaNumValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (string.IsNullOrWhiteSpace((value ?? "").ToString()))
return new ValidationResult(false, Resources.Strings.MessagesResource.RequiredField);
else if (value.ToString().MatchRegex(RegexResource.ALPHANUMERIC))
return new ValidationResult(true, null);
else
return new ValidationResult(false, Resources.Strings.MessagesResource.InvalidFormat);
}
}
The ViewModel
public override bool IsValid
{
get { return !string.IsNullOrWhiteSpace(IdCardNumber); }
}
private string idCardNumber;
public string IdCardNumber
{
get { return idCardNumber; }
set { Set(() => IdCardNumber, ref idCardNumber, value);
RaisePropertyChanged("IsValid");
}
}
What I want to have is to update IsValid everytime the IdCard input is updated , I tried different ValidationStep but none do as I wish.
At first when loading the input for the first time IsValid is false , when typing a correct value it becomes true after deleting input and adding wrong non-supported values IsValid stays the same since it keeps the last correct value.
Any way to solve this ?

There is an attached event Validation.Error that is fired when binding error occurs.
So basically you could attach to this event and set value of Validation.HasErrors property to your viewmodel's IsValid property.
I see a conflict however. You defined your validation logic in the View, but you want to access it in your ViewModel, that's why you are having troubles.
I recommend you to move entire validation logic to your viewmodel by implementing INotifyDataErrorInfo. then you will have all validation rules and validation errors at your disposal in viewmodel.

You can try to change the UpdateSourceTrigger property with LostFocus:
<Binding Mode="TwoWay"
Path="IdCardNumber"
UpdateSourceTrigger="LostFocus">
<Binding.ValidationRules>
<v:AlphaNumValidationRule ValidationStep="UpdatedValue" />
</Binding.ValidationRules>
</Binding>
Edit :
To bind the validation result you can use HasError property :
<TextBox Name="TextBox">
<TextBox.Text>
<Binding Mode="TwoWay"
Path="Text"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:AlphaNumValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBlock Text="{Binding (Validation.HasError), ElementName=TextBox}"/>

Related

Bind custom class with Validation in WPF

I noticed that it is possible to bind variables of the type DateTime to a textbox in WPF. If I enter a wrong value it will not validate and show a red border.
How can I implement my own class, that I can bind to a textbox without having to bind to a property of the class? The Textbox should show a string and the class will validate the input.
Is this possible?
My current solution is this:
In the Model:
public string DefaultLanguageValue
{
get
{
return _defaultLanguageValue;
}
set
{
if (value != this._defaultLanguageValue)
{
ValidateLanguage(value);
this._defaultLanguageValue = value;
NotifyPropertyChanged();
}
}
}
private void ValidateLanguage(string value)
{
string rx = "([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*";
if (!Regex.IsMatch(value, rx))
{
throw new ArgumentException();
}
}
In the XAML:
<TextBox Text="{Binding TreeViewModel.Model.DefaultLanguageValue, UpdateSourceTrigger=PropertyChanged, ValidatesOnExceptions=True}" BorderThickness="0" MinWidth="100"/>
It would be nice to have a Class that I can just bind like a String, Int or DateTime for examlpe. Any Ideas?
You could bind to the Tag property of the TextBox itself and validate using a ValidationRule:
public class DateValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if (!DateTime.TryParse(value as string, out DateTime _))
return new ValidationResult(false, "Invalid date...");
return ValidationResult.ValidResult;
}
}
XAML:
<TextBox>
<TextBox.Text>
<Binding Path="Tag" RelativeSource="{RelativeSource Self}"
UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:DateValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
This doesn't require you to bind to a view model.
I finally tried the solution suggested by mm8.
The only issue I have now is that if one enters an invalid value into the textbox, it will not update the textbox when I programmatically change the value of the source after clicking a button.
I tried Validation after update, but this allows the user to save invalid values.
<TreeViewItem>
<TreeViewItem.Header>
<StackPanel Orientation="Horizontal">
<CheckBox x:Name="chkDefaultLanguage" IsChecked="{Binding TreeViewModel.TreeModel.DefaultLanguage, UpdateSourceTrigger=PropertyChanged}"/>
<TextBlock Text="DefaultLanguage: " />
<TextBox BorderThickness="0" MinWidth="100">
<TextBox.Text>
<Binding Path="TreeViewModel.TreeModel.DefaultLanguageValue" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<validationrules:LanguageCodeValidationRule/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
</StackPanel>
</TreeViewItem.Header>
</TreeViewItem>
class LanguageCodeValidationRule : ValidationRule
{
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string rx = "([a-zA-Z]{2}|[iI]-[a-zA-Z]+|[xX]-[a-zA-Z]{1,8})(-[a-zA-Z]{1,8})*";
if (!Regex.IsMatch(value.ToString(), rx))
{
return new ValidationResult(false, "Invalid Language Codee.");
}
return ValidationResult.ValidResult;
}
}

Set custom error message in ValidationRules in WPF

I have set everything perfectly. If I set some string in ErrorMessage then it shows without error.
What I want is, I want to set ErrorMessage dynamically/programmatically. something
MyValidation.ErrorMessage = "some new message";
username.Update() //something
XAML Code
<TextBox Margin="5" Name="userName">
<TextBox.Text>
<Binding RelativeSource="{RelativeSource Self}" Path="Tag" Mode="OneWayToSource" UpdateSourceTrigger="PropertyChanged">
<Binding.ValidationRules>
<local:MyValidation ErrorMessage="Static String" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
C# Class Code
public class MyValidation : ValidationRule {
public string ErrorMessage { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo) {
if (ErrorMessage.Length > 0) {
return new ValidationResult(false, ErrorMessage);
}
return ValidationResult.ValidResult;
}
}
If you give the ValidationRule a name in the XAML markup:
<Binding.ValidationRules>
<local:MyValidation x:Name="val" ErrorMessage="Static String" />
</Binding.ValidationRules>
...you could set its ErrorMessage property directly and then just explicitly update the binding:
val.ErrorMessage = "some new message";
userName.GetBindingExpression(TextBox.TextProperty)?.UpdateSource();
You can implement INotifyDataErrorInfo in your viewmodel. Implement GetErrors(string) so it returns different error messages based on your condition. You can even return multiple messages at once and they will be displayed below each other.
Here's a nice tutorial, but feel free to implement it on your own. Keep in mind that there's not just one correct approach and the interface gives you a lot of freedom.

NotifyPropertyChanged isn't updating DependencyProperty in CustomControl

I have made a CustomControl, with the DP Threshold, like so:
public class SymbolControl : Control
{
public static readonly DependencyProperty ThresholdProperty = DependencyProperty.Register("Threshold", typeof(IThreshold<SolidColorBrush>), typeof(SymbolControl));
public IThreshold<SolidColorBrush> Threshold
{
get { return (IThreshold<SolidColorBrush>)GetValue(ThresholdProperty); }
set { SetValue(ThresholdProperty, value); }
}
...
}
Here is where the Property is used in the xaml of the custom control:
...
<Border.Background>
<MultiBinding Converter="{StaticResource ThreshholdToReturnValueConverter}" NotifyOnTargetUpdated="True" >
<Binding Path="Threshold" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="SymbolValue" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="DefaultBackground" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
</MultiBinding>
</Border.Background>
...
And here is How the CustomControl is used:
<controls:SymbolControl ... Threshold="{Binding Threshold, NotifyOnTargetUpdated=True, Converter={StaticResource DummyConverter}}" .../>
When I call NotifyPropertyChanged(nameof(Threshold)), the CustomControl does not update.
However I have placed a dummy converter, with a breakpoint in, in the Threshold binding when the custom control is instantiated and this breakpoint triggers when I call NotifyPropertyChanged(nameof(Threshold)), so it appears the binding isn't updating the target?
I have also tried adding a PropertyChangedCallback for the DP ThresholdProperty with a breakpoint in which only triggers when the original property is first instantiated.
I have also found doing this in the ViewModel causes the custom control to update:
var temp = Threshold;
Threshold = null;
Threshold = temp;
I have done lots of searching online and had no luck, any ideas of what the problem could be?
I found another work around as I was unable to use the other work around given in the question:
I added the code into Threshold.cs:
public Threshold : IThreshold<SolidColorBrush>, INotifyPropertyChanged
{
...
public Threshold()
{
...
this.PropertyChanged += delegate (object sender, PropertyChangedEventArgs e) { if (e.PropertyName != nameof(WorkAround)) { NotifyPropertyChanged(nameof(WorkAround)); } };
}
...
public bool WorkAround { set; get; }
}
In the xaml of the custom control I added the 4th binding to the MultiBinding:
...
<Border.Background>
<MultiBinding Converter="{StaticResource ThreshholdToReturnValueConverter}" NotifyOnTargetUpdated="True" >
<Binding Path="Threshold" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="SymbolValue" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="DefaultBackground" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
<Binding Path="Threshold.WorkAround" RelativeSource="{RelativeSource TemplatedParent}" NotifyOnTargetUpdated="True" />
</MultiBinding>
</Border.Background>
...
However this workaround is not ideal, so I will not accept it, if anyone finds a better solution please let me know :)

Validation using ValidationRules - Ok button should trigger validation and not continue execution

I have a simple example application, 2 text boxes a validation rule and a button.
I want my button to trigger validation and if it's not valid, not to continue with its execution.
<TextBox>
<TextBox.Text>
<Binding Path="FirstName">
<Binding.ValidationRules>
<local:MyValidationRule ErrorMessage="Enter first name" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox>
<TextBox.Text>
<Binding Path="LastName">
<Binding.ValidationRules>
<local:MyValidationRule ErrorMessage="Enter last name" />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Command="{Binding OkCommand}" Content="Ok" />
The validation rule:
public class MyValidationRule : ValidationRule
{
public string ErrorMessage { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
string valueToCheck = value as string;
if (string.IsNullOrWhiteSpace(valueToCheck))
{
return new ValidationResult(false, ErrorMessage);
}
else
{
return new ValidationResult(true, null);
}
}
}
My OkCommand is empty method:
OkCommand= new RelayCommand(OkRequested);
private void OkRequested()
{
// Do stuff
}
However, no matter if the text boxes are empty OkCommand gets executed properly. What am I doing wrong here? I want text boxes to be styled if they're not valid (I've excluded styles from example), but that does not happen.
You should add into OkCommand something like this. And same for other textBoxes.
if ( Validation.GetHasError( textBox1 ) )
{
MessageBox.Show( Validation.GetErrors( textBox1 )[0].ErrorContent.ToString() );
return;
}

How can I handle a Validation.Error in my ViewModel instead of my View's code behind?

I'm trying to get WPF validation to work within the MVVM pattern.
In my View, I can validate a TextBox like this which gets handled by the code-behind method "HandleError", which works fine:
<TextBox Width="200"
Validation.Error="HandleError">
<TextBox.Text>
<Binding Path="FirstName"
NotifyOnValidationError="True"
Mode="TwoWay">
<Binding.ValidationRules>
<validators:DataTypeLineIsValid/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
However, I would like to handle the validation in my ViewModel via a DelegateCommand but when I try it with the following code, I get the explicit error "'{Binding HandleErrorCommand}' is not a valid event handler method name. Only instance methods on the generated or code-behind class are valid."
Are there any workaround for this so that we can handle validations within a MVVM pattern?
View:
<TextBox Width="200"
Validation.Error="{Binding HandleErrorCommand}">
<TextBox.Text>
<Binding Path="FirstName"
NotifyOnValidationError="True"
Mode="TwoWay">
<Binding.ValidationRules>
<validators:DataTypeLineIsValid/>
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
ViewModel:
#region DelegateCommand: HandleError
private DelegateCommand handleErrorCommand;
public ICommand HandleErrorCommand
{
get
{
if (handleErrorCommand == null)
{
handleErrorCommand = new DelegateCommand(HandleError, CanHandleError);
}
return handleErrorCommand;
}
}
private void HandleError()
{
MessageBox.Show("in view model");
}
private bool CanHandleError()
{
return true;
}
#endregion
I don't know if this will help you, but I'll offer it all the same.
Also, I'm using Silverlight, not WPF.
I don't specify any validation in my Views, neither in the code behind nor the xaml. My View has only data bindings to properties on the ViewModel.
All my error checking/validation is handled by the ViewModel. When I encounter an error, I set a ErrorMessage property, which is bound to the view as well. The ErrorMessage textblock (in the view) has a value converter which hides it if the error is null or empty.
Doing things this way makes it easy to unit test input validation.
Here's a way to do this using Expression Blend 3 behaviors. I wrote a ValidationErrorEventTrigger because the built-in EventTrigger doesn't work with attached events.
View:
<TextBox>
<i:Interaction.Triggers>
<MVVMBehaviors:ValidationErrorEventTrigger>
<MVVMBehaviors:ExecuteCommandAction TargetCommand="HandleErrorCommand" />
</MVVMBehaviors:ValidationErrorEventTrigger>
</i:Interaction.Triggers>
<TextBox.Text>
<Binding Path="FirstName"
Mode="TwoWay"
NotifyOnValidationError="True">
<Binding.ValidationRules>
<ExceptionValidationRule />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
ViewModel: (could be unchanged, but here's a look at how I dug into the validation arguments to find the error message when using the exception validation rule)
public ICommand HandleErrorCommand
{
get
{
if (_handleErrorCommand == null)
_handleErrorCommand = new RelayCommand<object>(param => OnDisplayError(param));
return _handleErrorCommand;
}
}
private void OnDisplayError(object param)
{
string message = "Error!";
var errorArgs = param as ValidationErrorEventArgs;
if (errorArgs != null)
{
var exception = errorArgs.Error.Exception;
while (exception != null)
{
message = exception.Message;
exception = exception.InnerException;
}
}
Status = message;
}
ValidationErrorEventTrigger:
public class ValidationErrorEventTrigger : EventTriggerBase<DependencyObject>
{
protected override void OnAttached()
{
Behavior behavior = base.AssociatedObject as Behavior;
FrameworkElement associatedElement = base.AssociatedObject as FrameworkElement;
if (behavior != null)
{
associatedElement = ((IAttachedObject)behavior).AssociatedObject as FrameworkElement;
}
if (associatedElement == null)
{
throw new ArgumentException("Validation Error Event trigger can only be associated to framework elements");
}
associatedElement.AddHandler(Validation.ErrorEvent, new RoutedEventHandler(this.OnValidationError));
}
void OnValidationError(object sender, RoutedEventArgs args)
{
base.OnEvent(args);
}
protected override string GetEventName()
{
return Validation.ErrorEvent.Name;
}
}

Categories

Resources