どうも、僕です。
ザムルってもうワクワクしますよね。
ということで、Qiita XAML Advent Calendar 2014 21日目のエントリーになります。
WPFではコントロールのプロパティにバインドされた値が不正値でないかどうか
評価する仕組みとして、Validationというのを持っています。
例えば、TextBoxのTextプロパティにint型のプロパティをTwoWayでバインドして、
画面から整数でない文字を入れると赤い枠が表示されます。
これが標準装備のValidation。
このルールをもっと細かくしたい、たとえば、
入力できる数値の範囲を制限したいとします。0~100とか。
そういうときはValidationRuleを自作する。
public class IntRangeRule : ValidationRule { public int MinValue { get; set; } public int MaxValue { get; set; } public IntRangeRule() { //既定値 MinValue = int.MinValue; MaxValue = int.MaxValue; } public override ValidationResult Validate(object value, CultureInfo cultureInfo) { if (value == null) return new ValidationResult(false, "値がNullです"); int inputNum; if (!int.TryParse(value.ToString(), out inputNum)) { return new ValidationResult(false, "値の形式が不正です"); } if (inputNum < MinValue || MaxValue < inputNum) { return new ValidationResult(false, "値が範囲外です"); } return ValidationResult.ValidResult; } }
こんなクラス。まあ直感的にわかりやすいですね。
これを以下のように使用します。
<Window x:Class="BindingValidationRuleSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:BindingValidationRuleSample" Title="MainWindow" Height="240" Width="360"> <Window.DataContext> <my:MainWindowViewModel /> </Window.DataContext> <Grid> <TextBox HorizontalAlignment="Left" Height="23" Margin="47,0,0,0" VerticalAlignment="Center" Width="120"> <TextBox.Text> <Binding Path="InputNum" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <my:IntRangeRule MinValue="0" MaxValue="100" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </Grid> </Window>
public class MainWindowViewModel { public int InputNum { get; set; } }
問題はここからだ。
xamlで設定してる、MinValue="0", MaxValue="100"っていうのを
Bindingでコンテクスト側から指定したいとする。
↓こんなイメージで。
<TextBox HorizontalAlignment="Left" Height="23" Margin="47,0,0,0" VerticalAlignment="Center" Width="120"> <TextBox.Text> <Binding Path="InputNum" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <my:IntRangeRule MinValue="{Binding MinInputValue}" MaxValue="{Binding MaxInputValue}" /> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox>
ところが、これは書けないですよね。
BindingはDependencyProperty(依存関係プロパティ)に対してしか使えないのです。
それならば、MinValueとMaxValueをDependencyPropertyにしてやるぞと思う。
ところがところが、DependencyPropertyを持つクラスは
DependencyObjectのサブクラスでないといけないのだが、
このIntRangeRuleはValidationRuleのサブクラスですよね!
こいつはDependencyObjectではない!
さてどうするかというと、DependencyPropertyにしたいプロパティを
ラップするだけのDependencyObjectサブクラスを作ります。
public class DependencyInt : DependencyObject { //依存関係プロパティ public static readonly DependencyProperty ValueProperty = DependencyProperty.Register("Value", typeof(int), typeof(DependencyInt)); //CLRプロパティ public int Value { get { return (int)GetValue(ValueProperty); } set { SetValue(ValueProperty, value); } } }
MinValueとMaxValueを一緒にひとつのクラスに入れちゃってもOKですが
今回は本当にIntのプロパティ1つだけのクラスにしました。
これを使ってIntRangeRuleを書き換える。
public class IntRangeRule : ValidationRule { public DependencyInt MinValue { get; set; } public DependencyInt MaxValue { get; set; } public IntRangeRule() { //既定値 MinValue = new DependencyInt { Value = int.MinValue }; MaxValue = new DependencyInt { Value = int.MaxValue }; } public override ValidationResult Validate(object value, CultureInfo cultureInfo) { if (value == null) return new ValidationResult(false, "値がNullです"); int inputNum; if (!int.TryParse(value.ToString(), out inputNum)) { return new ValidationResult(false, "値の形式が不正です"); } if (inputNum < MinValue.Value || MaxValue.Value < inputNum) { return new ValidationResult(false, "値が範囲外です"); } return ValidationResult.ValidResult; } }
これでバインドできるさね。
<Window x:Class="BindingValidationRuleSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:BindingValidationRuleSample" Title="MainWindow" Height="240" Width="360"> <Window.DataContext> <my:MainWindowViewModel /> </Window.DataContext> <Grid> <TextBox HorizontalAlignment="Left" Height="23" Margin="47,0,0,0" VerticalAlignment="Center" Width="120"> <TextBox.Text> <Binding Path="InputNum" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <my:IntRangeRule> <my:IntRangeRule.MinValue> <my:DependencyInt Value="{Binding MinInputValue}" /> </my:IntRangeRule.MinValue> <my:IntRangeRule.MaxValue> <my:DependencyInt Value="{Binding MaxInputValue}" /> </my:IntRangeRule.MaxValue> </my:IntRangeRule> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </Grid> </Window>
public class MainWindowViewModel { public int InputNum { get; set; } public int MinInputValue { get { return 0; } } public int MaxInputValue { get { return 100; } } }
これでうまくいくかと思いきや、いま一歩のところでこれは機能しません!
なにがだめかというと、ValidationRuleはVisualTreeで繋がってないので、
DataContextが共有されないんすね。
なので、先ほど作ったDependencyIntのバインドソースは、改めて指定してやる必要がある。
ここはもうゴリッとやってしまいますよ。ずばばば。
<Window x:Class="BindingValidationRuleSample.MainWindow" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:my="clr-namespace:BindingValidationRuleSample" Title="MainWindow" Height="240" Width="360"> <Window.Resources> <my:MainWindowViewModel x:Key="vm" /> </Window.Resources> <Grid DataContext="{StaticResource vm}"> <TextBox HorizontalAlignment="Left" Height="23" Margin="47,0,0,0" VerticalAlignment="Center" Width="120"> <TextBox.Text> <Binding Path="InputNum" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged"> <Binding.ValidationRules> <my:IntRangeRule> <my:IntRangeRule.MinValue> <my:DependencyInt Value="{Binding MinInputValue, Source={StaticResource vm}}" /> </my:IntRangeRule.MinValue> <my:IntRangeRule.MaxValue> <my:DependencyInt Value="{Binding MaxInputValue, Source={StaticResource vm}}" /> </my:IntRangeRule.MaxValue> </my:IntRangeRule> </Binding.ValidationRules> </Binding> </TextBox.Text> </TextBox> </Grid> </Window>
上記のようにできないケースもあると思いますが、その場合はうまいことやってください。
以上、ValidationRuleなんてどの程度使う機会があるかわかんないですが、
そんなことより、とにかくBindingで解決できるとなんか嬉しくなりませんか!