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?
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:
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));
}
}
}
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:
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...
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With