Xamarin MvvmCross ViewModel Validation

6.1k views Asked by At

I'm building my first Xamarin MvvmCross application at the moment and I'm currently looking at validating user input to the view models.

Doing a lot of searching around everything (including the MvvmCross team) link to this plugin:

MVVMCross.Plugins.Validation

This plugin makes use of a very old version of MvvmCross v3. I have tried taking the code from this plugin and building it directly into my application Core project until I came across the Bindings breaking change. I then came to the conclusion that this plugin would actually require a complete re-write due to this in order to use the latest version of MvvmCross.

So I'm now a little stuck.

What is the currently recommended best approach for performing input validation in a view model?

2

There are 2 answers

12
Kiliman On BEST ANSWER

EDIT: Add sample project on GitHub https://github.com/kiliman/mvx-samples/tree/master/MvxSamples.Validation

I use MVVM Validation Helpers http://www.nuget.org/packages/MvvmValidation/

It's a simple validation library that's easy to use. It's not tied to MvvmCross.

Here's how I use it, for example, in my SigninViewModel:

private async void DoSignin()
{
    try
    {
        if (!Validate())
        {
            return;
        }

        IsBusy = true;
        Result = "";
        var success = await SigninService.SigninAsync(Email, Password);

        if (success)
        {
            Result = "";
            ShowViewModel<HomeViewModel>();
            Close();
            return;
        }

        Result = "Invalid email/password. Please try again.";
    }
    catch (Exception ex)
    {
        Result = "Error occured during sign in.";
        Mvx.Error(ex.ToString());
    }
    finally
    {
        IsBusy = false;
    }
}

private bool Validate()
{
    var validator = new ValidationHelper();
    validator.AddRequiredRule(() => Email, "Email is required.");
    validator.AddRequiredRule(() => Password, "Password is required.");

    var result = validator.ValidateAll();

    Errors = result.AsObservableDictionary();

    return result.IsValid;
}

The nice part of it is that you can get Errors as a collection and bind them in your view. For Android, I set the Error property to the keyed Error message.

<EditText
    android:minHeight="40dp"
    android:layout_margin="4dp"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:inputType="textEmailAddress"
    android:hint="Email"
    local:MvxBind="Text Email; Error Errors['Email']"
    android:id="@+id/EmailEditText" />
<EditText
    android:minHeight="40dp"
    android:layout_margin="4dp"
    android:inputType="textPassword"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:hint="Password"
    local:MvxBind="Text Password; Error Errors['Password']"
    android:id="@+id/PasswordEditText" />

And here's what the validation looks like:

Validation message

EDIT: show helper code

public static class ValidationResultExtension
{
    public static ObservableDictionary<string, string> AsObservableDictionary(this ValidationResult result)
    {
        var dictionary = new ObservableDictionary<string, string>();
        foreach (var item in result.ErrorList)
        {
            var key = item.Target.ToString();
            var text = item.ErrorText;
            if (dictionary.ContainsKey(key))
            {
                dictionary[key] = dictionary.Keys + Environment.NewLine + text;
            }
            else
            {
                dictionary[key] = text;
            }
        }
        return dictionary;
    }
}

public class ObservableDictionary<TKey, TValue> : IDictionary<TKey, TValue>, INotifyCollectionChanged, INotifyPropertyChanged
{
    private const string CountString = "Count";
    private const string IndexerName = "Item[]";
    private const string KeysName = "Keys";
    private const string ValuesName = "Values";

    private IDictionary<TKey, TValue> _dictionary;

    protected IDictionary<TKey, TValue> Dictionary
    {
        get { return _dictionary; }
    }

    public ObservableDictionary()
    {
        _dictionary = new Dictionary<TKey, TValue>();
    }

    public ObservableDictionary(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = new Dictionary<TKey, TValue>(dictionary);
    }

    public ObservableDictionary(IEqualityComparer<TKey> comparer)
    {
        _dictionary = new Dictionary<TKey, TValue>(comparer);
    }

    public ObservableDictionary(int capacity)
    {
        _dictionary = new Dictionary<TKey, TValue>(capacity);
    }

    public ObservableDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
    {
        _dictionary = new Dictionary<TKey, TValue>(dictionary, comparer);
    }

    public ObservableDictionary(int capacity, IEqualityComparer<TKey> comparer)
    {
        _dictionary = new Dictionary<TKey, TValue>(capacity, comparer);
    }

    #region IDictionary<TKey,TValue> Members

    public void Add(TKey key, TValue value)
    {
        Insert(key, value, true);
    }

    public bool ContainsKey(TKey key)
    {
        return Dictionary.ContainsKey(key);
    }

    public ICollection<TKey> Keys
    {
        get { return Dictionary.Keys; }
    }

    public bool Remove(TKey key)
    {
        if (key == null)
        {
            throw new ArgumentNullException("key");
        }

        TValue value;
        Dictionary.TryGetValue(key, out value);
        var removed = Dictionary.Remove(key);
        if (removed)
        {
            OnCollectionChanged(NotifyCollectionChangedAction.Remove, new KeyValuePair<TKey, TValue>(key, value));
        }
        return removed;
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        return Dictionary.TryGetValue(key, out value);
    }

    public ICollection<TValue> Values
    {
        get { return Dictionary.Values; }
    }

    public TValue this[TKey key]
    {
        get
        {
            return Dictionary.ContainsKey(key) ? Dictionary[key] : default(TValue);
        }
        set
        {
            Insert(key, value, false);
        }
    }

    #endregion IDictionary<TKey,TValue> Members

    public void Add(KeyValuePair<TKey, TValue> item)
    {
        Insert(item.Key, item.Value, true);
    }

    public void Clear()
    {
        if (Dictionary.Count > 0)
        {
            Dictionary.Clear();
            OnCollectionChanged();
        }
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return Dictionary.Contains(item);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        Dictionary.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return Dictionary.Count; }
    }

    public bool IsReadOnly
    {
        get { return Dictionary.IsReadOnly; }
    }

    public bool Remove(KeyValuePair<TKey, TValue> item)
    {
        return Remove(item.Key);
    }

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return Dictionary.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return ((IEnumerable)Dictionary).GetEnumerator();
    }

    public event NotifyCollectionChangedEventHandler CollectionChanged;

    public event PropertyChangedEventHandler PropertyChanged;

    public void AddRange(IDictionary<TKey, TValue> items)
    {
        if (items == null)
        {
            throw new ArgumentNullException("items");
        }

        if (items.Count > 0)
        {
            if (Dictionary.Count > 0)
            {
                if (items.Keys.Any((k) => Dictionary.ContainsKey(k)))
                {
                    throw new ArgumentException("An item with the same key has already been added.");
                } 
                else
                {
                    foreach (var item in items)
                    {
                        Dictionary.Add(item);
                    }
                }
            }
            else
            {
                _dictionary = new Dictionary<TKey, TValue>(items);
            }

            OnCollectionChanged(NotifyCollectionChangedAction.Add, items.ToArray());
        }
    }

    private void Insert(TKey key, TValue value, bool add)
    {
        if (key == null)
        {
            throw new ArgumentNullException("key");
        }

        TValue item;
        if (Dictionary.TryGetValue(key, out item))
        {
            if (add)
            {
                throw new ArgumentException("An item with the same key has already been added.");
            }
            if (Equals(item, value))
            {
                return;
            }
            Dictionary[key] = value;

            OnCollectionChanged(NotifyCollectionChangedAction.Replace, new KeyValuePair<TKey, TValue>(key, value), new KeyValuePair<TKey, TValue>(key, item));
        }
        else
        {
            Dictionary[key] = value;

            OnCollectionChanged(NotifyCollectionChangedAction.Add, new KeyValuePair<TKey, TValue>(key, value));
        }
    }

    private void OnPropertyChanged()
    {
        OnPropertyChanged(CountString);
        OnPropertyChanged(IndexerName);
        OnPropertyChanged(KeysName);
        OnPropertyChanged(ValuesName);
    }

    protected virtual void OnPropertyChanged(string propertyName)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    private void OnCollectionChanged()
    {
        OnPropertyChanged();
        if (CollectionChanged != null)
        {
            CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
        }
    }

    private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> changedItem)
    {
        OnPropertyChanged();
        if (CollectionChanged != null)
        {
            CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, changedItem));
        }
    }

    private void OnCollectionChanged(NotifyCollectionChangedAction action, KeyValuePair<TKey, TValue> newItem, KeyValuePair<TKey, TValue> oldItem)
    {
        OnPropertyChanged();
        if (CollectionChanged != null)
        {
            CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItem, oldItem));
        }
    }

    private void OnCollectionChanged(NotifyCollectionChangedAction action, IList newItems)
    {
        OnPropertyChanged();
        if (CollectionChanged != null)
        {
            CollectionChanged(this, new NotifyCollectionChangedEventArgs(action, newItems));
        }
    }
}
0
Mark Z. On

There's no set recommendation really, it's what you're most comfortable with. I find a lot of the options to be particularly verbose (i.e. require a lot of boiler plate code, even with some of the helper libraries).

The library that I landed on was FluentValidation for writing the rules (and they have a lot of common ones built-in and great ways for re-use/customization, including context-specific rules), and to reduce a lot of the complication and lines of code required, wrote a little helper library of my own which can be seen here (complete with example): FluentValidation MVVM Plugin

The example there uses Prism but it is not at all reliant on any MVVM framework.

Here's a peek at the example:

Class to build/validate:

public class Email
{
    public string RecipientEmailAddress { get; set; }
    public string RecipientName { get; set; }
}

Properties in your ViewModel using the Validatable object provided in my library, and Fody.PropertyChanged (which will also save you a lot of boiler-plate code for INPC):

public Validatable<string> RecipientName { get; set; } = new Validatable<string>(nameof(Email.RecipientName));
public Validatable<string> EmailAddress { get; set; } = new Validatable<string>(nameof(Email.RecipientEmailAddress));

Creating a FluentValidation AbstractValidator for the class:

public class EmailValidator : AbstractValidator<Email>
{
    public EmailValidator()
    {
        RuleFor(e => e.RecipientEmailAddress)
            .Cascade(CascadeMode.StopOnFirstFailure)
            .NotEmpty()
            .EmailAddress();

        RuleFor(e => e.RecipientName)
            .NotEmpty();

        When(e => e.RecipientName != null, () =>
        {
            RuleFor(e => e.RecipientName)
                .MinimumLength(3).WithMessage("How you bout to enter a FULL 'name' with less than 3 chars!?")
                .Must(name => name.Contains(" ")).WithMessage("Expecting at least first and last name separated by a space!");
        });
    }
}

Implementing IValidate in your ViewModel:

public void SetupForValidation() // to be called from your ViewModel's constructor
{
    // set validators and prop groups
    _emailValidator = new EmailValidator();
    _emailValidatables = new Validatables(RecipientName, EmailAddress);

    // maybe even set some defaults
    RecipientName.Value = "Fred Fredovich";
}

public OverallValidationResult Validate(Email email)
{
    return _emailValidator.Validate(email).ApplyResultsTo(_emailValidatables);
}

public void ClearValidation(string clearOptions = "")
{
    _emailValidatables.Clear(clearOptions);
}

Implementing Commands (the example below uses Prism's DelegateCommand but obviously that is not a requirement) to use those methods:

private DelegateCommand<string> _clearValidationCommand;
private DelegateCommand _validateEmailCommand;

public DelegateCommand<string> ClearValidationCommand =>
    _clearValidationCommand ?? (_clearValidationCommand = new DelegateCommand<string>(ClearValidation)); // already defined above in step 4 as part of the interface requirements

public DelegateCommand ValidateEmailCommand =>
    _validateEmailCommand ?? (_validateEmailCommand = new DelegateCommand(ExecuteValidateEmailCommand));

public void ExecuteValidateEmailCommand()
{
    var email = _emailValidatables.Populate<Email>(); // this conveniently creates a new Email instance with the values from our Validatable objects (populated by the user via the View)
    var overallValidationResult = Validate(email); // remember, this will also populate each individual Validatable's IsValid status and Errors list.

    if (overallValidationResult.IsValidOverall)
    {
        // do something with the validated email instance
    }
    else
    {
        // do something else
    }

    if (overallValidationResult.NonSplitErrors.Any())
    {
        // do something with errors that don't pertain to any of our Validatables (which is not possible in our little example here)
    }
}

Finally, the View (in XAML in this example):

<Entry
    Placeholder="Email"
    Text="{Binding EmailAddress.Value}">
    <Entry.Behaviors>
        <!-- Note this behavior is included in the Prism Library -->
        <behaviors:EventToCommandBehavior
            Command="{Binding ClearValidationCommand}"
            CommandParameter="RecipientEmailAddress"
            EventName="Focused" />
    </Entry.Behaviors>
</Entry>
<Label
    Style="{StaticResource ErrorLabelStyle}"
    Text="{Binding EmailAddress.FirstError}" />

<Button
    Command="{Binding ValidateEmailCommand}"
    Text="Validate" />

This is probably the most common use case - we have:

  • an entry to take in our uer's input (only showing 1 instead of both for each property
  • for brevity)
  • a button that will perform the validation
  • a label showing the first of potential many errors under the entry, or none of course
  • if validation succeeded
  • a behavior for clearing the validation error label once the user activates the entry again (presumably to fix the error)

But you could also a button to clear all the validation at once, or even along with the actual values (clear the whole form), etc. - just give the full example a read-through in the link to the repo, as well as the fully working Xamarin sample project using it (which includes some more advanced examples, e.g. using context-based rules).

Hope this helps...