SourceChord

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

WPFでの入力値検証・その8 ~複数のプロパティが絡むバリデーションを作成する~

今度は、オブジェクト内の複数のプロパティが絡むバリデーション処理を実装してみたいと思います。


以下のような画面を作り、文字列を入力するテキストボックスと、文字列の上限数を入力するテキストボックスを作ります。
f:id:minami_SC:20150215154844p:plain

ViewModelには以下の二つのプロパティを作成します。

プロパティ名 内容
InputString string テキストボックスの文字列とバインドします。文字数の上限値チェックのために、CustomValidation属性を付加します。
ValidationCount int InputStringプロパティの文字数チェックで使用する上限文字数を設定します。

InputStringの入力値検証では、自身の値だけでなくValidationCountプロパティの値にも依存して結果が変わります。
今回は、このように複数のプロパティが絡むバリデーションを行ってみたいと思います。

CustomValidationでの検証ロジックの作成

以下のように、検証対象のInputStringプロパティにCustomValidation属性を付け、検証用のロジックとしてCustomMethodメソッドを指定します。

        [CustomValidation(typeof(MainWindowViewModel), "CustomMethod")]
        public string InputString
        {

        ・・・中略・・・
        /// <summary>
        /// InputStringの入力値検証に使用するカスタム検証メソッド
        /// </summary>
        public static ValidationResult CustomMethod(string value, ValidationContext context)
        {
            var obj = context.ObjectInstance as MainWindowViewModel;
            if (obj != null)
            {
                // ValidationCountプロパティで指定された文字数以上だった場合は、エラーとする。
                if (obj.InputString.Length >= obj.ValidationCount)
                {
                    var msg = string.Format("{0}文字以内で入力してください。", obj.ValidationCount);
                    return new ValidationResult(msg);
                }
            }
            return ValidationResult.Success;
        }
        ・・・中略・・・
        public int ValidationCount
        {
            get { return validationCount; }
            set { this.SetProperty(ref this.validationCount, value); this.ValidateProperty(this.InputString, "InputString"); }
        }

そして、CustomMethod側ではValidationContext型の引数が渡ってくるので、これを使って検証対象のオブジェクト全体のインスタンスを取得します。(context.ObjectInstanceプロパティをキャストして使用します。)
検証ロジック自体は、InputString.Lengthと、ValidationCountプロパティを比較するだけのシンプルなものです。


「this.ValidateProperty(this.InputString, "InputString");」と書いて、別のプロパティの検証を走らせる部分ですが、プロパティ名の文字列を直接リテラルとして使用しています。
リファクタリングとかに追従しなくて微妙にイヤだなぁという場合は、以下を参考にプロパティ名の文字列を取得する仕組みを付け足しておけばよいかと思います。
http://d.hatena.ne.jp/dotnetmemo/20090913/1252801891
C#6が使えるようになれば、nameof演算子で一発ですね。

結果

以下のように、ValidationCountプロパティに設定した値に応じて、文字数チェックが行われます。
f:id:minami_SC:20150215155729p:plain

ValidationCountプロパティを更新した時にも、InputString側の検証結果が即座に反映されます。
f:id:minami_SC:20150215155735p:plain


サンプルコード

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

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="300"
        Height="200">
    <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>
        <Grid HorizontalAlignment="Center" VerticalAlignment="Center">
            <Grid.RowDefinitions>
                <RowDefinition Height="Auto" />
                <RowDefinition Height="Auto" />
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="Auto" />
                <ColumnDefinition Width="Auto" />
            </Grid.ColumnDefinitions>
            <TextBlock Text="ここに文字列を入力:" />
            <TextBox Grid.Column="1"
                     Width="120"
                     Height="23"
                     Margin="0,0,0,5"
                     Text="{Binding InputString,
                                    UpdateSourceTrigger=PropertyChanged}"
                     Validation.ErrorTemplate="{StaticResource ValidationTemplate}" />

            <TextBlock Grid.Row="1" Text="上限文字数:" />
            <TextBox Grid.Row="1"
                     Grid.Column="1"
                     Width="120"
                     Height="23"
                     Text="{Binding ValidationCount,
                                    UpdateSourceTrigger=PropertyChanged}"
                     TextWrapping="Wrap" />
        </Grid>
    </Grid>
</Window>
MainWindowViewModel.cs
    public class MainWindowViewModel : ValidateableBase
    {
        private string inputString;
        [CustomValidation(typeof(MainWindowViewModel), "CustomMethod")]
        public string InputString
        {
            get { return inputString; }
            set { this.SetProperty(ref this.inputString, value); }
        }

        /// <summary>
        /// InputStringの入力値検証に使用するカスタム検証メソッド
        /// </summary>
        public static ValidationResult CustomMethod(string value, ValidationContext context)
        {
            var obj = context.ObjectInstance as MainWindowViewModel;
            if (obj != null)
            {
                // ValidationCountプロパティで指定された文字数以上だった場合は、エラーとする。
                if (obj.InputString.Length >= obj.ValidationCount)
                {
                    var msg = string.Format("{0}文字以内で入力してください。", obj.ValidationCount);
                    return new ValidationResult(msg);
                }
            }
            return ValidationResult.Success;
        }

        private int validationCount;
        /// <summary>
        /// InputStringの検証に使用する上限文字数を取得または設定します。
        /// </summary>
        public int ValidationCount
        {
            get { return validationCount; }
            set { this.SetProperty(ref this.validationCount, value); this.ValidateProperty(this.InputString, "InputString"); }
        }

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