Xamarin MvvmCross ViewModel验证

时间:2015-06-11 10:54:12

标签: validation mvvm xamarin mvvmcross

我正在构建我的第一个Xamarin MvvmCross应用程序,目前我正在考虑验证视图模型的用户输入。

大量搜索所有内容(包括MvvmCross团队)链接到此插件:

MVVMCross.Plugins.Validation

此插件使用了非常旧版本的MvvmCross v3。我已经尝试从这个插件中获取代码并将其直接构建到我的应用程序Core项目中,直到我遇到Bindings破坏更改。然后我得出结论,这个插件实际上需要完全重写才能使用最新版本的MvvmCross。

所以我现在有点卡住了。

目前推荐的在视图模型中执行输入验证的最佳方法是什么?

2 个答案:

答案 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" />

以下是验证的内容:

Validation message

编辑:显示帮助代码

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" />

这可能是最常见的用例-我们有:

  • 要接受我们输入的条目(每个属性仅显示1而不是两个
  • 为简洁起见
  • 将执行验证的按钮
  • 一个标签,显示条目下潜在的许多错误中的第一个,或者当然没有一个错误
  • 如果验证成功
  • 用户再次激活条目后清除验证错误标签的行为(大概是为了纠正错误)

但是您也可以使用一个按钮一次清除所有验证,甚至清除实际值(清除整个表单),等等。-只需在回购链接中提供完整的示例通读,以及使用它的功能齐全的Xamarin示例项目(包括一些更高级的示例,例如,使用基于上下文的规则)。

希望这对您有帮助...