c# - UWP中的MVVM验证

标签 c# validation mvvm uwp uwp-xaml

在上周,我一直试图以最优雅的方式将MVVM模式应用于Universal Windows Plataform,这意味着应用SOLID原理和一些流行的设计模式。

我一直在尝试通过以下链接重现此练习:
http://www.sullinger.us/blog/2014/7/4/custom-object-validation-in-winrt

该链接也适用于Windows 8应用程序,根据此论坛上的MSDN答案,该链接也适用于Windows 10应用程序:https://social.msdn.microsoft.com/Forums/windowsapps/en-US/05690519-1937-4e3b-aa12-c6ca89e57266/uwp-what-is-the-recommended-approach-for-data-validation-in-uwp-windows-10?forum=wpdevelop

让我向您展示我的类(class),这是我的最终观点:

<Page
x:Class="ValidationTestUWP.MainPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:ValidationTestUWP"
xmlns:conv="using:ValidationTestUWP.Converters"
xmlns:viewmodels="using:ValidationTestUWP.ViewModel"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d">

<Page.DataContext>
    <viewmodels:AccountCreationViewModel/>
</Page.DataContext>

<Page.Resources>
    <conv:ValidationMessageConverter x:Key="ValidationMessageConverter"/>
</Page.Resources>

    <StackPanel Grid.Row="1"
            VerticalAlignment="Center"
            HorizontalAlignment="Center">

    <!-- E-Mail address input -->
    <TextBlock Text="Email"
               Style="{StaticResource TitleTextBlockStyle}" />
    <TextBox x:Name="EmailTextBox"
             Margin="0 5 0 0"
             MinWidth="200"
             Text="{Binding Path=AppUser.Email, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>

    <!--We now have one more thing to do. We need to update our XAML. 
        The Error TextBlocks will now bind to the ValidationMessages property within the model,
        using an index matching the property they are bound to.-->
    <TextBlock x:Name="EmailValidationErrorTextBlock"
               Text="{Binding AppUser.ValidationMessages[Email], Converter={StaticResource ValidationMessageConverter}}"
               Foreground="Red" />

        <!-- Password input -->
    <TextBlock Text="Password"
               Margin="0 30 0 0"
               Style="{StaticResource TitleTextBlockStyle}"/>
    <TextBox x:Name="PasswordTextBox"
             Margin="0 5 0 0"
             MinWidth="{Binding ElementName=EmailTextBox, Path=MinWidth}"
             Text="{Binding Path=AppUser.ValidationMessages[Password], Converter={StaticResource ValidationMessageConverter}}"/>

    <TextBlock x:Name="PasswordValidationToShortErrorTextBlock"
               Text="{Binding PasswordToShortError}"
               Foreground="Red" />
    <TextBlock x:Name="PasswordValidationToLongErrorTextBlock"
               Text="{Binding PasswordToLongError}"
               Foreground="Red" />

    <!-- Login command button -->
    <Button Content="Create Account"
            Margin="0,10, 0, 0"
            Command="{Binding CreateAccount}"/>
</StackPanel>
</Page>

我的模型最终看起来像这样:(同样,我还在类的注释中添加了对此类的解释。)
public class User : ValidatableBase
{
    private string email = string.Empty;

    public string Email
    {
        get { return email; }
        set
        {
            email = value;
            OnPropertyChanged("Email");
        }
    }

    private string password = string.Empty;

    public string Password
    {
        get { return password; }
        set
        {
            password = value;
            OnPropertyChanged("Password");
        }
    }

    /*Now that we are inheriting from our base class, we need to implement the required Validate() method.
     * In order to keep with the Single-Responsibility-Principle, we will invoke other methods from within
     * the Validate() method. 
     * Since we have to validate multiple properties, we should have each property validation be contained 
     * within it's own method. This makes it easier to test.*/
    public override void Validate()
    {
        ValidatePassword("Password");
        //base.OnPropertyChanged("Password");
        ValidateEmail("Email");
        //base.OnPropertyChanged("Email");

        // Passing in an empty string will cause the ValidatableBase indexer to be hit.
        // This will let the UI refresh it's error bindings.
        base.OnPropertyChanged(string.Empty);
    }

    /*Here we just invoke a ValidatePassword and ValidateEmail method. 
     * When we are done, we notify any observers that the entire object has changed by not specifying a property name
     * in the call to OnPropertyChanged. 
     * This lets the observers (in this case, the View) know its bindings need to be refreshed.*/
    private IValidationMessage ValidateEmail(string property)
    {
        const string emailAddressEmptyError = "Email address can not be blank.";
        if (string.IsNullOrEmpty(this.Email))
        {
            var msg = new ValidationErrorMessage(emailAddressEmptyError);
            return msg;
        }

        return null;
    }

    private IValidationMessage ValidatePassword(string property)
    {
        const string passwordToShortError = "Password must a minimum of 8 characters in length.";
        const string passwordToLongError = "Password must not exceed 16 characters in length.";
        if (this.Password.Length < 8)
        {
            var msg = new ValidationErrorMessage(passwordToShortError);
            return msg;
        }
        if (this.Password.Length > 16)
        {
            var msg = new ValidationErrorMessage(passwordToLongError);
            return msg;
        }

        return null;
    }

这是我的ViewModel:
/*View Model 
 * 
 * Next, we need to revise our View Model.
 * We will delete all of the error properties within it, along with the INotifyPropertyChanged implementation.
 * We will only need the AppUser property and the ICommand implementation.*/
public class AccountCreationViewModel
{
    public AccountCreationViewModel()
    {
        this.AppUser = new User();
        CreateAccount = new MyCommand(CreateUserAccount);
    }

    private User appUser;

    public event EventHandler CanExecuteChanged = delegate { };
    public MyCommand CreateAccount { get; set; }

    public User AppUser
    {
        get { return appUser; }
        set
        {
            appUser = value;
        }
    }

    private void CreateUserAccount()
    {
        AppUser.Validate();

        if (AppUser.HasValidationMessageType<ValidationErrorMessage>())
        {
            return;
        }
        // Create the user
        // ......
    }

    /*Now, when you run the app and enter an invalid Email or Password,
     * the UI will automatically inform you of the validation errors when you press the Create Account button. 
     * If you ever need to add more Email validation (such as the proper email format) 
     * or more Password validation (such as not allowing specific characters) you can do so without needing
     * to modify your View Model or your View.
     * 
     * If you need to add a whole new property to the Model, with validation, you can. You don't need to modify
     * your View Model, you only need to add a TextBlock to the View to display the validation.*/
}

我还应用了RelayCommand模式:
public class MyCommand : ICommand
{
    Action _TargetExecuteMethod;
    Func<bool> _TargetCanExecuteMethod;

    public MyCommand(Action executeMethod)
    {
        _TargetExecuteMethod = executeMethod;
    }

    public MyCommand(Action executeMethod, Func<bool> canExecuteMethod)
    {
        _TargetExecuteMethod = executeMethod;
        _TargetCanExecuteMethod = canExecuteMethod;
    }

    public void RaiseCanExecuteChanged()
    {
        CanExecuteChanged?.Invoke(this, EventArgs.Empty);
    }

    /*Beware - should use weak references if command instance lifetime 
    is longer than lifetime of UI objects that get hooked up to command*/
    // Prism commands solve this in their implementation public event 
    public event EventHandler CanExecuteChanged = delegate { };

    public bool CanExecute(object parameter)
    {
        if (_TargetCanExecuteMethod != null)
            return _TargetCanExecuteMethod();

        if (_TargetExecuteMethod != null)
            return true;

        return false;
    }

    public void Execute(object parameter)
    {
        /*This sintax use the null*/
        _TargetExecuteMethod?.Invoke();
    }
}

这是乐趣的开始,我将介绍在我先前显示的博客上创建的ValidatableBase:
public abstract class ValidatableBase : IValidatable, INotifyPropertyChanged
{
    /*Our initial class contains the Dictionary that will hold our validation messages. 
     * Next, we implement the read-only property required by our interface.*/
    private Dictionary<string, List<IValidationMessage>> _validationMessages = 
        new Dictionary<string, List<IValidationMessage>>();

    /*The call to OnPropertyChanged will let the UI know that this collection has changed. 
     * This in most cases won't be used since the collection is read-only, but since it is going in to a base class,
     * we want to provide support for that.*/
    public Dictionary<string, List<IValidationMessage>> ValidationMessages
    {
        get { return _validationMessages; }
        set
        {
            _validationMessages = value;
            OnPropertyChanged("ValidationMessages");
        }
    }

    public event PropertyChangedEventHandler PropertyChanged = delegate { };

    /*our base class implements the INotifyPropertyChanged method, 
     * so we will remove it from our model and put the implementation in to our base class.*/
    public void OnPropertyChanged(string propertyName)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }

    /*In this method, we check if the collection contains a Key matching the property supplied.
     * If it does, then we check it's values to see if any of them match the Type specified in < T>.
     * This lets you do something like
     * 
     * HasValidationMessageType< ValidationErrorMessage>("Email");
     * 
     * to check if the model has a validation error on the email property.*/
    public bool HasValidationMessageType<T>(string property = "")
    {
        if (string.IsNullOrEmpty(property))
        {
            bool result = _validationMessages.Values.Any(collection =>
                collection.Any(msg => msg is T));
            return result;
        }

        return _validationMessages.ContainsKey(property);
    }

    /*In this method we create a new collection if the key doesn't exist yet,
     * we then double check to ensure this validation message does not already exist in the collection.
     * If not, we add it.*/
    public void AddValidationMessage(IValidationMessage message, string property = "")
    {
        if (string.IsNullOrEmpty(property))
        {
            return;
        }

        // If the key does not exist, then we create one.
        if (!_validationMessages.ContainsKey(property))
        {
            _validationMessages[property] = new List<IValidationMessage>();
        }

        if (_validationMessages[property].Any(msg => msg.Message.Equals(message.Message) || msg == message))
        {
            return;
        }

        _validationMessages[property].Add(message);
    }

    /*Here we just check if there is any message for the supplied Key and remove it.
     * At the moment, this does not do any Type checking to see if there is more 
     * than one Type of object (Warning and Error) in the collection with the same message.
     * The method just removes the first thing it finds and calls it good.*/
    public void RemoveValidationMessage(string message, string property = "")
    {
        if (string.IsNullOrEmpty(property))
        {
            return;
        }

        if (!_validationMessages.ContainsKey(property))
        {
            return;
        }

        if (_validationMessages[property].Any(msg => msg.Message.Equals(message)))
        {
            // Remove the error from the key's collection.
            _validationMessages[property].Remove(
                _validationMessages[property].FirstOrDefault(msg => msg.Message.Equals(message)));
        }
    }

    /*We just check if a key exists that matches the property name and then clear out its messages contents
     * and remove the key from the Dictionary.*/
    public void RemoveValidationMessages(string property = "")
    {
        if (string.IsNullOrEmpty(property))
        {
            return;
        }

        if (!_validationMessages.ContainsKey(property))
        {
            return;
        }

        _validationMessages[property].Clear();
        _validationMessages.Remove(property);
    }

    /*Finally, we finish implementing the interface by building the ValidateProperty method.
     * In this method, we just invoke the delegate we are provided, and accept a IValidationMessage object in return.
     * If the return value is not null, then we add it to the ValidationMessages collection.
     * If it is null, then we can assume that the validation passed and there are no issues.
     * Since that is the case, we remove it from the validation collection.*/
    public IValidationMessage ValidateProperty(Func<string, IValidationMessage> validationDelegate,
        string failureMessage, string propertyName = "")
    {
        IValidationMessage result = validationDelegate(failureMessage);
        if (result != null)
        {
            this.AddValidationMessage(result, propertyName);
        }
        else
        {
            this.RemoveValidationMessage(failureMessage, propertyName);
        }

        return result;
    }

    /*We have satisfied the requirements of the IValidatable interface, but there is one more method
     * we need to add to the base class. This will let us group all of our property validations in to a single call.
     * 
     * We mark it as abstract, since the base class has nothing to validate, 
     * and we want to force any object that inherits from the base class to implement the method. 
     * If you don't want to do this, you can opt out of in your code. Not everyone needs to have this feature,
     * thus the reason why it was left out of the interface.*/
    public abstract void Validate();
}

最后,这是我的界面:
//The first thing I did was created an interface that all models needing validation would be required to implement. 
public interface IValidatable
{
    /*This is a read-only property, that will contain all of our validation messages. 
     * The property has a Key typed to a string, which will be the Models property name. 
     * The value is a collection of IValidationMessage objects (We will discuss what the IValidationMessage is later).
     * The idea being that for each property in the model, we can store more than 1 error.*/
    Dictionary<string, List<IValidationMessage>> ValidationMessages { get; }

    /*This method is used to add a validation message to the ValidationMessages collection.
     * The property will be assigned as the Key, with the message being added as the value.*/
    void AddValidationMessage(IValidationMessage message, string property = "");

    /*Just like we can add a validation message, we will provide ourselves with the ability to remove it.*/
    void RemoveValidationMessage(string message, string property = "");

    /*We can use this method to completely clear out all validation messages in one shot for a single property.*/
    void RemoveValidationMessages(string property = "");

    /*This method will return true if the object has validation messages matching <T> and false if it does not.*/
    bool HasValidationMessageType<T>(string property = "");

    /*This method can be called to actually perform validation on a property within the object and 
     * build the collection of errors. The arguments require a method delegate that returns an IValidationMessage object.
     * This is how the validation becomes reusable. Each individual object can pass in a method delegate that performs
     * the actual validation. The IValidatable implementation will take the results and determine if it must go in to
     * the ValidationMessages collection or not.*/
    IValidationMessage ValidateProperty(Func<string, IValidationMessage> validationDelegate, 
        string failureMessage,
        string propertyName = "");
}

/*The idea with this, is that we can create objects that implement this interface,
 * but containing different types of messages. For instance, in this post, we will create a ValidationErrorMessage
 * and a ValidationWarningMessage. You could go on and create any kind of messaging you want and use it
 * for binding to the View.*/
public interface IValidationMessage
{
    string Message { get; }
}

这是我的转换器:
/*The idea with this, is that we can create objects that implement this interface,
 * but containing different types of messages. For instance, in this post, we will create a ValidationErrorMessage
 * and a ValidationWarningMessage. You could go on and create any kind of messaging you want and use it
 * for binding to the View.*/
public interface IValidationMessage
{
    string Message { get; }
}

最后是我的ValidationErrorMessages:
 /*Before we end the post, I will show you two implementations of the IValidationMessage.
 * They both do the same thing, but are Typed differently so that you can segregate your messages by Type.
 * This gives more flexibility that using an Enum.
 * 
 * First is the Error validation message.*/
public class ValidationErrorMessage : IValidationMessage
{
    public ValidationErrorMessage() : this(string.Empty)
    { }

    public ValidationErrorMessage(string message)
    {
        this.Message = message;
    }

    public string Message { get; private set; }
}

现在,每次我在Sullinger Blog上显示的示例中运行类似此代码的代码时,都会得到一个异常:

System.Reflection.TargetInvocationException: 'The text associated with this error code could not be found.



我正在使用VS2017,正在尝试将MVVM模式应用于UWP中的验证,当然我可以对每个字段的ViewModel进行验证,但这意味着我必须为所创建的每个 View 编写验证,直到我在此示例中看到,这可以节省大量代码。

有人知道这段代码有什么问题吗?

我不想使用MVVM Light或MVVM Cross或Prism等工具,这纯粹是UWP上的自定义MVVM。

最佳答案

好的,最终我能够使此代码正常工作,它已进行了一些修复,但我能够自己理解并自行解决,在发布答案之前,因为我对此问题有两个答案,我深表歉意对于社区,我不想寻求帮助,以便您可以为我做这件事,这不是我的意图,如果看起来像这样,对不起,我会尽量减少您的需要。

深入解决方案:

我发布的代码的主要问题是抽象方法Validate,因为您必须为每个字段编写自己的验证,并且在这里您可以控制错误消息的添加和删除,所以我编写了一个Validate方法,例如一:

public override void Validate()
    {
        RemoveValidationMessages("Password");
        RemoveValidationMessages("Email");
        AddValidationMessage(ValidatePassword("Password"), "Password");
        AddValidationMessage(ValidateEmail("Email"), "Email");

        // Passing in an empty string will cause the ValidatableBase indexer to be hit.
        // This will let the UI refresh it's error bindings.
        base.OnPropertyChanged(string.Empty);
    }

如您所见,我总是开始删除所有消息的方法,以便我们不会添加多个消息,因为addvalidation消息不允许您重复相同的错误消息。
然后,在方法中使用AddValidationMessageMethod,将密码或电子邮件用于自定义验证方法,以便它们返回一条消息,以将其添加到自定义方法中,这是我的问题,我向消息返回null,因此每次触发转换器时,都会引发我在我的问题上显示了异常(exception)。
因此,为了解决此问题,而不是在文本框包含一些文本时不返回null,我返回了ValidationErrorMessages类的空构造函数,如下所示:
private IValidationMessage ValidateEmail(string property)
    {
        const string emailAddressEmptyError = "Email address can not be blank.";
        if (string.IsNullOrEmpty(this.Email))
        {
            var msg = new ValidationErrorMessage(emailAddressEmptyError);
            return msg;
        }

        return new ValidationErrorMessage();
    }

    private IValidationMessage ValidatePassword(string property)
    {
        const string passwordToShortError = "Password must a minimum of 8 characters in length.";
        const string passwordToLongError = "Password must not exceed 16 characters in length.";
        if (this.Password.Length < 8)
        {
            var msg = new ValidationErrorMessage(passwordToShortError);
            return msg;
        }
        if (this.Password.Length > 16)
        {
            var msg = new ValidationErrorMessage(passwordToLongError);
            return msg;
        }

        return new ValidationErrorMessage();
    }

这样解决了触发异常的问题。
您也可以让此方法返回null,但是您必须修改转换器,以便它检查IValidationMessages集合是否为null值。

它看起来像这样:
 public object Convert(object value, Type targetType, object parameter, string language)
    {
        if (!(value is IEnumerable<IValidationMessage>))
        {
            return string.Empty;
        }

        var collection = value as IEnumerable<IValidationMessage>;
        if (!collection.Any())
        {
            return string.Empty;
        }

        if (collection.FirstOrDefault() == null)
        {
           return string.Empty;
        }           

        return collection.FirstOrDefault().Message;
    }

这样,我们可以更新字段的错误消息,现在您可以拥有适用于UWP的MVVM验证工作模式。具有高度可测试,可维护和可扩展的应用程序。

希望此解决方案对那些在UWP进行挖掘的人有所帮助。
另外,还有来自许多不同来源的精彩文章,您可以尝试使用sullinger博客,这是我正在采用的方法,但是wintellect也有一篇文章,我将在此处分享其链接:http://www.wintellect.com/devcenter/jlikness/simple-validation-with-mvvm-for-windows-store-apps

这也适用于我已经测试过的UWP,但是您需要多做一些工作。
杰里·尼克松(Jerry Nixon)也有一篇有关UWP Validation的好文章,他的模型比Sullinger优雅得多,这是尼克松验证的链接:http://blog.jerrynixon.com/2014/07/lets-code-handling-validation-in-your.html

他在这里有源代码:http://xaml.codeplex.com/SourceControl/latest#Blog/201406-Validation/App10/Common/ModelBase.cs

希望这对别人有帮助。
如有任何疑问,我也很乐意为您提供帮助。

关于c# - UWP中的MVVM验证,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43268648/

相关文章:

listview - 如何在 Listview 上使用 ScrollTo 方法

c# - 通过 sql profiler 测试 Web API 服务?

javascript - DatePicker 验证 Jquery 如何验证

html - 尝试修复 w3 验证器错误和警告

java - 我如何知道用户是否在 Java 的 Swing 中插入了有效的日期?

c# - 如何将 Caliburn.Micro.Logging 与 Ninject.Logging & log4net 结合使用

mvvm - 测试 ViewState 的 PublishSubject

c# - .NET Core Web应用程序启动类中的app.UseMigrationsEndPoint做什么

c# - 比较带引号的字符串

c# - 错误 "CommentsController does not have a default constructor"