locked
ValidationSummaryに検証結果が表示されない RRS feed

  • 質問

  • 「データ入力検証の自作クラスとのバインドしたときのデバッグ実行について」のタイトルで質問して色々教えていただいたことによりTextBoxと自作クラスをバインドさせて検証結果を通知することができました。

    "ValidationSummary"にも検証結果を表示させようとしたのですがエラーが通知されておらず"DisplayedErrors"、"Errors"ともに0件になり表示されません。

    自作クラスに問題あるのか、"ValidationSummary"の設定に問題があるのか問題点を特定できずにいます。

    初歩的な問題かもしれませんがよろしくご教授いただければと思います。

    =クラス=

    public class BusinessConnectionRow : ObjectModelBase
        {
            private string companyName;                 // 
            private bool isKanaCompanyName;
            
            ~略~
    
            private Nullable<Decimal> annualSale;                    //  
    
            public BusinessConnectionRow()
            {
                this.AllClear();
            }
            
            
            ~略~
            
            public string CompanyName
            {
                get { return this.companyName; }
                set
                {
                    if (this.companyName != value)
                    {
                        this.companyName = value;
                        OnPropertyChanged("CompanyName", "IsKanaCompanyName");
                    }
                }
            }
    
            public bool IsKanaCompanyName
            {
                get { return this.isKanaCompanyName; }
                set
                {
                    if (this.isKanaCompanyName != value)
                    {
                        this.isKanaCompanyName = value;
                        OnPropertyChanged("IsKanaCompanyName", "CompanyName");
                    }
                }
            }
            
            
            [RegularExpression(@"^([1-9]\d*|0)(\.\d+)?$", ErrorMessage = "金額を入力してください")]
            [Display(Description = "金額を入力してください")]
            public Nullable<Decimal> AnnualSale
            {
                get { return this.annualSale; }
                set
                {
                    if (this.annualSale != value)
                    {
                        this.annualSale = value;
                        OnPropertyChanged("AnnualSale");
                    }
                }
            }
        }
    
    
        public class ObjectModelBase : INotifyPropertyChanged, INotifyDataErrorInfo
        {
            public event PropertyChangedEventHandler PropertyChanged;
            public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
    
            public System.Collections.IEnumerable GetErrors(string propertyName)
            {
                var result = new List<ValidationResult>();
                // TryValidatePropertyが適任。プロパティの値を取得するのにリフレクションを
                // 使っている部分が気に入らない
    
                switch (propertyName)
                {
                    case "CompanyName":
                    case "IsKanaCompanyName":
                        if ((bool)GetType().GetProperty("IsKanaCompanyName").GetValue(this, null)
                            && GetType().GetProperty("CompanyName").GetValue(this, null) != null
                            && !string.IsNullOrEmpty(GetType().GetProperty("CompanyName").GetValue(this, null).ToString())
                            && !Regex.IsMatch(GetType().GetProperty("CompanyName").GetValue(this, null).ToString(), @"^[ア-ンガ-ボァ-ョヮッー]*$"))
                            return new[] { "全角カナで入力してください。" };
                        else
                            return null;
    
                    case "OfficeName":
                    case "IsKanaOfficeName":
                        if ((bool)GetType().GetProperty("IsKanaOfficeName").GetValue(this, null)
                            && GetType().GetProperty("OfficeName").GetValue(this, null) != null
                            && !string.IsNullOrEmpty(GetType().GetProperty("OfficeName").GetValue(this, null).ToString())
                            && !Regex.IsMatch(GetType().GetProperty("OfficeName").GetValue(this, null).ToString(), @"^[ア-ンガ-ボァ-ョヮッー]*$"))
                            return new[] { "全角カナで入力してください。" };
                        else
                            return null;
                }
    
    
                if (GetType().GetProperty(propertyName) == null) { return null; }
    
                if (Validator.TryValidateProperty(
                    GetType().GetProperty(propertyName).GetValue(this, null),
                    new ValidationContext(this, null, null) { MemberName = propertyName },
                    result))
                {
                    return null;
                }
                return result.Select(vr => vr.ErrorMessage);
            }
    
       
    
            protected virtual void OnPropertyChanged(string propertyName)
            {
                RaisePropertyChanged(propertyName);
                RaiseErrorChanged(propertyName);
    
                // HasErrorsプロパティにも変更があったことを通知する
                RaisePropertyChanged("HasErrors");
            }
    
    
            protected virtual void OnPropertyChanged(string propertyName, string validPropertyName)
            {
                RaisePropertyChanged(propertyName);
                RaiseErrorChanged(propertyName);
                RaiseErrorChanged(validPropertyName);
    
                // HasErrorsプロパティにも変更があったことを通知する
                RaisePropertyChanged("HasErrors");
            }
    
            private void RaisePropertyChanged(string propertyName)
            {
                var h = PropertyChanged;
                if (h == null) return;
                h(this, new PropertyChangedEventArgs(propertyName));
            }
    
            private void RaiseErrorChanged(string propertyName)
            {
                var h = ErrorsChanged;
                if (h == null) return;
                h(this, new DataErrorsChangedEventArgs(propertyName));
            }
    
            public bool HasErrors
            {
                get
                {
                    var dummy = new List<ValidationResult>();
                    
                    return !Validator.TryValidateObject(
                        this,
                        new ValidationContext(this, null, null),
                        dummy);
                }
            }
    


    =XAML=

     		    <StackPanel Grid.Row="0" Grid.Column="1" Style="{StaticResource SearchStackPanelStyle}">
                            <TextBlock Text="{Binding Path=BusinessConnectionStrings.CompanyNameLabel, Source={StaticResource ResourceWrapper}}" Style="{StaticResource HeaderTextBlockStyle}"/>
                            <c1:C1TextBoxBase Style="{StaticResource InputC1TextBoxStyle}" 
                                              Text="{Binding CompanyName, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}"/>
                            <CheckBox Content="カナ" Style="{StaticResource InputCheckBoxStyle}"
                                      IsChecked="{Binding IsKanaCompanyName, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True}"/>
                        </StackPanel>
    					~略~
     					<StackPanel Grid.Row="5" Grid.Column="0" Style="{StaticResource SearchStackPanelStyle}">
                            <TextBlock Text="売上高(百万円):" Style="{StaticResource HeaderTextBlockStyle}"/>
                            <c1:C1TextBoxBase Style="{StaticResource InputC1TextBoxStyle}"
                                              Text="{Binding AnnualSale, Converter={StaticResource NullableConverter}, Mode=TwoWay, NotifyOnValidationError=True, ValidatesOnDataErrors=True, ValidatesOnExceptions=True}"/>
                        </StackPanel>
                        ~略~	
                         <StackPanel Grid.Row="7" Grid.Column ="1" Style="{StaticResource SearchStackPanelStyle}">
                            <sdk:ValidationSummary x:Name="validationSummary" Visibility="Visible" ></sdk:ValidationSummary>
                        </StackPanel>

    =CS=

    public EditBusinessConnectionControl()
    {
                InitializeComponent();
    
                this.searchParm = new BusinessConnectionRow();
                this.DataContext = this.searchParm;
    }
    


     

     

    2011年10月13日 9:42

回答

  • 自己解決しました。

    "ValidationSummary"は"StackPanel"の中にあるとエラーが通知されないようです。

    Tetsuaki Uchida 様色々とご教授いただきありがとうございました。

    本件はこれで終了とさせていただきますが、先の投稿で質問させていただきました内容につきましてご教授いただけることがございましたらよろしくお願いいたします。

    2011年10月17日 2:06
  • こんにちは。
    解決された様で何よりです。

    >"ValidationSummary"は"StackPanel"の中にあるとエラーが通知されないようです。

    「おや」と思いもう1度ValidationSummaryクラスをきちんと確認したところ

    私の返信した

    >ValidationSummaryはDataContextのErrorsChangedイベントを購読しますので、
    >TextBoxとDataContextが違えば通知されないと思います。

    大嘘でした。ごめんなさい。

    ValidationSummary は、その親コンテナーの BindingValidationError イベントを受け取ります。

    という記述がきちんとありました。
    なので、
    エラーコントロールの先祖のどれかの直子じゃないと表示してくれないですね。ミスリードしてすみませんでした。

    あと、CustomValidatorの件ですが、
    質問文から読み取れるだ要件は
    ・CompanyNameは必須
    ・IsKanaCompanyNameがTrueの場合(チェックボックスにチェックが入っている)場合のみCompanyNameは全角カナで無ければならない
    ・チェックはCompanyNameが変化した時と、IsKanaCompanyNameが変化したときに行う
    ・エラーの表示はCheckBoxには出さず、CompanyNameを入力するTextBoxにのみ表示する

    ということかなと思います。
    #ソース見た限りだとIsKanaCompanyNameがTrueじゃないとCompanyNameの必須チェックが走らなそうな気がしますが、
    #上の要件の方が自然だと(勝手に)思うのでこの要件での実装例を考えてみます。

    ポイントは
    ・CompanyNameとIsKanaCompanyNameの変化時に同じ検証ロジックを通す
    ・どちらから呼ばれた検証ロジックもCompanyNameのエラーとして通知する
    ・全角カナチェックは条件付きなので、RegularExpressionAttributeは使えないのでCustomValidatorAttributeを使う

    を押さえればいけると思います。

    全部書くと長いのでかいつまんで書けば

    //カスタム検証の実装を記述したクラス
    public static class CustomValidator
    {
        public static ValidationResult ValidateCompanyName(string value, ValidationContext context)
        {
            var vm = context.ObjectInstance as MainViewModel;
            if (vm == null) throw new ArgumentException("context");
    
            if (vm.IsKanaCompanyName && !Regex.IsMatch(value , @"^[ア-ンガ-ボァ-ョヮッー]*$"))
            {
                return new ValidationResult("全角カナで入力してください",  new string[] { "CompanyName" } );
            }
            return ValidationResult.Success;
            }
    }
       
    public class BusinessConnectionRow : ObjectModelBase
    {
    //~~
            //属性を使った検証を行うメソッド
            private void ValidateProperty(string propertyName,  string value)
            {
              // ※Dictionaryクラス等でViewModel内にエラーを保存しておく
              
                var result = new List<ValidationResult>();
                if (Validator.TryValidateProperty(value, new ValidationContext(this, null, null) { MemberName = propertyName }, result))
                {
                    //エラーをクリアする 
                }
                else
                {
                    //エラーに追加する
                }
                //エラー変更通知を出す
                RaiseErrorsChanged(propertyName);
            }
    
    
    //~~
            [Required]
            [CustomValidation(typeof(CustomValidator), "ValidateCompanyName")] //CustomValidatorクラスを指定する
            public string CompanyName
            {
                get
                {
                    return _CompanyName;
                }
                set
                {
                    if (value == _CompanyName) return;
    
                    _CompanyName = value;
                    //検証メソッドを呼ぶ
                    ValidateProperty("CompanyName", value);
                    RaisePropertyChanged("CompanyName");
                }
            }
    //~~
            public bool IsKanaCompanyName
            {
                get
                {
                    return _IsKanaCompanyName;
                }
                set
                {
                    if (value == _IsKanaCompanyName) return;
    
                    _IsKanaCompanyName = value;
                    //CompanyNameの名前と値で検証メソッドを呼ぶ
                    ValidateProperty("CompanyName", CompanyName);
                    RaisePropertyChanged("IsKanaCompanyName");
                }
            }
    

           
    こんな感じで書くとすっきりするのではと思うので是非試してみてください。
    2011年10月17日 17:19

すべての返信

  • TextBoxのBindingプロパティで、NotifyOnValidationError=true を指定してはどうでしょうか。

    ショッピング サイト (Silverlight 実装) ~アプリケーション実装のポイント~ 7. 入力データの検証

    によると、BindingValidationError イベントが発生し、それをValidationSummaryコントロールが受け取ると読めます。

    実際、私はこの方法でValidationSummaryを使っています。

    2011年10月13日 10:58
  • ご返信ありがとうございます。

    TextBoxのBindingプロパティのNotifyOnValidationErrrorは既にTrueにさせていただいておりますので、TextBoxへのエラーの通知までは問題ありません。

    今回の問題はどうしてValidationSummaryでエラーを受け取ることでつまづいています。

    今回検証用のクラスを作成するにあたりご紹介いただいているサイトなどを参考にさせていただいております。

    ただこのサイトのやり方だとエラー通知がされず、この前に質問させていただいたときに"INotifyPropertyChanged"を実装することを教えていただきました。

    よろしくお願いいたします。

     

     

    2011年10月14日 0:28
  • こんにちは。

    Xaml全体がわからないの予想ですが、おそらく
    CompanyNameをバインドしているTextBoxのDataContextと、ValidationSummaryのDateContextが違うということは無いですか?
    「BusinessConnectionRow」という名前からの想像ですが、なにかしらのItemsControlのItemにバインドされていませんか?

    ValidationSummaryはDataContextのErrorsChangedイベントを購読しますので、
    TextBoxとDataContextが違えば通知されないと思います。

    あと、本筋では無いですがObjectModelBase クラスの実装が結構無茶苦茶です。

    INotifyDataErrorInfoは

    ・ViewModelが検証を行い、エラーを作成する(または削除する)

    ・ViewModelがErrorsChangedを発行する

    ・ViewがErrorsChangedの通知を受け取り、VMのGetErrorsを呼び出してエラーを確認する

    ・Viewがエラーの有無によってUIを変更する

    のシーケンスになるのが正しい実装になるかと思います。
    質問文の実装だと、GetErrorsメソッド内で検証をしてしまっているので
    GetErrorsが呼ばれる度に検証が実行されてしまいます。

    また、HasErrorsプロパティの中でも検証が実装されています(しかもGetErrorsと異なるロジックで)
    これだと、GetErrorsではエラーが返るのにHasErrorsはfalseということが起こってしまいます。

    1度、TextBox1個とINotifyPropertyChanged、INotifyDataErrorInfoだけを実装したクラスでデバッグして、
    基本的な動きを確認することをお勧めします。

    # もう1つ余談ですが、せっかく1部ValidationAttributeを使用して検証しているので、
    # 可能ならCustomValidationAttributeを使用するなどして全部ValidationAttributeに統一した方が可読性が上がると思います。
    2011年10月16日 12:24
  • ご返信ありがとうございます。

    TextBoxとValidationSummaryのDataContextは同じになります。(BusinessConnectionRowで同じ値になっていることをデバッグで確認しました。)

    "this.DataContext"にBusinessConnectionRowのオブジェクトである"this.searchParam"をセットしているだけでXAMLには何も記述しておりません。

    DataContextに検証するオブジェクトをセットする→INotifyDataErrorInfoの機能でエラー内容を収集する→INotifyDataErrorInfo、ValidationSummaryの機能で自動的に伝達するの流れになり、TextBoxに伝わればValidationSummaryにも当然伝わると考えました。

    今回ご返信いただいた中に「XAML全体が分からない」など私の伝え方が不十分であったり、確認すべきポイントがずれているところもあると思っておりますのでそちらの方でも何かございましたらお願いいたします。

    「BusinessConnectionRow」の名前は集合体で使用する可能性があったのでこの名前にしていますが、単体のオブジェクトで使用しております。

    GetErrorsメソッド内で検証しているのは2つのプロパティの値による検証およびエラーの通知を2つのコントロールに行いたくこの方法で実現できたのでとりあえずOKということにしております。

    ちなみにCustomValidationAttributeで実装しようとしたのですが上手くいかなかったのでこの形にしましたがいずれにしてもおっしゃられるとおりきれいな形ではなく結構無理しておりますので今後の課題としております。(INotifyPropertyChanged、INotifyDataErrorInfo、検証機能自体今回使用するのがはじめてで仕組み自体理解が不十分でありますのでまずは動きがなんとか実現できるところから始めています。)

    本件は今回の質問の本筋から外れますがよい方法などございましたら是非ともご教授いただければと思います。

    また、ご指摘のとおり単純なサンプルを作成してこの検証機能との連動の確認は行いたいと思います。

    長くなりましたがよろしくお願いいたします。

     

    2011年10月17日 0:49
  • 自己解決しました。

    "ValidationSummary"は"StackPanel"の中にあるとエラーが通知されないようです。

    Tetsuaki Uchida 様色々とご教授いただきありがとうございました。

    本件はこれで終了とさせていただきますが、先の投稿で質問させていただきました内容につきましてご教授いただけることがございましたらよろしくお願いいたします。

    2011年10月17日 2:06
  • こんにちは。
    解決された様で何よりです。

    >"ValidationSummary"は"StackPanel"の中にあるとエラーが通知されないようです。

    「おや」と思いもう1度ValidationSummaryクラスをきちんと確認したところ

    私の返信した

    >ValidationSummaryはDataContextのErrorsChangedイベントを購読しますので、
    >TextBoxとDataContextが違えば通知されないと思います。

    大嘘でした。ごめんなさい。

    ValidationSummary は、その親コンテナーの BindingValidationError イベントを受け取ります。

    という記述がきちんとありました。
    なので、
    エラーコントロールの先祖のどれかの直子じゃないと表示してくれないですね。ミスリードしてすみませんでした。

    あと、CustomValidatorの件ですが、
    質問文から読み取れるだ要件は
    ・CompanyNameは必須
    ・IsKanaCompanyNameがTrueの場合(チェックボックスにチェックが入っている)場合のみCompanyNameは全角カナで無ければならない
    ・チェックはCompanyNameが変化した時と、IsKanaCompanyNameが変化したときに行う
    ・エラーの表示はCheckBoxには出さず、CompanyNameを入力するTextBoxにのみ表示する

    ということかなと思います。
    #ソース見た限りだとIsKanaCompanyNameがTrueじゃないとCompanyNameの必須チェックが走らなそうな気がしますが、
    #上の要件の方が自然だと(勝手に)思うのでこの要件での実装例を考えてみます。

    ポイントは
    ・CompanyNameとIsKanaCompanyNameの変化時に同じ検証ロジックを通す
    ・どちらから呼ばれた検証ロジックもCompanyNameのエラーとして通知する
    ・全角カナチェックは条件付きなので、RegularExpressionAttributeは使えないのでCustomValidatorAttributeを使う

    を押さえればいけると思います。

    全部書くと長いのでかいつまんで書けば

    //カスタム検証の実装を記述したクラス
    public static class CustomValidator
    {
        public static ValidationResult ValidateCompanyName(string value, ValidationContext context)
        {
            var vm = context.ObjectInstance as MainViewModel;
            if (vm == null) throw new ArgumentException("context");
    
            if (vm.IsKanaCompanyName && !Regex.IsMatch(value , @"^[ア-ンガ-ボァ-ョヮッー]*$"))
            {
                return new ValidationResult("全角カナで入力してください",  new string[] { "CompanyName" } );
            }
            return ValidationResult.Success;
            }
    }
       
    public class BusinessConnectionRow : ObjectModelBase
    {
    //~~
            //属性を使った検証を行うメソッド
            private void ValidateProperty(string propertyName,  string value)
            {
              // ※Dictionaryクラス等でViewModel内にエラーを保存しておく
              
                var result = new List<ValidationResult>();
                if (Validator.TryValidateProperty(value, new ValidationContext(this, null, null) { MemberName = propertyName }, result))
                {
                    //エラーをクリアする 
                }
                else
                {
                    //エラーに追加する
                }
                //エラー変更通知を出す
                RaiseErrorsChanged(propertyName);
            }
    
    
    //~~
            [Required]
            [CustomValidation(typeof(CustomValidator), "ValidateCompanyName")] //CustomValidatorクラスを指定する
            public string CompanyName
            {
                get
                {
                    return _CompanyName;
                }
                set
                {
                    if (value == _CompanyName) return;
    
                    _CompanyName = value;
                    //検証メソッドを呼ぶ
                    ValidateProperty("CompanyName", value);
                    RaisePropertyChanged("CompanyName");
                }
            }
    //~~
            public bool IsKanaCompanyName
            {
                get
                {
                    return _IsKanaCompanyName;
                }
                set
                {
                    if (value == _IsKanaCompanyName) return;
    
                    _IsKanaCompanyName = value;
                    //CompanyNameの名前と値で検証メソッドを呼ぶ
                    ValidateProperty("CompanyName", CompanyName);
                    RaisePropertyChanged("IsKanaCompanyName");
                }
            }
    

           
    こんな感じで書くとすっきりするのではと思うので是非試してみてください。
    2011年10月17日 17:19
  • ご返信ありがとうございます。

    "StackPanel"の中に配置すると検知できない理由まではまだ確認できていなかったのですがそういう理由なのですね。

    また、情報伝達不足にも関わらず推測してCustomValidatorの実装方法についてご説明していただきありがとうございます。

    推測していただいた要件はCompanyNameが必須ではなくて任意だけ異なりますので次の部分を訂正して思い通りの動作になりました。

    あとご指摘のようにエラーの管理を変更することですっきりした形になりそうです。

    ありがとうございました。

      if (vm.IsKanaCompanyName && !Regex.IsMatch(value , @"^[ア-ンガ-ボァ-ョヮッー]*$"))

    if (vm.IsKanaCompanyName && !string.IsNullOrEmpty(value) 
    && !Regex.IsMatch(value , @"^[ア-ンガ-ボァ-ョヮッー]*$"))
    2011年10月18日 2:06