前回:[Silverlight][C#]Silverlight2での入力値の検証 その3
前回で、IDataErrorInfoインターフェースを作って、それを実装する小さなサンプルを作りました。
だけど、まだ本題の入力値の検証は、PersonViewModelクラスでこっそり行われているものの、その結果をViewにフィードバックしたりとかいった部分が全然出来ていません。
ということで、ここで入力値のエラー状態をViewにフィードバックするようにしてみようと思います。
Silverlightには、そこらへんのことをしてくれるコントロールとかは、ぱっと見見当たらないので、ガリガリ作っていきます。
ということで、今回のエントリの目標は「IDataErrorInfoインターフェースとINotifyPropertyChangedインターフェースを実装したクラスで発生した検証エラーを表示するコントロールの作成」です。
現時点での、Visual Studio 2008 SP1では、Silverlightのコントロールの作成のためのウィザードとかは無いので自分でクラスを作ったりしないといけません。
今回作るコントロールの名前は「DataError」にします。
早速作ってみよう
ということでControlを継承したDataErrorというクラスを作成します。
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace ValidationSampleSL
{
public class DataError: Control
{
}
}
ビルドすれば、Page.xamlに置くことが出来ます。
まだ、何もしてないので表示すらされませんが、横においてみます。
<UserControl x:Class="ValidationSampleSL.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:ValidationSampleSL"
Width="400" Height="300">
<UserControl.DataContext>
<l:PersonViewModel />
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="名前:" />
<TextBox Text="{Binding Name, Mode=TwoWay}" Width="250"/>
<l:DataError /> <!-- 置いてみた -->
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="年齢:" />
<TextBox Text="{Binding Age, Mode=TwoWay}" Width="250"/>
<l:DataError /> <!-- 置いてみた -->
</StackPanel>
<Button Content="自己紹介" Click="Button_Click"/>
<TextBlock Text="{Binding GreetMessage, Mode=TwoWay}" />
</StackPanel>
</Grid>
</UserControl>
ちょっと脱線
ここで、少し気づいてしまったので脱線します。(作りながらBlogの記事書いてるのでこんなことに…)
昨日、寝る前にエントリを書いていたのでしょうもないバグを埋め込んでました。2つあるので1つずつ解決していきます。
ViewModelBaseクラスにバグがありました。ここでちょっと脱線しますが修正します。
SetErrorメソッドで_errors.Add(propertyName, error);と書いてるせいで、同じ名前のプロパティで2回エラーメッセージが登録されようとすると、例外が発生してしまいますorz
ということで以下のように修正しました。
/// <summary>
/// 指定したプロパティにエラー情報をセットする
/// </summary>
/// <param name="propertyName"></param>
/// <param name="error"></param>
protected void SetError(string propertyName, string error)
{
_errors[propertyName] = error;
}
続いて、PersonViewModelです。
入力値の検証が、プロパティに値がセットされるまで行われません。そのため、初期状態では何もプロパティに値がセットされてないので、検証エラーが出るべきなのですが、エラー無しの状態になってました。
とりあえず、コンストラクタで入力値の検証メソッドを呼ぶようにしました。
public PersonViewModel()
{
ValidateName();
ValidateAge();
}
とりあえず表示させよう
置いてみても何も出ないんじゃ気が乗らないので、とりあえず、表示してみようと思います。
プロジェクト直下にThemesフォルダを作って、その下にgeneric.xamlという名前のファイルをテキストファイルとして新規作成します。
generic.xamlに以、WPFと同じようにStyleを定義していきます。とりあえず見た目だけを作りたいので、DataErrorのスタイルのTemplateプロパティだけ設定します。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:ValidationSampleSL">
<Style TargetType="l:DataError">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="l:DataError">
<!-- とりあえずね -->
<TextBlock Text="仮の見た目" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
このスタイルを使うようにDataErrorクラスのコンストラクタでDefaultStyleKeyプロパティにDataErrorの型を指定します。
using System;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Ink;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;
namespace ValidationSampleSL
{
public class DataError: Control
{
public DataError()
{
// これで、generic.xamlのスタイルと紐付ける
this.DefaultStyleKey = typeof(DataError);
}
}
}
これで実行するとテキストボックスの横に表示されるようになります。
エラー表示機能を作っていこう
やっと本題!!エラー表示の機能をつくっていきます。
DataErrorコントロールは、DataContextの指定されたプロパティのエラーを表示する機能を作りこむ必要があります。
この監視対象のプロパティの指定は、stringでプロパティ名を設定するようにします。PropertyNameという依存プロパティをDataErrorコントロールに追加します。
/// <summary>
/// 監視対象のプロパティ名を取得または設定する
/// </summary>
public string PropertyName
{
get { return (string)GetValue(PropertyNameProperty); }
set { SetValue(PropertyNameProperty, value); }
}
public static readonly DependencyProperty PropertyNameProperty = DependencyProperty.Register(
"PropertyName",
typeof(string),
typeof(DataError),
new PropertyMetadata(PropertyNameChanged));
// 監視対象のプロパティ名が変わったときの処理
private static void PropertyNameChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
// TODO : あとで
}
次に、DataContextプロパティも監視しないといけません。
これは、ダミーのMyDataContextプロパティを作って、PropertyMetadataを使って変更時の処理を設定します。このMyDataContextプロパティとDataContextをバインドすることでDataContextの変更を監視します。
#region MyDataContextProperty
/// <summary>
/// DataContextの変更を感知するためのダミープロパティ
/// </summary>
private static readonly DependencyProperty MyDataContextProperty = DependencyProperty.Register(
"MyDataContext",
typeof(object),
typeof(DataError),
new PropertyMetadata(DataContextChanged));
// DataContextが変更されたときの処理
private static void DataContextChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var self = (DataError) sender;
self.DataContextChanged(e);
}
public void DataContextChanged(DependencyPropertyChangedEventArgs e)
{
// TODO : DataContextが変更されたときの処理を書く
}
#endregion
コンストラクタに、DataContextプロパティとMyDataContextプロパティをバインドする処理を追加します。
// DataContextとMyDataContextをバインド(DataContextの変更を監視するために)
SetBinding(MyDataContextProperty, new Binding());
これで、DataContextの変更の監視と、何のプロパティを監視するのかを設定するPropertyNameプロパティの定義が終わりました。
次に、監視対象のプロパティで発生したエラーメッセージを保持するためのプロパティを定義します。
こいつは外部から設定される必要は無いのでsetはprivateとして定義します。
#region ErrorMessageプロパティ
/// <summary>
/// 監視対象のプロパティのエラーメッセージを取得する
/// </summary>
public string ErrorMessage
{
get { return (string)GetValue(ErrorMessageProperty); }
private set { SetValue(ErrorMessageProperty, value); }
}
public static readonly DependencyProperty ErrorMessageProperty = DependencyProperty.Register(
"ErrorMessage",
typeof(string),
typeof(DataError),
new PropertyMetadata(null));
#endregion
粒は揃ったので、内部の処理を作りこんでいきます。
DataContextから、監視対象のプロパティのエラーメッセージを取得してErrorMessageプロパティにセットする処理を作ります。この処理は、UpdateStateというprivateメソッドにしました。
// エラーメッセージを最新の状態にする
private void UpdateState()
{
var errorInfo = this.DataContext as IDataErrorInfo;
if (errorInfo == null)
{
// IDataErrorInfoじゃない場合はErrorMessageを無しに
ErrorMessage = null;
return;
}
// DataContextからエラーメッセージを取得してErrorMessageプロパティに設定する
ErrorMessage = errorInfo[this.PropertyName];
}
次に、DataContextのプロパティが変更されたときの処理を追加します。
これは、INotifyPropertyChangedインターフェースのPropertyChangedイベントのハンドラとして登録するので、引数はobjectとPropertyChangedEventArgsになります。
こいつはprivateのDataContextPropertyChangedメソッドとして実装します。
// DataContextにプロパティの変更があったときの処理
private void DataContextPropertyChanged(object sender, PropertyChangedEventArgs e)
{
// 自分が監視する対象のプロパティの場合に状態を更新する
if (e.PropertyName != this.PropertyName) return;
UpdateState();
}
このDataContextPropertyChangedをイベントハンドラとして登録するのは、DataContextが変わったときが一番いいので、さっき作ったDataContextChangedメソッドにその処理を書きます。
public void DataContextChanged(DependencyPropertyChangedEventArgs e)
{
// 古いDataContextに追加してたイベントハンドラを削除
var oldDataContext = e.OldValue as INotifyPropertyChanged;
if (oldDataContext != null)
{
oldDataContext.PropertyChanged -= DataContextPropertyChanged;
}
// 新しいDataContextにイベントハンドラを追加
var dataContext = e.NewValue as INotifyPropertyChanged;
if (dataContext != null)
{
dataContext.PropertyChanged += DataContextPropertyChanged;
}
// 状態を更新
UpdateState();
}
後は、監視対象のプロパティ名が変わったときにエラーメッセージを最新化する必要があるので、PropertyNameプロパティが変更されたときの処理でUpdateStateを呼ぶようにします。ということで、さっき作ったPropertyNameChangedメソッドの中身を以下のようにします。
// 監視対象のプロパティ名が変わったときの処理
private static void PropertyNameChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
var self = sender as DataError;
if (self == null) return;
// 状態を更新
self.UpdateState();
}
最後に、気分的にエラーメッセージは赤色というイメージがあるので、コンストラクタで前景色を赤色にしてしまう処理を追加します。
public DataError()
{
// これで、generic.xamlのスタイルと紐付ける
this.DefaultStyleKey = typeof(DataError);
// DataContextとMyDataContextをバインド(DataContextの変更を監視するために)
SetBinding(MyDataContextProperty, new Binding());
// デフォの前景色は赤色
Foreground = new SolidColorBrush(Colors.Red);
}
これで、C#のコード側は完成です。
コントロールの見た目を作っていこう
先ほど、仮の見た目としてTextBlockだけを置いた奴をちゃんと作りこんでいきます。
といってもやることは、Borderを置いて背景とかパディングとかマージンをバインドして、その中にTextBlockを置いてErrorMessageプロパティとForegroundプロパティをバインドするくらいです。
もっと凝った見た目にしたい場合は、必要に応じてスタイルを設定してTemplateを上書きしてもらえばいいかな。
ということで、generic.xamlは以下のようになります。
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:ValidationSampleSL">
<Style TargetType="l:DataError">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="l:DataError">
<Border Background="{TemplateBinding Background}"
Margin="{TemplateBinding Margin}"
Padding="{TemplateBinding Padding}">
<!-- エラーメッセージを表示する -->
<TextBlock Text="{TemplateBinding ErrorMessage}"
Foreground="{TemplateBinding Foreground}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
以上で、コントロールは完成です。
早速試してみよう
Page.xamlを以下のように変更して、DataErrorコントロールに監視対象のプロパティを指定します。
今回の例では、上のほうのDataErrorコントロールはNameを、下のほうのDataErrorコントロールはAgeを設定しました。
<UserControl x:Class="ValidationSampleSL.Page"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:l="clr-namespace:ValidationSampleSL">
<UserControl.DataContext>
<l:PersonViewModel />
</UserControl.DataContext>
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<TextBlock Text="名前:" />
<TextBox Text="{Binding Name, Mode=TwoWay}" Width="250"/>
<l:DataError PropertyName="Name"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="年齢:" />
<TextBox Text="{Binding Age, Mode=TwoWay}" Width="250"/>
<l:DataError PropertyName="Age" />
</StackPanel>
<Button Content="自己紹介" Click="Button_Click"/>
<TextBlock Text="{Binding GreetMessage, Mode=TwoWay}" />
</StackPanel>
</Grid>
</UserControl>
それでは実行してみます。
ちゃんと、エラーメッセージが表示されてます。名前に何か入力してフォーカスを移動させると…
エラーメッセージが消えました。
次に年齢に数字以外を入力すると…
ちゃんと整数値を入力すると…
エラーメッセージが消えました。ばっちり!!
いい感じかも?
プロジェクトのダウンロードは以下からどうぞ。一応同じものをVBでも作ってみました。
- C#版
- VB版(VB初心者なので間違いや不自然な表記があったらコメント下さい)