Proper Validation Using MVVM

Warning: very long and detailed post.

Ok, validation in WPF when using MVVM. I read a lot of things now, looked at many SO questions and tried many approaches, but at some point everything looks a little strange, and I'm really not sure how to do it right β„’.

Ideally, I want all validation to happen in the view model using IDataErrorInfo ; so here is what i did. However, there are various aspects that make this decision not a complete solution for the whole topic of verification.

Situation

Let us take the following simple form. As you can see, nothing special. We just have two text fields that bind to string and the int property in each view model. In addition, we have a button associated with ICommand .

Simple form with only a string and integer input

So, for verification, we now have two options:

  1. We can run the check automatically every time the value of the text field changes. Thus, the user receives an instant response when they enter something invalid.
    • We can take another step to disable the button when errors occur.
  2. Or we can run the test only explicitly when the button is pressed, and then all errors are shown, if applicable. Obviously, we cannot disable the button for errors here.

Ideally, I want to implement option 1. For normal data bindings with activated ValidatesOnDataErrors this is the default behavior. Therefore, when the text changes, the binding updates the source and starts checking IDataErrorInfo for this property; errors are reported in reverse. So far, so good.

Validation Status in the View Model

An interesting point is to inform the view model or button in this case of errors. How IDataErrorInfo works, it is mainly designed to report errors back to the view. This way, the view can easily see if there are any errors, display them and even show annotations using Validation.Errors . In addition, validation always occurs by looking at a single property.

Thus, it is difficult to have an idea of ​​the presentation model when there are any errors or if the verification was successful. A common solution is to simply run an IDataErrorInfo check for all properties in the view model itself. This is often done using the separate IsValid property. The advantage is that it can also be easily used to disable a command. The disadvantage is that it can run a check on all properties too often, but most checks should be simple enough so as not to degrade performance. Another solution would be to recall which properties caused errors when using validation, and only validate them, but in most cases this seems too complicated and unnecessary.

The bottom line is that this may work fine. IDataErrorInfo provides verification of all properties, and we can simply use this interface in the view model itself to perform verification there for the entire object. Presenting the problem:

Mandatory Exceptions

The view model uses the actual types for its properties. Thus, in our example, the whole property is the actual int . However, the text box used in the view only supports text. Thus, when binding to int in the view model, the data binding engine will automatically perform type conversions - or at least try. If you can enter text in a text field dedicated to numbers, it is highly likely that there will not always be real numbers inside: therefore, the data binding mechanism will not be able to convert FormatException .

Data binding engine throws an exception and thats displayed in the view

From the viewpoint, we can easily see it. Exceptions from the binding mechanism are automatically caught by WPF and displayed as errors - you don’t even need to include the Binding.ValidatesOnExceptions that are required for the exceptions, Binding.ValidatesOnExceptions in the installer. Error messages have a common text, so this can be a problem. I solved this for myself using the Binding.UpdateSourceExceptionFilter handler, the Binding.UpdateSourceExceptionFilter thrown exception and looking at the source property, and then generating a less general error message. All of this is hidden in my own Binding markup extension, so I can have all the necessary defaults.

So the opinion is in order. The user makes a mistake, sees some kind of mistake and can fix it. The presentation model is however lost. Since the binding engine threw an exception, the source was never updated. Thus, the view model still has the old value, which is not displayed to the user, and the IDataErrorInfo check IDataErrorInfo obviously not applicable.

Even worse, the view model does not have a good way to find out. At least I have not yet found a good solution for this. What would be possible is to return the presentation report to the presentation model indicating that an error has occurred. This can be done by binding the data of the Validation.HasError property to the view model (which is not possible directly), so the view model can first check the state of the views.

Another option is to pass the exception handled by Binding.UpdateSourceExceptionFilter to the view model so that it also Binding.UpdateSourceExceptionFilter about it. A view model can even provide some kind of binding interface to report these things, allowing for custom error messages instead of common to each type of message. But this will create a stronger link between the view and the view model, which I usually want to avoid.

Another β€œsolution” is to get rid of all typed properties, use the properties of a simple string and instead perform the conversion in the view model. This would obviously move the whole validation to the presentation model, but it would also mean an incredible amount of duplication of things, which the data binding mechanism usually takes care of. In addition, this would change the semantics of the view model. For me, the presentation is built for the presentation model, and not vice versa - of course, the design of the presentation model depends on what we represent for the presentation, but there is still general freedom how the presentation does it. Thus, the view model defines the int property because there is a number; Now the view can use a text field (resolving all these problems) or use what works with numbers initially. So no, changing property types to string is not an option for me.

In the end, it is a problem of vision. A view (and its data binding mechanism) is responsible for providing the correct values ​​for the view model to work with. But in this case, there seems to be no good way to tell the view model that it should invalidate the old value of the property.

Bindinggroups

Linking groups is one of the ways I tried to do this. Binding groups have the ability to group all checks, including IDataErrorInfo and IDataErrorInfo exceptions. If they are available for the presentation model, they even have the ability to check the validation status for all of these validation sources, for example, using CommitEdit .

By default, linking groups implement option 2 on top. They explicitly update the bindings, essentially adding an extra uncommitted state. Thus, when a button is pressed, the team can commit these changes, cause updates to the source and all checks and get a single result if successful. Thus, the action of the command may be as follows:

  if (bindingGroup.CommitEdit()) SaveEverything(); 

CommitEdit will return true only if all CommitEdit checks CommitEdit successful. It will take into account IDataErrorInfo and also check for binding exceptions. This seems to be the ideal solution for choice 2. The only thing that causes a lot of trouble is managing a group of bindings using bindings, but I created myself something that basically takes care of this ( connected ).

If a binding group is present for the binding, the default binding will be explicit UpdateSourceTrigger . To implement option 1 from above using binding groups, we basically need to change the trigger. Since I have my own snap extension anyway, it's pretty simple, I just installed it on LostFocus for everyone.

So now the bindings will be updated when the text field changes. If the source can be updated (the binding mechanism does not throw an exception), then IDataErrorInfo will work as usual. If it could not be updated, the view can still see it. And if we press our button, the basic command can call CommitEdit (although nothing needs to be fixed) and get a general check result to see if it can continue.

We may not be able to easily disable the button in this way. At least not from the point of view of the model. Checking the check again and again is not a good idea to simply update the status of the command, and the view model is not notified when in any case a binding mechanism exception is thrown (which should then disable the button), or when it disappears to turn on the button again. We can still add a trigger to disable the button in the view using Validation.HasError so this is not impossible.

Decision?

All in all, this seems to be the perfect solution. What is my problem though? To be honest, I'm not quite sure. Linking groups is a complex thing that seems to be commonly used in small groups, possibly with multiple linking groups in one view. Using one large linking group for the whole presentation just to ensure my validation gives the impression that I am abusing it. And I just keep thinking that there should be a better way to resolve this whole situation, because, of course, I cannot be the only one who has these problems. And so far, I really have not seen many people generally use binding groups to check with MVVM, so this seems weird.

So what exactly is the correct way to check in WPF with MVVM, while checking for binding mechanism exceptions?




My decision (/ hack)

First of all, thanks for your input! As I wrote above, I already use IDataErrorInfo to validate my data, and I personally think that this is the most convenient utility for performing validation. I use utilities similar to what Sheridan suggested in his answer below, so maintenance also works fine.

In the end, my problem boiled down to the problem of mandatory exceptions when the presentation model simply did not know when it happened. Although I could deal with this with the help of the required groups, as described above, I still decided not to do it, because I just felt uneasy about it. So what have I done instead?

As I mentioned above, I detect view-side binding exceptions by listening to UpdateSourceExceptionFilter bindings. There I can get a reference to the view model from DataItem binding DataItem . Then I have an IReceivesBindingErrorInformation interface that registers the view model as a possible recipient of binding error information. Then I use this to pass the binding and exception path to the view model:

 object OnUpdateSourceExceptionFilter(object bindExpression, Exception exception) { BindingExpression expr = (bindExpression as BindingExpression); if (expr.DataItem is IReceivesBindingErrorInformation) { ((IReceivesBindingErrorInformation)expr.DataItem).ReceiveBindingErrorInformation(expr.ParentBinding.Path.Path, exception); } // check for FormatException and produce a nicer error // ... } 

In the view model, I recall whenever I get informed about the path binding expression:

 HashSet<string> bindingErrors = new HashSet<string>(); void IReceivesBindingErrorInformation.ReceiveBindingErrorInformation(string path, Exception exception) { bindingErrors.Add(path); } 

And whenever the IDataErrorInfo repeatedly IDataErrorInfo property, I know that the binding worked, and I can remove the property from the hash set.

In the view model, I can then check whether the collection of hashes contains any elements, and abort any action that requires a full check of the data. This might not be the best solution because of the relationship between the view and the view model, but using this interface is at least a few less problems.

+55
c # validation wpf mvvm
Oct 21 '13 at 15:21
source share
5 answers

Warning: long answer also

I use the IDataErrorInfo interface for validation, but I customized it to my needs. I think you will find that it solves some of your problems. One difference of your question is that I implement it in my base class of data types.

As you pointed out, this interface simply deals with one property at a time, but obviously on this day and at an age, which is not good. So I added a collection property instead:

 protected ObservableCollection<string> errors = new ObservableCollection<string>(); public virtual ObservableCollection<string> Errors { get { return errors; } } 

To solve your problem of inability to display external errors (in your case from the view, but in my version of the model), I just added another property of the collection:

 protected ObservableCollection<string> externalErrors = new ObservableCollection<string>(); public ObservableCollection<string> ExternalErrors { get { return externalErrors; } } 

I have a HasError property that looks at my collection:

 public virtual bool HasError { get { return Errors != null && Errors.Count > 0; } } 

This allows me to bind this to Grid.Visibility using a custom BoolToVisibilityConverter , for example. to show the Grid with a collection control inside, which shows errors when they are. It also allows me to change Brush to Red to highlight the error (using a different Converter ), but I assume you get the idea.

Then, in each data type or model class, I override the Errors property and implement the Item indexer (simplified in this example):

 public override ObservableCollection<string> Errors { get { errors = new ObservableCollection<string>(); errors.AddUniqueIfNotEmpty(this["Name"]); errors.AddUniqueIfNotEmpty(this["EmailAddresses"]); errors.AddUniqueIfNotEmpty(this["SomeOtherProperty"]); errors.AddRange(ExternalErrors); return errors; } } public override string this[string propertyName] { get { string error = string.Empty; if (propertyName == "Name" && Name.IsNullOrEmpty()) error = "You must enter the Name field."; else if (propertyName == "EmailAddresses" && EmailAddresses.Count == 0) error = "You must enter at least one e-mail address into the Email address(es) field."; else if (propertyName == "SomeOtherProperty" && SomeOtherProperty.IsNullOrEmpty()) error = "You must enter the SomeOtherProperty field."; return error; } } 

The AddUniqueIfNotEmpty method is the usual extension method and "does what the gesture says." Notice how it will call each property that I want to check, in turn, and compile a collection of them, ignoring repeated errors.

Using the ExternalErrors collection, I can verify that I cannot verify in the data class:

 private void ValidateUniqueName(Genre genre) { string errorMessage = "The genre name must be unique"; if (!IsGenreNameUnique(genre)) { if (!genre.ExternalErrors.Contains(errorMessage)) genre.ExternalErrors.Add(errorMessage); } else genre.ExternalErrors.Remove(errorMessage); } 

To answer a question about a situation where a user enters an alphabetic character in an int field, I usually use a custom IsNumeric AttachedProperty for a TextBox , for example. I do not allow them to make such mistakes. I always feel that it’s better to stop it than to let it happen and then fix it.

In general, I am very pleased with my ability to check in WPF, and I really do not want to do this.

To complete and for completeness, I felt that I should warn you that there is now an INotifyDataErrorInfo interface that includes some of these added features. You can learn more at the INotifyDataErrorInfo Interface page on MSDN.




UPDATE β†’>

Yes, the ExternalErrors property allows me to add errors related to a data object from outside this object ... sorry, my example was not complete ... if I showed you IsGenreNameUnique you would see that it uses LinQ for all Genre data elements in the collection to determine if an object name is unique or not:

 private bool IsGenreNameUnique(Genre genre) { return Genres.Where(d => d.Name != string.Empty && d.Name == genre.Name).Count() == 1; } 

As for your int / string problem, the only way to see how you get these errors in your data class is to declare all your properties as object , but then you will have a lot of casting. Perhaps you can double your properties as follows:

 public object FooObject { get; set; } // Implement INotifyPropertyChanged public int Foo { get { return FooObject.GetType() == typeof(int) ? int.Parse(FooObject) : -1; } } 

Then, if Foo was used in the code, and FooObject was used in Binding , you can do this:

 public override string this[string propertyName] { get { string error = string.Empty; if (propertyName == "FooObject" && FooObject.GetType() != typeof(int)) error = "Please enter a whole number for the Foo field."; ... return error; } } 

This way you can fulfill your requirements, but you will have a lot of additional code to add.

+17
Oct. 21 '13 at 16:22
source share

The disadvantage is that this can lead to checking all the properties of a bit too often, but most validations should be simple enough so as not to damage performance. Another solution would be to remember which properties generated errors using validation and only those checked, but this seems a bit complicated and unnecessary in most cases.

You do not need to keep track of which properties have errors; you only need to know that there are errors. The view model may contain a list of errors (also useful for displaying a summary of errors), and the IsValid property may simply be a reflection of whether there is anything in the list. You do not need to check everything every time IsValid is called, while you are sure that the summary of errors is current and that IsValid updated every time it changes.




In the end, it is a presentation problem. The view (and its data binding mechanism) is responsible for providing the correct values ​​for the view model to work with. But in this case, there seems to be no good way to tell the view model that it should invalidate the value of the old property.

You can listen for errors in the container that are bound to the view model:

 container.AddHandler(Validation.ErrorEvent, Container_Error); ... void Container_Error(object sender, ValidationErrorEventArgs e) { ... } 

, , , e.Error.Exception , .

, , . , " ".

+1
21 . '13 16:43
source share

, , . ViewModel , ViewModel .

, , . , , .

, :

 public struct Failable<T> { public T Value { get; private set; } public string Text { get; private set; } public bool IsValid { get; private set; } public Failable(T value) { Value = value; try { var converter = TypeDescriptor.GetConverter(typeof(T)); Text = converter.ConvertToString(value); IsValid = true; } catch { Text = String.Empty; IsValid = false; } } public Failable(string text) { Text = text; try { var converter = TypeDescriptor.GetConverter(typeof(T)); Value = (T)converter.ConvertFromString(text); IsValid = true; } catch { Value = default(T); IsValid = false; } } } 

, - ( ), . .

:

 public class StringToFailableConverter<T> : IValueConverter { public object Convert(object value, Type targetType, object parameter, CultureInfo culture) { if (value.GetType() != typeof(Failable<T>)) throw new InvalidOperationException("Invalid value type."); if (targetType != typeof(string)) throw new InvalidOperationException("Invalid target type."); var rawValue = (Failable<T>)value; return rawValue.Text; } public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) { if (value.GetType() != typeof(string)) throw new InvalidOperationException("Invalid value type."); if (targetType != typeof(Failable<T>)) throw new InvalidOperationException("Invalid target type."); return new Failable<T>(value as string); } } 

XAML Handy Converters

XAML, :

 public static class Failable { public static StringToFailableConverter<Int32> Int32Converter { get; private set; } public static StringToFailableConverter<double> DoubleConverter { get; private set; } static Failable() { Int32Converter = new StringToFailableConverter<Int32>(); DoubleConverter = new StringToFailableConverter<Double>(); } } 

.

Using

, int Failable<int> :

ViewModel

 public Failable<int> NumberValue { //Custom logic along with validation //using IsValid property } 

Xaml

 <TextBox Text="{Binding NumberValue,Converter={x:Static local:Failable.Int32Converter}}"/> 

, ( IDataErrorInfo INotifyDataErrorInfo - ) ViewModel , IsValid . IsValid , Value .

+1
03 . '16 0:39
source share

, , , ...
- ..
, ...
, / "" MVVM, "" , , .

... , MVVM:

" , . , , , , , theres "

.. - ?

, View-Model , .
- .

, :

public Visibility MyPresenter { get...

Visibility , -, ?
, , .

MVVM View-Models :

  • - , , ..
  • - , .

- .

:

 public abstract class ViewModelBase : INotifyPropertyChanged { public event PropertyChangedEventHandler PropertyChanged; public void RaisePropertyChanged([CallerMemberName] string propertyName = null) { if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); } } 


 public class VmSomeEntity : ViewModelBase, INotifyDataErrorInfo { //This one is part of INotifyDataErrorInfo interface which I will not use, //perhaps in more complicated scenarios it could be used to let some other VM know validation changed. public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged; //will hold the errors found in validation. public Dictionary<string, string> ValidationErrors = new Dictionary<string, string>(); //the actual value - notice it is 'int' and not 'string'.. private int storageCapacityInBytes; //this is just to keep things sane - otherwise the view will not be able to send whatever the user throw at it. //we want to consume what the user throw at us and validate it - right? :) private string storageCapacityInBytesWrapper; //This is a property to be served by the View.. important to understand the tactic used inside! public string StorageCapacityInBytes { get { return storageCapacityInBytesWrapper ?? storageCapacityInBytes.ToString(); } set { int result; var isValid = int.TryParse(value, out result); if (isValid) { storageCapacityInBytes = result; storageCapacityInBytesWrapper = null; RaisePropertyChanged(); } else storageCapacityInBytesWrapper = value; HandleValidationError(isValid, "StorageCapacityInBytes", "Not a number."); } } //Manager for the dictionary private void HandleValidationError(bool isValid, string propertyName, string validationErrorDescription) { if (!string.IsNullOrEmpty(propertyName)) { if (isValid) { if (ValidationErrors.ContainsKey(propertyName)) ValidationErrors.Remove(propertyName); } else { if (!ValidationErrors.ContainsKey(propertyName)) ValidationErrors.Add(propertyName, validationErrorDescription); else ValidationErrors[propertyName] = validationErrorDescription; } } } // this is another part of the interface - will be called automatically public IEnumerable GetErrors(string propertyName) { return ValidationErrors.ContainsKey(propertyName) ? ValidationErrors[propertyName] : null; } // same here, another part of the interface - will be called automatically public bool HasErrors { get { return ValidationErrors.Count > 0; } } } 

- - " CanExecute" VmEntity.HasErrors.

:)

0
08 . '15 13:23
source share

, ...

, int ( ), .

viewmodel, .

123abc, , , . .

, int viewmodel . , .

IMHO WPF , ( ) , , . , , , .

Microsoft , , int decimal, - . , , XAML, .

Thanks and respect to the other guys who provided detailed answers on this topic.

0
Dec 11 '18 at 21:17
source share



All Articles