2017-04-07 56 views
1

在上週,我一直試圖以最優雅的方式將MVVM模式應用於通用Windows Plataform,這意味着應用SOLID原則和一些流行的設計模式。UWP中的MVVM驗證

我一直在試圖從這個鏈接複製此練習: http://www.sullinger.us/blog/2014/7/4/custom-object-validation-in-winrt

而且鏈接指向它也適用於Windows的Windows 8應用程序10的應用程序根據在這個論壇MSDN答案: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

讓我告訴你我的班,這是我的看法我最後的觀點:

<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博客中顯示的示例我得到一個例外:

System.Reflection.TargetInvocationException:'找不到與此錯誤代碼關聯的文本。

我使用VS2017我想MVVM模式應用到驗證在UWP,我當然可以做驗證我的視圖模型的每一個領域,但它意味着我將不得不寫驗證對每個視圖我創建並據我所知在這個例子中,這可以節省大量的代碼。

有沒有人明白這段代碼有什麼問題?

我不想使用像MVVM Light或MVVM Cross或Prism這樣的工具,這純粹是UWP上的Custom MVVM。

+0

_「......當然,我可以做驗證我的視圖模型的每一個領域,但它意味着我將不得不寫驗證對每個視圖...」 _ - 我傾向於做,因爲使用**系統。** M **上的ComponentModel.DataAnnotations **並在那裏執行驗證。 ** VM **僅僅調用一個輔助函數來讓** M **驗證自己。使用這種方法允許更大的代碼重用 – MickyD

+0

以及我的意思的,在薩林傑博客中,他表現出三種方式,第一種是代碼隱藏的方法,第二種方法是一個我一直在試圖描述他在那裏執行驗證模型並設置也返回結果到視圖的視圖模型。 第三種方法是我沒有能夠解決的問題我不明白爲什麼我會得到這個異常,你能幫助我嗎? –

+0

我建議你避免要求某人把你的工作和完成它 - 我已經從你的問題中刪除了。堆棧溢出是爲了獲得幫助,以便您可以完成您的項目;它不是免費工作的訂購服務。 – halfer

回答

1

好的最後,我能夠使這個代碼工作,它有一些修復,但我能夠理解它,也可以在我自己解決它,然後我發佈我的答案,因爲我有這個問題的兩個答案,我想向社區道歉,我不想要求幫助,這樣你就可以爲我做,我不打算,如果看起來像這樣,我很抱歉,我會嘗試聽起來不那麼需要。

那麼到了解決方案:

我已經公佈了代碼的主要問題是在抽象方法驗證,因爲你得寫自己的驗證每個字段,也這是你控制添加和刪除錯誤信息,所以我寫了一個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); 
    } 

正如你可以看到我一直開始方法刪除所有信息,使我們不加一次消息的更多,因爲添加驗證消息不允許您重複相同的錯誤消息。 然後我們使用我們使用自定義的驗證密碼或電子郵件方式的方法中的AddValidationMessageMethod讓他們返回的消息在這裏所以每次加入我們的自定義方法是我的問題,我返回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集合的空值。

它應該是這樣的:

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挖掘。 也有一些偉大的文章從不同的來源,你可以嘗試薩林傑博客這是我正在採用的做法,但也Wintellect的有一篇文章我將分享這裏的鏈接:http://www.wintellect.com/devcenter/jlikness/simple-validation-with-mvvm-for-windows-store-apps

這也爲UWP我的工作」我已經測試過它,但你必須多做一點。 和傑里尼克鬆對UWP驗證一個偉大的文章太多,他的模型的方式更優雅比薩林傑這是鏈接尼克松驗證: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

也希望這幫助別人。 任何問題,我很樂意幫助你。