読者です 読者をやめる 読者になる 読者になる

SourceChord

C#とXAML好きなプログラマの備忘録。最近はWPF系の話題が中心です。

WPFでの入力値検証・その7 ~独自の検証ロジックを作成する~

C# WPF

久しぶりに、WPFのバリデーション・ネタです。
以前、DataAnnotationsの機能を使って、プロパティの属性としてバリデーションの条件を記述する方法を書きました。


このバリデーションの条件として、カスタムのロジックを作成する方法についてまとめてみました。
以下の二つの方法を扱っています。

  1. ValidationAttribute派生のクラスを作成する方法
  2. CustomValidationAttributeを使用する方法

使い分けとしては、汎用的に使いまわしそうな検証ロジックはValidationAttribute派生で作り、特定のクラス用に特化したロジックなどはCustomValidationAttributeを用いる、というのが相性が良いように思います。

ValidationAttribute派生クラスを作成

まずはValidationAttribute派生のクラスの作り方です。
string型のプロパティに対し、もしプロパティの値が空白(IsNullOrWhiteSpaceの戻り値がtrue)だったらエラー、という動作のバリデーション用属性を作ってみます。


そもそも、string型のプロパティに対するバリデーションであれば、RegularExpressionAttributeが非常に汎用的に利用できるので、このような派生クラスを実際に自分で作ることはないとは思います。
ここでは練習として、こんな感じの手抜きのバリデーション用クラスを作ってみます。
string型のプロパティ値が空白だった場合に、以下のようにメッセージを表示するだけのシンプルなものです。
f:id:minami_SC:20150215003716p:plain

手順はとてもシンプルで、以下2点をするだけです。

  1. ValidationAttribute派生のクラスを作り
  2. IsValidメソッドをオーバーライドする。

このIsValidメソッドですが、2種類のオーバーロードがあるので、ここではそのそれぞれの方法を試してみます。

bool IsValid(object value)をオーバーライド

引数で渡ってきたvalue値をチェックし、検証結果をbool値で返すだけのシンプルなものです。

    class WhiteValidateAttribute : ValidationAttribute
    {
        public override bool IsValid(object value)
        {
            var v = value as string;
            if (v != null && string.IsNullOrWhiteSpace(v))
            {
                return false;
            }
            return true;
        }
    }

使い方はこんな感じ。
プロパティに属性を付けるときに、コンストラクタの名前付き引数でErrorMessage引数に与えた文字列を、エラー発生時の表示用文字列とすることができます。

        private string inputString;
        [WhiteValidateAttribute(ErrorMessage="空白は無効です")]
        public string InputString
        {
            get { return inputString; }
            set { this.SetProperty(ref this.inputString, value); }
        }
ValidationResult IsValid(object value, ValidationContext validationContext)をオーバーライド

次は、二つの引数を持つIsValidメソッドをオーバーライドしてみます。
こちらでは、validationContextが引数にわたってくるので、バリデーション対象のオブジェクトの情報なども見ることができます。が、このサンプルでは特に使用しません。
また、こちらをオーバーライドする場合には、メソッドの戻り値をboolではなく、ValidationResult型で返す点に注意が必要です。

    class WhiteValidateAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var v = value as string;
            if (v != null && string.IsNullOrWhiteSpace(v))
            {
                return new ValidationResult(this.ErrorMessage);
            }
            return ValidationResult.Success;
        }
    }

ここでは、ValidationResultのインスタンスを生成する時に、ErrorMessageプロパティを参照して、エラー発生時の表示用文字列としています。

CustomValidateion属性を利用

CustomValidationAttributeクラスを使うと、任意のstaticメソッドを用いたバリデーションが行えます。
以下のようなstaticメソッドを、ViewModelクラスに作り、バリデーション対象のプロパティのCustomValidation属性で、このメソッドを指定します。

        public static ValidationResult CustomMethod(string value, ValidationContext context)
        {
            if (value != null && string.IsNullOrWhiteSpace(value))
            {
                return new ValidationResult(null);
            }
            return ValidationResult.Success;
        }

ValidationResultのインスタンスを作るときの引数をnullにしておくと、エラー時の表示文字列として、属性のErrorMessageプロパティを使ってくれるようになります。


↓属性の設定側はこんな感じ。

        private string inputString;
        [CustomValidation(typeof(MainWindowViewModel), "CustomMethod", ErrorMessage = "空白は無効です")]
        public string InputString
        {
            get { return inputString; }
            set { this.SetProperty(ref this.inputString, value); }
        }

全サンプルコード

今回のサンプルのすべてのコードは以下の通りです。

MainWindow.xaml
<Window x:Class="WpfBaseTemplate1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:local="clr-namespace:WpfBaseTemplate1"
        Title="MainWindow"
        Width="525"
        Height="350">
    <Window.DataContext>
        <local:MainWindowViewModel />
    </Window.DataContext>
    <Window.Resources>
        <ControlTemplate x:Key="ValidationTemplate">
            <StackPanel>
                <ItemsControl ItemsSource="{Binding AdornedElement.(Validation.Errors), ElementName=adornedelem}">
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <TextBlock Foreground="Red" Text="{Binding ErrorContent}" />
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
                <AdornedElementPlaceholder x:Name="adornedelem" />
            </StackPanel>
        </ControlTemplate>
    </Window.Resources>
    <Grid>
        <TextBox Width="120"
                 Height="23"
                 Margin="50"
                 HorizontalAlignment="Left"
                 VerticalAlignment="Top"
                 Text="{Binding InputString,
                                UpdateSourceTrigger=PropertyChanged}"
                 Validation.ErrorTemplate="{StaticResource ValidationTemplate}">
        </TextBox>
    </Grid>
</Window>
MainWindowViewModel.cs
    public class MainWindowViewModel : ValidateableBase
    {
        private string inputString;
        //[WhiteValidateAttribute(ErrorMessage="空白は無効です")]
        [CustomValidation(typeof(MainWindowViewModel), "CustomMethod", ErrorMessage = "空白は無効です")]
        public string InputString
        {
            get { return inputString; }
            set { this.SetProperty(ref this.inputString, value); }
        }

        public static ValidationResult CustomMethod(string value, ValidationContext context)
        {
            if (value != null && string.IsNullOrWhiteSpace(value))
            {
                return new ValidationResult(null);
            }
            return ValidationResult.Success;
        }

        public MainWindowViewModel()
        {
            this.InputString = string.Empty;
        }
    }

    class WhiteValidateAttribute : ValidationAttribute
    {
        protected override ValidationResult IsValid(object value, ValidationContext validationContext)
        {
            var v = value as string;
            if (v != null && string.IsNullOrWhiteSpace(v))
            {
                return new ValidationResult(this.ErrorMessage);
            }
            return ValidationResult.Success;
        }
    }