Proper Validation with Mvvm

Proper validation with MVVM

Warning: Long answer also

I use the IDataErrorInfo interface for validation, but I have customised it to my needs. I think that you'll find that it solves some of your problems too. One difference to your question is that I implement it in my base data type class.

As you pointed out, this interface just deals with one property at a time, but clearly in this day and age, that's no good. So I just added a collection property to use instead:

protected ObservableCollection<string> errors = new ObservableCollection<string>();

public virtual ObservableCollection<string> Errors
{
get { return errors; }
}

To address your problem of not being able to display external errors (in your case from the view, but in mine from the view model), I simply added another collection property:

protected ObservableCollection<string> externalErrors = new ObservableCollection<string>();

public ObservableCollection<string> ExternalErrors
{
get { return externalErrors; }
}

I have an HasError property which looks at my collection:

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

This enables me to bind this to Grid.Visibility using a custom BoolToVisibilityConverter, eg. to show a Grid with a collection control inside that shows the errors when there are any. It also lets me change a Brush to Red to highlight an error (using another Converter), but I guess 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 a custom extension method and 'does what is says on the tin'. Note how it will call each property that I want to validate in turn and compile a collection from them, ignoring duplicate errors.

Using the ExternalErrors collection, I can validate things that I can't validate 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 address your point regarding the situation where a user enters an alphabetical character into a int field, I tend to use a custom IsNumeric AttachedProperty for the TextBox, eg. I don't let them make these kinds of errors. I always feel that it's better to stop it, than to let it happen and then fix it.

Overall I'm really happy with my validation ability in WPF and am not left wanting at all.

To end with and for completeness, I felt that I should alert you to the fact that there is now an INotifyDataErrorInfo interface which includes some of this added functionality. You can find out more from the INotifyDataErrorInfo Interface page on MSDN.


UPDATE >>>

Yes, the ExternalErrors property just let's me add errors that relate to a data object from outside that object... sorry, my example wasn't complete... if I'd have shown you the IsGenreNameUnique method, you would have seen that it uses LinQ on all of the Genre data items in the collection to determine whether the object's 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 I can see you getting those errors in your data class is if you declare all your properties as object, but then you'd have an awful lot of casting to do. Perhaps you could double your properties like this:

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 code and FooObject was used in the Binding, you could 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;
}
}

That way you could fulfil your requirements, but you'll have a lot of extra code to add.

How to implement data validation in model of MVVM?

The data annotations method you've linked is somewhat redundant as the framework already has the concept of bindable validation rules (see Taking data binding, validation and MVVM to the next level). The only time the data annotations approach will be useful is when validating in a spot that is completely divorced from the UI.

Aside from those two options, using IDataErrorInfo is your only other choice. Note that you are not "exposing the entire model to the view as a single property", rather you are using an interface to expose your custom entity to the binding framework - there's a big difference. If you choose this method then make sure you use resource strings to hold your error messages rather than using hard coded text. The IDataErrorInfo approach is useful if you have different people working on the view and viewmodel - the person doing the view doesn't need to know anything about the specific validations of the viewmodel.

How to implement validation using ViewModel and Databinding?

There can be many ways to implement this. I am telling you two solutions, both works well, you can use which you find suitable for you.

I use extends BaseObservable because I find that easy than converting all fields to Observers. You can use ObservableFields too.

Solution 1 (Using custom BindingAdapter)

In xml

<variable
name="model"
type="sample.data.Model"/>

<EditText
passwordValidator="@{model.password}"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={model.password}"/>

Model.java

public class Model extends BaseObservable {
private String password;

@Bindable
public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
notifyPropertyChanged(BR.password);
}
}

DataBindingAdapter.java

public class DataBindingAdapter {
@BindingAdapter("passwordValidator")
public static void passwordValidator(EditText editText, String password) {
// ignore infinite loops
int minimumLength = 5;
if (TextUtils.isEmpty(password)) {
editText.setError(null);
return;
}
if (editText.getText().toString().length() < minimumLength) {
editText.setError("Password must be minimum " + minimumLength + " length");
} else editText.setError(null);
}
}

Solution 2 (Using custom afterTextChanged)

In xml

<variable
name="model"
type="com.innovanathinklabs.sample.data.Model"/>

<variable
name="handler"
type="sample.activities.MainActivityHandler"/>

<EditText
android:id="@+id/etPassword"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:afterTextChanged="@{(edible)->handler.passwordValidator(edible)}"
android:text="@={model.password}"/>

MainActivityHandler.java

public class MainActivityHandler {
ActivityMainBinding binding;

public void setBinding(ActivityMainBinding binding) {
this.binding = binding;
}

public void passwordValidator(Editable editable) {
if (binding.etPassword == null) return;
int minimumLength = 5;
if (!TextUtils.isEmpty(editable.toString()) && editable.length() < minimumLength) {
binding.etPassword.setError("Password must be minimum " + minimumLength + " length");
} else {
binding.etPassword.setError(null);
}
}
}

MainActivity.java

public class MainActivity extends AppCompatActivity {
ActivityMainBinding binding;

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
binding.setModel(new Model());
MainActivityHandler handler = new MainActivityHandler();
handler.setBinding(binding);
binding.setHandler(handler);
}
}

Update

You can also replace

android:afterTextChanged="@{(edible)->handler.passwordValidator(edible)}"

with

android:afterTextChanged="@{handler::passwordValidator}"

Because parameter are same of android:afterTextChanged and passwordValidator.

MVVM - Validation

You could consider using the System.ComponentModel.IDataErrorInfo interface. This very handy interface gives you the ability to:

  • do validation in a MVVM compliant manner
  • do custom validation for any particular field (the validation could check several values if you want it to)
  • bind your UI to the validation errors

You implement IDataErrorInfo on your viewmodel (or even virtually in your view model base, and override it in your derived view models). Due to the nature of databinding, the values i need to check are all there in the view model, and i can test any combination of them. Of course you still have your validation in your business layer, but you no longer need to make a trip to your business layer (or Model) just to effect some validation.

Here is a quick example from a (WPF) screen that gathers some user details and does basic validation on them:

C# code:

    #region IDataErrorInfo Members

/// <summary>
/// Gets an error message indicating what is wrong with this object.
/// </summary>
/// <value></value>
/// <returns>An error message indicating what is wrong with this object. The default is an empty string ("").</returns>
public override string Error
{
get
{
return this["UserCode"] + this["UserName"] + this["Password"] + this["ConfirmedPassword"] + this["EmailAddress"];
}
}

/// <summary>
/// Gets the <see cref="System.String"/> with the specified column name.
/// </summary>
/// <value></value>
public override string this[string columnName]
{
get
{
switch (columnName)
{
case "UserCode":
if (!string.IsNullOrEmpty(UserCode) && UserCode.Length > 20)
return "User Code must be less than or equal to 20 characters";
break;

case "UserName":
if (!string.IsNullOrEmpty(UserCode) && UserCode.Length > 60)
return "User Name must be less than or equal to 60 characters";
break;

case "Password":
if (!string.IsNullOrEmpty(Password) && Password.Length > 60)
return "Password must be less than or equal to 60 characters";
break;

case "ConfirmedPassword":
if (Password != ConfirmedPassword)
return Properties.Resources.ErrorMessage_Password_ConfirmedPasswordDoesntMatch;
break;

case "EmailAddress":
if (!string.IsNullOrEmpty(EmailAddress))
{
var r = new Regex(_emailRegex);
if (!r.IsMatch(EmailAddress))
return Properties.Resources.ErrorMessage_Email_InvalidEmailFormat;
}
break;
}
return string.Empty;
}
}

#endregion

and here is the XAML markup for two of the textboxes on the page (note particularly the ValidatesOnDataErrors and ValidatesOnExceptions properties in the Text binding):

<TextBox Name="UserCodeTextBox" 
Text="{Binding UserCode,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
GotFocus="Input_GotFocus"
VerticalAlignment="Top"
Margin="165,0,150,0"
CharacterCasing="Upper"
/>

<TextBox Name="UserNameTextBox"
Text="{Binding UserName,
Mode=TwoWay,
UpdateSourceTrigger=PropertyChanged,
ValidatesOnDataErrors=True,
ValidatesOnExceptions=True,
NotifyOnSourceUpdated=True,
NotifyOnTargetUpdated=True}"
GotFocus="Input_GotFocus"
VerticalAlignment="Top"
Margin="165,30,0,0"
/>


Related Topics



Leave a reply



Submit