Search code examples
validationmvvmxamarinmvvmcross

Xamarin MvvmCross ViewModel Validation


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?


Solution

  • 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));
            }
        }
    }