2012年4月6日金曜日

wpf : ValidationRuleをC#のコードから追加

TextBoxなどの入力値検証でValidationRuleを継承したクラスを作ることが多いですよね。 たいていの場合はxamlで追加します。

<TextBox>
    <TextBox.Text>
        <Binding>
            <Binding.ValidationRules>
                <local:なんたらValidationRule/>
            </Binding.ValidationRules>
        </Binding>
    </TextBox.Text>
</TextBox>

xamlでValidationRuleを追加するコードを書いた場合、自作ValidationRuleはどこかで勝手にインスタンス化されます。 このインスタンスにアクセスできれば、次のようなことが可能になります。

  • 検証の閾値を動的に設定。
  • 他のクラスに持たせた値を検証に使用。

ValidationRuleのインスタンスを探すには、Bindingクラスのインスタンスからたどっていきます。 例えば、xamlで<TextBox Name="textBox"/>と書いて作ったTextBoxのBindingは、

Binding textBinding =
    BindingOperations.GetBinding(textBox, TextBox.TextProperty);

BindingのプロパティであるValidationRulesコレクションに自作ValidationRuleが収まっています。 そのコレクションから登録したルールを探し出せばあれこれ複雑な検証が可能...

ってちょっとコード量が多くなりますよね。 xamlで追加して探すより、C#のコードでValidationRuleのインスタンスを追加した方が早そうです。

Binding textBinding =
    BindingOperations.GetBinding(textBox, TextBox.TextProperty);
なんたらValidationRule validationRule = new なんたらValidationRule();
textBinding.ValidationRules.Add(validationRule);

ValidationRuleへの参照をとっておけば、あれこれ複雑な検証が可能になります。

ちなみに、バインドした値を検証する機会はValidationRule以外にもあります。 プロパティのsetterなどです。 プロパティのsetterでの検証は多くの場合必須になります。 どちらで検証するのが筋が通っているかとか、コードが短くなるかとか、よく考えた方がいいかもしれませんね?

入力値の検証については、前の投稿でもちょっと書きました。 参考になるかもしれないので、ヒマな人はどうぞ。

以下、サンプルコードです。 まずはMainWindow.xaml。

<Window x:Class="AddValidationRuleOnCS.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525">
    <Window.Resources>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="True">
                    <Setter Property="ToolTip">
                        <Setter.Value>
                            <Binding
                                    Path="(Validation.Errors)[0].ErrorContent"
                                    RelativeSource="{x:Static RelativeSource.Self}"
                            />
                        </Setter.Value>
                    </Setter>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <StackPanel>
        <TextBox
                Name="textBox"
                Text="{Binding
                        ElementName=textBlock,
                        Path=Text,
                        UpdateSourceTrigger=PropertyChanged
                }"
        />
        <TextBlock Margin="20,4">↓検証済み文字列</TextBlock>
        <TextBlock Name="textBlock"/>
    </StackPanel>
</Window>

TextBoxに文字を入力すると、検証に通ったものだけが下のTextBlockに表示されます。 検証を通らなかった場合、TextBoxの文字にマウスカーソルを合わせるとポップアップでエラーメッセージが表示されます。

C#のコードでValidationRuleを追加するということで、xamlにはBinding.ValidationRules要素を書いていません。 ちょっとだけスッキリ。

次はMainWindow.xaml.csです。

// MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;

namespace AddValidationRuleOnCS
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Binding textBinding = BindingOperations.GetBinding(textBox, TextBox.TextProperty);
            TestValidationRule validationRule = new TestValidationRule(this);
            textBinding.ValidationRules.Add(validationRule);
        }

        public int TextMaxLength = 10;
    }
}

コンストラクタで自作のValidationRuleを追加しています。 TextMaxLengthフィールドはTestValidationRuleから使います。 本来なら、固定数値を渡すだけならTestValidationRuleにプロパティを追加すればいいだけなんですが、今回はサンプルって事で簡単なものにしました。

最後にTestValidationRule.csです。

// TestValidationRule.cs
using System.Globalization;
using System.Windows.Controls;

namespace AddValidationRuleOnCS
{
    class TestValidationRule : ValidationRule
    {
        private MainWindow _owner;

        public TestValidationRule(MainWindow owner)
        {
            _owner = owner;
        }

        public override ValidationResult Validate(object value, CultureInfo cultureInfo)
        {
            if (!(value is string))
                return new ValidationResult(false, "文字列じゃない");

            string param = (string)value;
            if(_owner.TextMaxLength < param.Length)
                return new ValidationResult(false, "文字列長すぎ");

            return new ValidationResult(true, null);
        }
    }
}

MainWindowクラスのフィールド値を使って検証しています。