WPFのWindow で バインドしているプロパティの検証エラーを表示する方法として IDataErrorInfo と Validation.ErrorTemplate を使用するサンプルを掲載します。

Validation.HasErrorアタッチプロパティをトリガーにし、エラーを検知した場合に対象コントロールのToolTip にエラーを設定するサンプルがWeb上でよく掲載されているようですが、今回は Validation.ErrorTemplate を使用してエラーを表示する方法を記載します。記事を作成するに当たり、次のURLを参考にさせてもらいました。

WPF で複雑なビジネス データの規則を適用する
http://msdn.microsoft.com/ja-jp/magazine/ff714593.aspx

WPF TextBox Validation with IDataErrorInfo
http://www.codearsenal.net/2012/06/wpf-textbox-validation-idataerrorinfo.html

INotifyDataErrorInfoを使用してみようと思ったのですが、 INotifyDataErrorInfo は .NET 4.5 から導入されたクラスのようなので今回はIDataErrorInfoを使用しています。

検証環境

  • Visual Studio 2012 , .NET 4.5
  • Windows 7

1. サンプルプログラムの作成

WPF アプリケーションを作成します。

既定で作成されるMainWindowにバインドする ViewModelを次のように定義します。今回はIDataErrorInfo を使用してViewにエラーを表示するため、 IDataErrorInfoを実装しています。また、ViewModel のプロパティの検証に DataAnnotations の各検証属性とValidatorを利用しています。基本クラスのViewModelBaseの詳細は省きます。

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IDataErrorInfoTest
{
    public class TestModel : ViewModelBase, IDataErrorInfo
    {
        public TestModel() { }

        private int _number = 0;
        [Required(ErrorMessage = "Required")]
        [Range(1, 100, ErrorMessage="1から100で入力してください。")]
        public int Number
        {
            get { return _number; }
            set
            {
                if (_number == value) return;

                _number = value;
                RaisePropertyChanged(() => Number);
            }
        }

        private string _name = string.Empty;
        [CustomValidation(typeof(TestModel), "ValidateName")]
        [MinLength(4)]
        public string Name
        {
            get { return _name; }
            set
            {
                if (_name == value) return;

                _name = value;
                RaisePropertyChanged(() => Name);
            }
        }

        public static ValidationResult ValidateName(string test, ValidationContext context)
        {
            if (string.IsNullOrEmpty(test)) return ValidationResult.Success;

            if (test.StartsWith("test"))
            {
                return ValidationResult.Success;
            }
            return new ValidationResult("test で始まってほしい");
        }

        public string Error
        {
            get { throw new NotImplementedException(); }
        }

        public string this[string columnName]
        {
            get
            {
                object v = GetValue(columnName);
                ICollection<ValidationResult> results = new List<ValidationResult>();
                Validator.TryValidateProperty(v, new ValidationContext(this) { MemberName = columnName }, results);
                foreach (var item in results)
                {
                    return item.ErrorMessage;
                }
                return null;
            }
        }

        public object GetValue(string propName)
        {
            return GetValue(propName, this);
        }

        public object GetValue(string propName, object model)
        {
            if (model == null) throw new ArgumentNullException("model");

            System.Reflection.PropertyInfo prop = model.GetType().GetProperty(propName,
                System.Reflection.BindingFlags.GetProperty | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
            return prop.GetValue(model);
        }
    }
}

上記クラスのNumberプロパティをバインドしたテキストボックスに非数字を入力するとデフォルトのコンバーターにより変換エラーが発生しますが、そのときのエラーメッセージがあまり新設ではないのでWPFのValidationRule 機能を使用します。新規クラスを作成し NumericValidation という整数かの検証を行うValidationRuleクラスを次のように作成します。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;

namespace IDataErrorInfoTest
{
    public class NumericValidation : ValidationRule
    {
        public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
        {
            int val;
            if (!string.IsNullOrEmpty((string)value))
            {
                if (!int.TryParse(value.ToString(), out val))
                {
                    return new ValidationResult(false, "整数を入力してください。");
                }
            }
            return new ValidationResult(true, null);
        }

    }
}

最後にxamlファイルを編集します。Window.ResourcesタグでTestModel クラスを作成し、 GridパネルのDataContext として使用しています。また、Window.ResourcesにValidation.ErrorTemplate で指定するために ControlTemplate を定義し、各テキストボックスから参照しています。IDataErrorInfoによる検証を行うためにBindの式でValidatesOnDataErrorsにtrueを設定しています。1番目のテキストボックスのBinding に NumericValidation ルールを設定し、非数字以外のテキストが入力されたさいにわかりやすいテキストが表示されるようにしています。

<Window x:Class="IDataErrorInfoTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="clr-namespace:IDataErrorInfoTest"
        Title="MainWindow" SizeToContent="WidthAndHeight">
    <Window.Resources>
        <vm:TestModel x:Key="testModel" />
        <ControlTemplate x:Key="validationTemplate">
            <Grid>
                <Border Background="MediumVioletRed" Width="15" Height="15" HorizontalAlignment="Right" CornerRadius="5" Margin="0,0,4,0">
                    <Border.ToolTip>
                        <Binding ElementName="adornedelem" Path="AdornedElement.(Validation.Errors).CurrentItem.ErrorContent"></Binding>
                    </Border.ToolTip>
                    <TextBlock TextAlignment="Center" Foreground="White" FontWeight="Bold" VerticalAlignment="Center">!</TextBlock>
                </Border>
                <AdornedElementPlaceholder x:Name="adornedelem">
                    <Border BorderBrush="MediumVioletRed" BorderThickness="2" Width="{Binding ElementName=adornedelem, Path=AdornedElement.ActualWidth}">
                    </Border>
                </AdornedElementPlaceholder>
            </Grid>
        </ControlTemplate>
    </Window.Resources>
    <Grid Margin="10" DataContext="{StaticResource testModel}">
        <Grid.RowDefinitions>
            <RowDefinition></RowDefinition>
            <RowDefinition></RowDefinition>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="1*"></ColumnDefinition>
            <ColumnDefinition Width="2*" MinWidth="100"></ColumnDefinition>
        </Grid.ColumnDefinitions>
        <Label Grid.Column="0" Grid.Row="0" Content="数値だけ"></Label>
        <TextBox Grid.Column="1" Grid.Row="0"  Validation.ErrorTemplate="{StaticResource validationTemplate}" >
            <TextBox.Text>
                <Binding Path="Number" ValidatesOnDataErrors="true">
                    <Binding.ValidationRules>
                        <vm:NumericValidation />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <Label Grid.Column="0" Grid.Row="1" Content="最小4文字以上"></Label>
        <TextBox Grid.Column="1" Grid.Row="1"  Text="{Binding Name, ValidatesOnDataErrors=true}" Validation.ErrorTemplate="{StaticResource validationTemplate}"  />

    </Grid>
</Window>

2.動作例

プログラムの実行例です。データ検証エラー時にコントロールが色つきの枠で囲まれ右端にエラーを表すエクスクラメーションが表示されます。エクスクラメーションマークにマウスを移動するとツールチップが表示されエラー内容が表示れます。

簡単ですが動作確認できました。

TextBox.Textは既定で Binding 時の UpdateSourceTrigger の値は LostFocus です(他はPropertyChanged)。テキスト入力中に検証を動作させたい場合は UpdateSourceTriggerをPropertyChanged にします。

3. まとめ

説明は以上です。 IDataErrorInfoを使用したエラー表示のサンプルはWeb上でたくさん見かけましたが自分用のメモとして記事を掲載しました。