2017-04-07 11 views
1

先週、私は、最も優雅な可能な方法でMVVMパターンをユニバーサルWindows Plataformに適用しようとしていました。これは、SOLID原理といくつかの一般的なデザインパターンを適用することを意味します。UVMのMVVM検証

私は、このリンクから、この運動を再現しようとしてきた: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

http://www.sullinger.us/blog/2014/7/4/custom-object-validation-in-winrt

またリンクは、それはまた、Windowsには、このフォーラムでのMSDNの答えに応じて10のアプリを適用するWindows 8のアプリのためのものです

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

は私のモデルは、このように見える終わる:

私はあなたに私のクラスをお見せしましょう、これは私の見解私の最後の図である(また、私は番目のコメントの中に、このクラスの説明を追加しています。私はRelayCommandパターンを適用しました。また

/*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.*/ 
} 

:Eクラス)

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ある

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: 'このエラーコードに関連付けられたテキストが見つかりませんでした。

私は、私はすべてのフィールドのための私のViewModelに検証を行うことができ、もちろん、私はUWPでの検証にMVVMパターンを適用しようとしているVS2017を使用していますが、それは私がすべてのビューのための検証を記述する必要がありますことを意味し私は作成し、この例で見る限り、これは私にたくさんのコードを節約することができます。

誰でもこのコードが間違っていることを理解していますか?

私はMVVM LightやMVVM CrossやPrismのようなツールを使いたくありません。これは純粋にUWP上のカスタムMVVMです。

+0

_ "もちろん、すべてのフィールドのViewModelで検証を行うことはできますが、それはすべてのビューに対して検証を記述する必要があることを意味します..." .ComponentModel.DataAnnotations **を** M **に追加し、そこで検証を実行します。 ** VM **はヘルパー関数を呼び出して** M **に自身を検証するように要求するだけです。このアプローチを使うとコードの再利用が可能になります – MickyD

+0

私はSullingerのブログで、それが意味することは、彼が3つのアプローチを示していることです。最初はコードビハインドアプローチで、2つ目のアプローチは、モデルの検証を行い、結果をビューに返すviewmodelを設定します。 3番目のアプローチは私が解決することができたhaventです私はなぜこの例外を得ているのか分かりません、あなたは私を助けることができますか? –

+0

私は、あなたの仕事を遂行して終了するように頼むことを避けることをお勧めします - 私はあなたの質問からそれを削除しました。スタックオーバーフローは、プロジェクトを完了できるように支援を得ることです。それは自由な仕事のための注文サービスではありません。 – halfer

答えて

1

[OK]最後に私はこの問題を解決するために2つの答えがあるので、私は答えを公表する前に、私はコミュニティに謝罪したい、私はあなたが私のためにそれを行うことができるように助けを求めていない、それは私の意図ではなかったように見えた場合、私は申し訳ありませんが、私は貧しい人々を聞こうとします。まあ溶液へのダウン

私が公開したコードの主な問題は、抽象メソッドValidateである、あなたは、各フィールドの独自の検証を書いても、追加の制御これはどこで得たので、そして、エラーメッセージの除去に、私はこの1のような検証方法を書いた:あなたはので、私たちは、メッセージを複数回追加しないように、私は常にすべてのメッセージを削除する方法を開始しました見ることができるよう

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を返します。私の質問で私が示した例外。 は、だからではなく、テキストボックスには、いくつかのテキストを持っていたときは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ブログを試すことができますが、また、私はここにリンクを共有します記事がありwi​​ntellect:http://www.wintellect.com/devcenter/jlikness/simple-validation-with-mvvm-for-windows-store-apps

これもUWP私のために働きます」それをテストしましたが、もう少し作業しなければなりません。 とジェリーニクソンがあまりにもUWP検証のための素晴らしい記事を持って、彼のモデルは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

もこのことを願っています他の人を助ける。 ご質問がありましたらお気軽にお手伝いします。

関連する問題