はじめに
今回の記事では MVVM 構成で実装された TextBox のバリデーションを行う方法についてまとめました。
作成したサンプルアプリケーションのイメージです。

WPF-SamplesのData Binding内のサンプルコードを参考にしています。
ViewModel の実装
データバインディングを実現するために ViewModel を作成します。また、ViewModel に継承しているプロパティ変更通知の基底クラスは以下の記事で解説している方法と同様の実装をしています。
namespace MVVM.TextBoxValidation.ViewModels
{
public sealed class MainWindowViewModel : BindableBase
{
public string ValidationRuleTextBox
{
get => _validationRuleTextBox;
set => SetProperty(ref _validationRuleTextBox, value);
}
private string _validationRuleTextBox;
public MainWindowViewModel()
{ }
}
}
XAML 側は以下のように実装します。
<Window x:Class="MVVM.TextBoxValidation.Views.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:local="clr-namespace:MVVM.TextBoxValidation"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="650"
Height="500"
WindowStartupLocation="CenterScreen"
mc:Ignorable="d">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="200" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="50" />
<RowDefinition Height="40" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0"
Grid.ColumnSpan="2"
Margin="8"
FontSize="20"
Text="10以上の数値を入力したり、負の値や文字を入力するとエラーになります。" />
<Label Grid.Row="1"
Grid.Column="0"
Margin="2"
Content="ValidationRule:"
FontSize="15" />
<TextBox Grid.Row="1"
Grid.Column="1"
Margin="2"
Padding="5"
VerticalContentAlignment="Center"
FontSize="15"
Text="{Binding ValidationRuleTextBox, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
</Window>
設置した TextBox のバリデーション方法を見ていきます。
ValidationRule
How to: Implement Binding Validationのサンプルコードでもありますが、まずはValidationRuleクラスを使用する方法です。
これはValidationRuleクラスを継承した独自クラスを作成する方法で、コンバータークラスを作成する方法に似ています。
ValidationRulesというフォルダを作成しTextBoxRuleというクラスを作成します。
using System.Globalization;
using System.Windows.Controls;
namespace MVVM.TextBoxValidation.ValidationRules
{
public class TextBoxRule : ValidationRule
{
/// <summary>
/// XAML側から指定するプロパティ
/// </summary>
public int Max { get; set; }
public override ValidationResult Validate(object value, CultureInfo cultureInfo)
{
if(string.IsNullOrWhiteSpace(value as string))
{
return new ValidationResult(false, "入力必須です。");
}
if (int.TryParse(value as string, out int number))
{
if (number < 0) return new ValidationResult(false, "負の値は入力できません。");
if (number > Max) return new ValidationResult(false, "最大値を超えています。");
// 第一引数(bool): チェックした値が有効な値か判定する(true:有効な値と判定、false: 無効な値と判定)
// 第二引数(object): エラーだった場合のメッセージを指定
// 以下は値が有効だった場合に返す値の例
//return new ValidationResult(true, null);
// 上記と同様の戻り値(値が有効だった場合に返す)
return ValidationResult.ValidResult;
}
else
{
return new ValidationResult(false, "数値を入力してください。");
}
}
}
}
オーバーライドしたValidateメソッドではValidationResultインスタンスを返します。
この際、値を判定するロジックを書いて、無効な値と判定する場合はnew ValidationResult(false, "表示するメッセージ")と指定します。
public int Maxプロパティは必須ではありませんが、今回は XAML 側から最大値を指定したいので実装しました。
このクラスを使用するために XAML 側の実装を以下のように修正します。
今までの実装を確認します。

エラーとなる値を入力すると TextBox の外枠が赤くなっていることが分かります。
しかし、エラーメッセージが表示されていないためAdornedElementPlaceholderを使用してエラーメッセージと、赤枠が装飾された TextBox を実装します。
AdornedElementPlaceholder
ControlTemplate内でAdornedElementPlaceholderを使用することで、元のコントロールの見た目や振る舞いが維持されたまま装飾を施した要素を実装することが出来ます。
今回の使い方では元の TextBox の位置とサイズを保持しつつ、エラーメッセージを表示することができます。
+
+
+
+
+
+
+
+
+
+
+
+
+
追加した箇所を詳しく見ていきます。
<Window.Resources>
Window.Resourcesでは、このウィンドウ内で共通して使用するリソースを定義します。
ここで定義したリソース(スタイルやテンプレートなど)は、ウィンドウ内の他のコントロールで再利用できます。
<ControlTemplate x:Key="validationErrorTemplate">
ControlTemplateは、コントロールの見た目を定義するものです。x:KeyにvalidationErrorTemplateという名前を定義することで、このテンプレートを他のコントロールから参照できます。
<Border BorderBrush="Red" BorderThickness="1">
<AdornedElementPlaceholder x:Name="adornedelem" />
</Border>
<Border BorderBrush="Red" BorderThickness="1">では赤枠の線を描画しています。
AdornedElementPlaceholderは、このテンプレートが適用されたコントロールを表示します。このテンプレートの呼び出し元はTextBoxなので、TextBoxが表示されます。
x:Name="adornedelem"でこの要素に名前を付けています。この名前は後ほどバインディングで使います。
<TextBlock Foreground="Red" Text="{Binding AdornedElement.(Validation.Errors)[0].ErrorContent, ElementName=adornedelem}" />
この実装ではAdornedElementで発生したエラー(ここでは TextBox)のバリデーションエラーメッセージを取得し、それを TextBlock の Text に表示するためにバインディングしています。
ElementName=adornedelemではバインディングソースとなる要素を指定しています。この実装がないと、どの要素のバリデーションエラー情報を取得すべきかわからず以下のバインディングエラーが発生します。
重大度レベル カウント データ コンテキスト バインド パス ターゲット ターゲット型 説明 ファイル 行 プロジェクト
エラー 9 ValidationError AdornedElement TextBlock.Text String 型 ValidationError のオブジェクトに AdornedElement プロパティが見つかりません。
一旦ビルドして今までの実装を確認します。

バリデーションエラーのメッセージが表示されていることが確認できました。
ValidatesOnExceptions
次にValidatesOnExceptionsを使用した方法を見ていきます。
これはデータバインディングした箇所にValidatesOnExceptions=Trueを指定し、ViewModel 側のSetterで発生させた例外をキャッチしてバリデーションエラーとして扱うことが出来ます。
XAML 側を以下のように修正して新しく TextBox を追加します。
+
+
ViewModel 側も以下のように修正します。
namespace MVVM.TextBoxValidation.ViewModels
{
public sealed class MainWindowViewModel : BindableBase
{
public string ValidationRuleTextBox
{
get => _validationRuleTextBox;
set => SetProperty(ref _validationRuleTextBox, value);
}
private string _validationRuleTextBox;
+ public string ValidatesOnExceptionsTextBox
+ {
+ get => _validatesOnExceptionsTextBox;
+ set
+ {
+ if(SetProperty(ref _validatesOnExceptionsTextBox, value))
+ {
+ if (string.IsNullOrWhiteSpace(value))
+ {
+ throw new ArgumentException("入力必須です。");
+ }
+ if (int.TryParse(value, out int number))
+ {
+ // int型の値に対するバリデーションを行う
+ if (number < 0)
+ throw new ArgumentException("負の値は入力できません。");
+ if (number > 10)
+ throw new ArgumentException("最大値を超えています。");
+ }
+ else
+ {
+ throw new ArgumentException("数値を入力してください。");
+ }
+ }
+ }
+ }
+ private string _validatesOnExceptionsTextBox;
public MainWindowViewModel()
{ }
}
}
バリデーションロジックなどはTextBoxRule.csと同様でそのままプロパティのSetterに記述しています。
例外は返すのではなくスローするように変更しています。
またValidation.ErrorTemplateプロパティに最初に作成したテンプレートを適応させることで同様にエラーメッセージを出力させることが出来ます。

ただ入力値によっては、バインディングされている値が変わる度に例外が発生するためか、パフォーマンスが悪くなる時がありました。
比較的実装は簡単ですが優先してこの方法を使用することは無いと思います。
INotifyDataErrorInfo
IDataErrorInfoを使う方法もありますが、改良されたINotifyDataErrorInfoを使用することが多いので今回は割愛します。
以下のように TextBox と Label を追加します。
+
+
+
+
今回はINotifyDataErrorInfoは直接 ViewModel に実装するのではなく、BindableBaseクラスに実装して共通化しておきます。
INotifyDataErrorInfoを実装すると以下の 3 つが強制的に定義されます。
bool HasErrorsevent EventHandler<DataErrorsChangedEventArgs>IEnumerable GetErrors(string? propertyName)
またいくつかprotectedなメソッドを実装し、継承先の ViewModel から呼び出せるようにします。
using System.Collections;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace MVVM.TextBoxValidation.ViewModels
{
///
/// プロパティ変更通知基底クラス
///
+ public abstract class BindableBase : INotifyPropertyChanged, INotifyDataErrorInfo
{
#region 継承先で使用する実装
///
/// エラーメッセージを格納するディクショナリ
///
+ protected Dictionary> _errors = new Dictionary>();
///
/// プロパティごとのバリデーションロジックを実装する
///
///
+ protected abstract void ValidateProperty(string propertyName);
///
/// 指定されたプロパティ名とエラーメッセージをエラーリストに追加する
///
/// エラーを追加するプロパティの名前
/// 追加するエラーメッセージ
+ protected void AddError(string propertyName, string error)
{
// 指定したキー(propertyName)に対応する値が存在するかチェック
// キーが存在すれば true を返し対応する値を out パラメータに割り当てる
// キーが無ければ false
// propertyName に対応するキーが無ければ新しくエラーリストを作成
if (!_errors.TryGetValue(propertyName, out var errorList))
{
errorList = new List();
_errors[propertyName] = errorList;
}
// リストにエラーメッセージが含まれていなかったら場合はメッセージを追加しエラーイベントを通知
if (!errorList.Contains(error))
{
errorList.Add(error);
// エラーが追加されたことを通知する
OnErrorsChanged(propertyName);
}
}
///
/// 指定したプロパティ名のエラーをクリアする
///
/// エラーをクリアするプロパティ名
+ protected void ClearErrors(string propertyName)
{
if(_errors.TryGetValue(propertyName, out List? errorList) && errorList != null)
{
errorList.Clear();
// エラーがクリアされたことを通知する
OnErrorsChanged(propertyName);
}
}
#endregion
///
/// 指定したプロパティ名に関連するエラーが変更されたときに発生するイベントを発行する
///
/// エラーが変更されたプロパティの名前
+ private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
#region INotifyDataErrorInfoインターフェース実装
///
/// エラーが存在するかどうかを取得する
///
/// エラーが存在する場合はtrue、存在しない場合はfalseを返す
+ public bool HasErrors => _errors.Any();
///
/// イベントハンドラー
///
+ public event EventHandler? ErrorsChanged;
///
/// 指定したプロパティ名に関するエラーを取得する
///
/// エラーを取得したいプロパティ名
/// プロパティ名に関連するエラーのリスト
+ public IEnumerable GetErrors(string? propertyName)
{
if (string.IsNullOrWhiteSpace(propertyName)) return Enumerable.Empty();
if (_errors.TryGetValue(propertyName, out List? errorList))
{
return errorList ?? Enumerable.Empty();
}
return Enumerable.Empty();
}
#endregion
#region データバインディングに必要な処理
///
/// プロパティ変更通知イベント
///
public event PropertyChangedEventHandler? PropertyChanged;
///
/// 値が変更されていた場合にのみOnPropertyChangedを実行する
///
///
/// 変更前のプロパティの値
/// 変更後のプロパティの値
/// 変更されたプロパティ名
/// 真偽値
+ protected virtual bool SetProperty(ref T storage, T value, bool validate = false, [CallerMemberName] string? propertyName = null)
{
if (Equals(storage, value)) return false;
storage = value;
OnPropertyChanged(propertyName);
+ if(validate && propertyName != null)
+ {
+ ValidateProperty(propertyName);
+ }
return true;
}
///
/// プロパティの変更を検知するメソッド
///
///
protected void OnPropertyChanged([CallerMemberName] string? property = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));
}
#endregion
}
}
処理の内容についてはコメントを見て頂ければ分かると思いますが、いくつかポイントを絞って解説します。
SetProperty<T>メソッドはもともとデータバインディングされているプロパティの値の変更通知を行うメソッドです。
このメソッドにbool validate = falseという引数を追加して、trueが渡ってきた場合に指定されたプロパティ名に対してValidatePropertyメソッドが実行されるように変更しました。
バリデーションを実行したいプロパティに対して以下のように記述します。
public string INotifyDataErrorInfoTextBox
{
get => _iNotifyDataErrorInfoTextBox;
set => SetProperty(ref _iNotifyDataErrorInfoTextBox, value, true); // true を追加する
}
private string _iNotifyDataErrorInfoTextBox;
また、ValidatePropertyメソッドはabstractとなっているため ViewModel で抽象メソッドの実装が強制されます。
protected override void ValidateProperty(string propertyName)
{
// 指定されたプロパティの既存のエラーを削除
ClearErrors(propertyName);
switch (propertyName)
{
case nameof(INotifyDataErrorInfoTextBox):
INotifyDataErrorInfoTextBoxValidate(INotifyDataErrorInfoTextBox, AddError);
break;
// 複数のプロパティのバリデーションを行いたい場合は追記する
default:
throw new ArgumentException("不正なプロパティ名です。");
}
}
INotifyDataErrorInfoTextBoxValidateメソッドはバリデーションロジックを記述します。引数のAddErrorメソッドは継承元のBindableBaseクラスに実装したprotectedなメソッドです。
private void INotifyDataErrorInfoTextBoxValidate(string value, Action<string, string> addError)
{
if (string.IsNullOrWhiteSpace(value))
{
addError(nameof(INotifyDataErrorInfoTextBox), "入力必須です。");
return;
}
if (int.TryParse(value, out int number))
{
if (number < 0)
addError(nameof(INotifyDataErrorInfoTextBox), "負の値は入力できません。");
if (number > 10)
addError(nameof(INotifyDataErrorInfoTextBox), "最大値を超えています。");
}
else
{
addError(nameof(INotifyDataErrorInfoTextBox), "数値を入力してください。");
}
}
これまではバリデーションに引っかかった場合に例外を発生させていましたが、渡ってきたメソッド(定義済みデリゲート)を実行してプロパティ名に対するエラーメッセージを格納しています。
そしてAddErrorメソッドでエラーメッセージが格納されるとOnErrorsChangedが実行されてエラーメッセージが格納されたことが通知されます。
ビルドして今までの実装を確認します。

エラーメッセージが表示されていることが確認できました。
エラー発生時のスタイルを修正
今回の内容とは異なりますが、Window.Resourcesに以下のスタイルを追加して、エラー発生時の TextBox をいい感じに装飾します。
<Style x:Key="ValidatedTextBoxStyle" TargetType="TextBox">
<Style.Triggers>
<Trigger Property="Validation.HasError" Value="True">
<Setter Property="BorderBrush" Value="#ff6366" />
<Setter Property="Background" Value="#ffe1df" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
</Trigger>
</Style.Triggers>
</Style>
各テキストボックスにStyle="{StaticResource ValidatedTextBoxStyle}"を追加します。

今回作成したアプリのリポジトリはこちらです。



