WPFプログラミング備忘録

WPF データの妥当性検証をしたい

WPF

はじめに

この記事ではデータの妥当性検証をする方法の1つについて確認します。

データの妥当性検証にはいくつか方法があります。

  • 例外とカスタムテンプレートを使用する
  • IDataErrorInfoインターフェースを使用する
  • ValidationRuleクラスを使用する
  • アノテーションを使用する

今回は例外とカスタムテンプレートを使用する方法を見ていきます。

ValidationRuleクラスを使用する方法については以下の記事をご確認ください。

>> WPF ValidationRuleクラスを使ってデータの妥当性検証をする

アノテーションを使用する方法については以下の記事をご確認ください。

>> WPF アノテーションを使ってデータの妥当性検証をする

プロジェクトのファイル構成は下図のとおりです。

開発環境は以下のとおりです。

オペレーティングシステムWindows10 x64
Visual StudioMicrosoft Visual Studio Community 2019 Version 16.11.2
.NET Framework4.7.2

WPFのデフォルトの挙動

今回のサンプルは単純にテキストボックスを1つだけ使います。

XAMLファイルは以下のとおりです。

<Window x:Class="WpfValidatingData1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfValidatingData1"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="600">
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="240"/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="1" Text="名前 : " FontSize="20" TextAlignment="Right" Margin="10"/>
        <TextBox Grid.Row="1" Grid.Column="1" FontSize="20"
                 Text="{Binding Name, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}"
                 Margin="2"/>
    </Grid>
</Window>

名前を入力するテキストボックスを配置しました。

妥当性検証をするためにBinding.ValidatesOnExceptionsをtrueに設定する必要があります(デフォルトはfalse)。

Person.csのコードは以下のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace WpfValidatingData1
{
    class Person : INotifyPropertyChanged
    {
        string _name;
        public string Name
        {
            get { return _name; }
            set
            {
                if (string.IsNullOrWhiteSpace(value))
                {
                    throw new Exception("名前は空欄にはできません");
                }

                if (value?.Length <= 1)
                {
                    throw new Exception("二文字以上を入力してください");
                }

                _name = value;

                NotifyPropertyChanged();
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged([CallerMemberName] string propertyName = "")
        {
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

Nameプロパティを定義しています。setアクセサではテキストボックスに入力が無いときと1文字以下のときに例外を投げます。

INotifyPropertyChangedインターフェースについては以下の記事をご確認ください。

>> WPF データバインディングとUIへの通知機能

コードビハインドは以下のとおりです。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfValidatingData1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new Person { Name = "Taro" };
        }
    }
}

DataContextを設定しています。

ビルドして実行すると以下のウィンドウが表示されます。

ここで例えば名前を空欄にすると、以下のようにテキストボックスの周囲が赤い枠線で囲まれることがわかります。

これはWPFのデフォルトの挙動です。

この挙動は以下で見ていくとおりカスタムテンプレートを使ってカスタマイズできます。

Validation.ErrorTemplate添付プロパティを使用する

全て実装したXAMLファイルは以下のとおりです。コードビハインドとPerson.csに変更はありません。

<Window x:Class="WpfValidatingData1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfValidatingData1"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="600">
    
    <Window.Resources>
        <ControlTemplate x:Key="_errorTemplate">
            <StackPanel Orientation="Horizontal">
                <Border BorderBrush="Red" BorderThickness="2" Margin="0,0,6,0">
                    <AdornedElementPlaceholder x:Name="_el"/>
                </Border>
                <TextBlock Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=_el}"
                           Foreground="Red" HorizontalAlignment="Right"
                           VerticalAlignment="Center" Margin="0,0,10,0"/>
            </StackPanel>
        </ControlTemplate>
    </Window.Resources>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="240"/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="1" Text="名前 : " FontSize="20" TextAlignment="Right" Margin="10"/>
        <TextBox Grid.Row="1" Grid.Column="1" FontSize="20"
                 Text="{Binding Name, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}"
                 Validation.ErrorTemplate="{StaticResource _errorTemplate}"
                 Margin="2"/>
    </Grid>
</Window>

リソースとしてカスタムテンプレート_errorTemplateを追加しています。これをTextBoxのValidation.ErrorTemplateプロパティに設定します。

ポイントはリソースにあるAdornedElementPlaceholderです。

これは妥当性検証の対象となるコントロール(今回の場合はTextBox)の配置場所を示しています。

StackPanelで横方向に左からTextBoxとTextBlockを並べています。TextBlockには妥当性検証に失敗したときに投げられる例外オブジェクトに渡したエラー文字列が表示されます。

Textプロパティに設定されている以下のBindingオブジェクトがややこしく見えますが、これについては後ほど触れます。

<TextBlock Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=_el}" 
Foreground="Red" HorizontalAlignment="Right"
                  VerticalAlignment="Center" Margin="0,0,10,0"/>

ビルドして実行した後、TextBoxを空欄にしたり1文字だけ入力すると、以下とおりエラーメッセージが表示されることが確認できます。赤色の枠線はデフォルトの挙動よりも少し太くしてあります。

さて、バインディングソースのBindingオブジェクトは以下のとおりとなっています。

<TextBlock Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=_el}"
 Foreground="Red" HorizontalAlignment="Right"
                  VerticalAlignment="Center" Margin="0,0,10,0"/>

私が不思議に思ったのは添え字のゼロです。

(Validation.Errors)を見る限り何らかのコレクションの最初の要素にエラーの内容が入っているのは想像できたのですが、2番目以降についてはどうなっているのだろうというのが疑問でした。

実は今回の妥当性検証を制御するために、ValidationクラスのHasError添付プロパティとErrors添付プロパティが関与しています。

これらの値を確認するために以下の静的メソッドが使えます。

public static bool GetHasError(System.Windows.DependencyObject element);
public static System.Collections.ObjectModel.ReadOnlyObservableCollection<System.Windows.Controls.ValidationError> GetErrors(System.Windows.DependencyObject element);
Validation Class (System.Windows.Controls)
Provides methods and attached properties that support data validation.

これら2つの中身がどうなっているのか確認したコードが以下となります。

<Window x:Class="WpfValidatingData1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:WpfValidatingData1"
        mc:Ignorable="d"
        Title="MainWindow" Height="250" Width="600">
    
    <Window.Resources>
        <ControlTemplate x:Key="_errorTemplate">
            <StackPanel Orientation="Horizontal">
                <Border BorderBrush="Red" BorderThickness="2" Margin="0,0,6,0">
                    <AdornedElementPlaceholder x:Name="_el"/>
                </Border>
                <TextBlock Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=_el}"
                           Foreground="Red" HorizontalAlignment="Right"
                           VerticalAlignment="Center" Margin="0,0,10,0"/>
            </StackPanel>
        </ControlTemplate>
    </Window.Resources>
    
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="30"/>
            <RowDefinition Height="Auto"/>
            <RowDefinition Height="Auto"/>
        </Grid.RowDefinitions>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="Auto"/>
            <ColumnDefinition Width="240"/>
        </Grid.ColumnDefinitions>

        <TextBlock Grid.Row="1" Text="名前 : " FontSize="20" TextAlignment="Right" Margin="10"/>
        <TextBox x:Name="txtBox" Grid.Row="1" Grid.Column="1" FontSize="20"
                 Text="{Binding Name, ValidatesOnExceptions=True, UpdateSourceTrigger=PropertyChanged}"
                 Validation.ErrorTemplate="{StaticResource _errorTemplate}"
                 Margin="2"/>
        <Button Grid.Row="3" Content="テスト" Margin="10" Click="OnBtnTest" />
    </Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

namespace WpfValidatingData1
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            DataContext = new Person { Name = "Taro" };
        }

        private void OnBtnTest(object sender, RoutedEventArgs e)
        {
            bool hasError = Validation.GetHasError(txtBox);
            var errors = Validation.GetErrors(txtBox);

            var sb = new StringBuilder();

            sb.AppendFormat("Validation.HasError : {0}\n", hasError);

            sb.Append("Validation.Errors : \n");
            sb.AppendFormat("\tエラーオブジェクトの数{0}個\n", errors.Count);

            if (errors.Count > 0)
            {
                sb.Append("\tエラーの内容\n");
            }

            for (int i = 0; i < errors.Count; i++)
            {
                sb.AppendFormat("\t\t{0} : {1}\n", i, errors[i].ErrorContent);
            }
            MessageBox.Show(sb.ToString());
        }
    }
}

XAMLファイルにButtonを追加してダイアログボックスにそれぞれの添付プロパティの内容を表示してみました。

妥当性検証で問題が無いときのダイアログボックスの内容は下図のとおりです。

Validation.HasError添付プロパティはfalseになっています。また、Validation.Errors添付プロパティの内容はエラーの個数がゼロでした。

次に問題があるときのダイアログボックスの内容は下図のとおりです。

Validation.HasError添付プロパティはtrueになっています。Validation.Errors添付プロパティの内容はエラーの個数が1つになりました。

妥当性検証に成功したり失敗すると、Validation.HasError添付プロパティはfalseやtrueに変化します。

また、妥当性検証に失敗するとエラー情報を持つオブジェクト(ValidationError)がValidation.Errorsで参照できるコレクション(ReadOnlyObservableCollection< ValidationError >)に1つ追加され、妥当性検証に成功すると削除されることがわかります。

Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=_el}"

上記のとおり添え字に固定値ゼロを指定して良いのは、妥当性検証の失敗時にはValidationErrorオブジェクトは最初の1つしか存在しないためだと思われます。

コントロールの種類と数および妥当性検証の結果によっては複数個のValidationErrorオブジェクトが存在することがあるのかもしれません。

なお、DataGridコントロールの場合は今回のようにValidation.ErrorTemplate添付プロパティを使用することはできません。詳細は以下をご確認ください。

方法: DataGrid コントロールを使用して検証を実装する - WPF .NET Framework
Windows Presentation Foundation の DataGrid コントロールによって、セルおよび行の両方のレベルで検証が実行され、検証エラーに関するフィードバックが提供されるしくみについて説明します。

今回は例外とカスタムテンプレートを使い、データの妥当性検証を行う方法について確認しました。

以上です。

コメント