我正在构建我的第一个Xamarin MvvmCross应用程序,目前我正在考虑验证视图模型的用户输入。
大量搜索所有内容(包括MvvmCross团队)链接到此插件:
此插件使用了非常旧版本的MvvmCross v3。我已经尝试从这个插件中获取代码并将其直接构建到我的应用程序Core项目中,直到我遇到Bindings破坏更改。然后我得出结论,这个插件实际上需要完全重写才能使用最新版本的MvvmCross。
所以我现在有点卡住了。
目前推荐的在视图模型中执行输入验证的最佳方法是什么?
答案 0 :(得分:27)
编辑:在GitHub上添加示例项目 https://github.com/kiliman/mvx-samples/tree/master/MvxSamples.Validation
我使用MVVM验证助手http://www.nuget.org/packages/MvvmValidation/
它是一个易于使用的简单验证库。它与MvvmCross无关。
以下是我如何使用它,例如,在我的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;
}
它的好处在于您可以将错误作为集合获取并在视图中绑定它们。对于Android,我将Error属性设置为键控错误消息。
<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" />
以下是验证的内容:
编辑:显示帮助代码
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));
}
}
}
答案 1 :(得分:1)
实际上没有固定的建议,这是您最满意的建议。我发现很多选项特别冗长(即,即使有一些帮助程序库,也需要大量样板代码)。
我登陆的库是FluentValidation,用于编写规则(它们内置了许多常见的规则,并且是重用/自定义的好方法,包括特定于上下文的规则),并且减少了很多复杂性和所需的代码行,写了我自己的一个小帮助程序库,可以在这里看到(带有示例): FluentValidation MVVM Plugin
该示例使用Prism,但它完全不依赖于任何MVVM框架。
以下是示例:
要构建/验证的类:
public class Email
{
public string RecipientEmailAddress { get; set; }
public string RecipientName { get; set; }
}
使用我的库中提供的Validatable对象和Fody.PropertyChanged 在ViewModel中的属性(这还将为您节省很多INPC的样板代码):< / strong>
public Validatable<string> RecipientName { get; set; } = new Validatable<string>(nameof(Email.RecipientName));
public Validatable<string> EmailAddress { get; set; } = new Validatable<string>(nameof(Email.RecipientEmailAddress));
为该类创建FluentValidation AbstractValidator :
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!");
});
}
}
在ViewModel中实现IValidate:
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);
}
实施命令(以下示例使用Prism的DelegateCommand,但显然不是必需的)以使用这些方法:
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)
}
}
最后,视图(在此示例中为XAML):
<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" />
这可能是最常见的用例-我们有:
但是您也可以使用一个按钮一次清除所有验证,甚至清除实际值(清除整个表单),等等。-只需在回购链接中提供完整的示例通读,以及使用它的功能齐全的Xamarin示例项目(包括一些更高级的示例,例如,使用基于上下文的规则)。
希望这对您有帮助...