【WPF】TextBoxにエラーを表示する方法 | サンプルコード付き

この記事ではwpfのTextBoxにエラーとなる内容を入力した際にエラーを表示する方法を解説していきたいと思います。ここではINotifyDataErrorInfo を利用して、TextBox に入力エラーを表示する仕組みを採用しています。次の流れで解説を行います。

この記事で作成するもの

エラーを表示するGif
エラーを表示するTextBoxの実装サンプル

テキストを入力してエラーとなる内容の場合に、画像のようなエラーを表示するように、エラークラスの実装とTextBoxのスタイルをカスタマイズしていきたいと思います。

フォルダ構成

ErrorInfo/                  # プロジェクト名
├── Styles/                 # スタイルフォルダ
    └── TextBox.xaml        # スピンボタン用の拡張ファイル
├── App.xaml                # デフォルトで用意されているファイル
├── AssemblyInfo.cs         # デフォルトで用意されているファイル
|── ErrorBase.cs            # エラーを表示するために使用する基底クラス
├── MainWindow.xaml         # メインウィンドウ
└── MainWindowViewModel.cs  # メインウィンドウのビューモデル

エラー表示の土台を実装

まず初めにエラーを処理するために使う土台を作っていきたいと思います。WPF では ValidationRule を使ったエラー表示も可能ですが、細かく制御したい場合には INotifyDataErrorInfo を利用する方が柔軟です。本記事では後者の方法を用いて、エラーをスタイル付きで表示する方法を紹介します。

ErrorBase.cs

using System.Collections;
using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ErrorInfo;

/// <summary>
/// エラー用の基底クラス
/// </summary>
internal class ErrorBase : INotifyDataErrorInfo
{
    // プロパティ名 -> バリデーション関数
    private readonly Dictionary<string, Func<IEnumerable<string>>> validators = [];

    // プロパティ名 -> 現在のエラーリスト
    private readonly Dictionary<string, List<string>> errors = [];

    public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;

    public bool HasErrors => errors.Any(kv => kv.Value.Count > 0);

    public IEnumerable GetErrors(string? propertyName)
    {
        if (string.IsNullOrEmpty(propertyName))
        {
            return errors.Values.SelectMany(x => x);
        }

        return errors.TryGetValue(propertyName, out var list) ? list : Enumerable.Empty<string>();
    }

    /// <summary>
    /// プロパティの値をセットして自動でバリデーションを実行
    /// </summary>
    protected bool SetProperty([CallerMemberName] string? propertyName = null)
    {
        Validate(propertyName);
        return true;
    }

    /// <summary>
    /// プロパティのバリデーションを実行
    /// </summary>
    protected void Validate([CallerMemberName] string? propertyName = null)
    {
        if (propertyName is null)
        {
            return;
        }

        if (validators.TryGetValue(propertyName, out var validator))
        {
            errors[propertyName] = validator().ToList();
            OnErrorsChanged(propertyName);
        }
    }

    /// <summary>
    /// バリデーション関数を登録
    /// </summary>
    protected void AddValidator(string propertyName, Func<IEnumerable<string>> validator)
    {
        validators[propertyName] = validator;
        errors[propertyName] = [];
    }

    protected void OnErrorsChanged(string propertyName)
    {
        ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
    }
}

このErrorBaseクラスを継承してエラーを実装していきます。

TextBoxのエラースタイル定義の作成

スタイルの作成と適用方法の詳しい解説はこちらの記事を参照ください。ここではTextBoxのスタイルを変更してエラーに適したスタイルを作っていきます。

TextBox.xaml

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
  
  <SolidColorBrush x:Key="TextBox.ErrorList.Foreground" Color="Red" o:Freeze="True"/>
  <ControlTemplate x:Key="ValidationErrorTemplate">
    <StackPanel>
      <!-- 元の TextBox の表示 -->
      <AdornedElementPlaceholder x:Name="adornedelem" />
      <!-- エラー一覧 -->
      <ItemsControl ItemsSource="{Binding AdornedElement.(Validation.Errors), ElementName=adornedelem}" Focusable="False">
        <ItemsControl.ItemTemplate>
          <DataTemplate>
            <TextBlock Text="{Binding ErrorContent}" Foreground="{StaticResource TextBox.ErrorList.Foreground}"/>
          </DataTemplate>
        </ItemsControl.ItemTemplate>
      </ItemsControl>
    </StackPanel>
  </ControlTemplate>

  <SolidColorBrush x:Key="TextBox.Static.Border" Color="#FFABAdB3" o:Freeze="True"/>
  <SolidColorBrush x:Key="TextBox.MouseOver.Border" Color="#FF7EB4EA" o:Freeze="True"/>
  <SolidColorBrush x:Key="TextBox.Focus.Border" Color="#FF569DE5" o:Freeze="True"/>
  <SolidColorBrush x:Key="TextBox.HasError.Background" Color="#FFffe1df" o:Freeze="True"/>
  <SolidColorBrush x:Key="TextBox.HasError.Border" Color="Red" o:Freeze="True"/>
  <Style x:Key="TextBoxErrorStyle" TargetType="{x:Type TextBox}">
    <Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"/>
    <Setter Property="BorderBrush" Value="{StaticResource TextBox.Static.Border}"/>
    <Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}"/>
    <Setter Property="BorderThickness" Value="1"/>
    <Setter Property="KeyboardNavigation.TabNavigation" Value="None"/>
    <Setter Property="HorizontalContentAlignment" Value="Left"/>
    <Setter Property="FocusVisualStyle" Value="{x:Null}"/>
    <Setter Property="AllowDrop" Value="true"/>
    <Setter Property="ScrollViewer.PanningMode" Value="VerticalFirst"/>
    <Setter Property="Stylus.IsFlicksEnabled" Value="False"/>
    <Setter Property="Validation.ErrorTemplate" Value="{StaticResource ValidationErrorTemplate}"/>
    <Setter Property="Template">
      <Setter.Value>
        <ControlTemplate TargetType="{x:Type TextBox}">
          <Border x:Name="border" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" SnapsToDevicePixels="True">
            <ScrollViewer x:Name="PART_ContentHost" Focusable="false" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden"/>
          </Border>
          <ControlTemplate.Triggers>
            <Trigger Property="IsEnabled" Value="false">
              <Setter Property="Opacity" TargetName="border" Value="0.56"/>
            </Trigger>
            <Trigger Property="IsMouseOver" Value="true">
              <Setter Property="BorderBrush" Value="{StaticResource TextBox.MouseOver.Border}"/>
            </Trigger>
            <Trigger Property="IsKeyboardFocused" Value="true">
              <Setter Property="BorderBrush" Value="{StaticResource TextBox.Focus.Border}"/>
            </Trigger>
          </ControlTemplate.Triggers>
        </ControlTemplate>
      </Setter.Value>
    </Setter>
    <Style.Triggers>
      <Trigger Property="Validation.HasError" Value="True">
        <Setter Property="BorderBrush" Value="{StaticResource TextBox.HasError.Border}"/>
        <Setter Property="BorderThickness" Value="2"/>
        <Setter Property="Background" Value="{StaticResource TextBox.HasError.Background}" />
      </Trigger>
      <MultiTrigger>
        <MultiTrigger.Conditions>
          <Condition Property="IsInactiveSelectionHighlightEnabled" Value="true"/>
          <Condition Property="IsSelectionActive" Value="false"/>
        </MultiTrigger.Conditions>
        <Setter Property="SelectionBrush" Value="{DynamicResource {x:Static SystemColors.InactiveSelectionHighlightBrushKey}}"/>
      </MultiTrigger>
    </Style.Triggers>
  </Style>

</ResourceDictionary>

使用方法

ErrorBaseクラスを用いて、ビューモデル側の実装を行っていきます。

MainWindowViewModel.cs

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace ErrorInfo;

internal class MainWindowViewModel : ErrorBase, INotifyPropertyChanged
{
    private string name = string.Empty;
    private string age = string.Empty;

    public MainWindowViewModel()
    {
        // バリデーションルールを登録
        this.AddValidator(nameof(Name), ValidateText);
        this.AddValidator(nameof(Age), ValidateText2);
    }

    public event PropertyChangedEventHandler? PropertyChanged;

    public string Name
    {
        get => name;
        set
        {
            if (this.name != value)
            {
                this.name = value;
                this.OnPropertyChanged();
                this.SetProperty();
            }
        }
    }

    public string Age
    {
        get => age;
        set 
        {
            if (this.age != value)
            {
                this.age = value;
                this.OnPropertyChanged();
                this.SetProperty();
            }
        }
    }

    private IEnumerable<string> ValidateText()
    {
        if (string.IsNullOrEmpty(this.name))
        {
            yield return "入力必須です。";
        }
    }

    private IEnumerable<string> ValidateText2()
    {
        if (!int.TryParse(this.age, out _))
        {
            yield return "数字を入力してください。";
        }
    }

    protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
    {
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

エラー実装の部分は以下のところになります。

this.AddValidator(nameof(Name), ValidateText);
this.AddValidator(nameof(Age), ValidateText2);
private IEnumerable<string> ValidateText()
{
    if (string.IsNullOrEmpty(this.name))
    {
        yield return "入力必須です。";
    }
}

private IEnumerable<string> ValidateText2()
{
    if (!int.TryParse(this.age, out _))
    {
        yield return "数字を入力してください。";
    }
}

この部分を自由に変更してエラーをカスタマイズしていくことが可能です。

次にビュー側を実装していきます。

<Window x:Class="ErrorInfo.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:ErrorInfo"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800"
        d:DataContext="{d:DesignInstance Type=local:MainWindowViewModel, IsDesignTimeCreatable=True}">
  <Window.DataContext>
    <local:MainWindowViewModel/>
  </Window.DataContext>
  <StackPanel Grid.IsSharedSizeScope="True" Margin="0,20,10,0">
    <Grid>
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
        <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>
      <Label
        Grid.Column="0"
        Content="名前"
        Margin="0,0,10,0"/>
      <TextBox 
        Grid.Column="1"
        Style="{StaticResource TextBoxErrorStyle}"
        Text="{Binding Name, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
        VerticalContentAlignment="Center"/>
    </Grid>
    <Grid Margin="0,20,0,0">
      <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto" SharedSizeGroup="Label"/>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition Width="*"/>
      </Grid.ColumnDefinitions>
      <Label
        Grid.Column="0"
        Content="年齢"
        Margin="0,0,10,0"/>
      <TextBox 
        Grid.Column="1"
        Style="{StaticResource TextBoxErrorStyle}"
        Text="{Binding Age, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}" 
        VerticalContentAlignment="Center"
        Width="50"/>
      <Label
        Grid.Column="2"
        Content="歳"/>
    </Grid>
  </StackPanel>
</Window>

ここでTextBoxのStyleはまだ先程のものが適用されていません。なのでApp.xamlの辞書にスタイルを登録する必要があります。

App.xaml

<Application x:Class="ErrorInfo.App"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:local="clr-namespace:ErrorInfo"
             StartupUri="MainWindow.xaml">
  <Application.Resources>
    <ResourceDictionary>
      <ResourceDictionary.MergedDictionaries>
        <ResourceDictionary Source="Styles/TextBox.xaml"/>
      </ResourceDictionary.MergedDictionaries>
    </ResourceDictionary>
  </Application.Resources>
</Application>

これで起動すると、こちらで示した実装が完了しました。

注意点

  • App.xamlにTextBoxのスタイルを登録しないと使用することができません。
  • Errorを表示したくないときはTextBoxにStyleを適用する必要はありません

まとめ

本記事では INotifyDataErrorInfo を利用して、TextBox に入力エラーを表示する仕組みを紹介しました。

  • ErrorBase.cs で共通バリデーション処理を定義
  • TextBox.xaml にエラースタイルを追加
  • ViewModel 側でバリデーションルールを登録
  • App.xaml でスタイルを有効化

といった流れで、汎用的なエラーチェック環境を整えることができます。