WPFでの単項データバインドまわり

前回はWindows Formsにおける単項データバインドでした。

WPFはそれ自体がBindingと深く関連づいておりネット上の情報も多いです。ただアレコレ登場するとわかりにくいかと思います、ここではWindowsFormsでやった単項データバインドをそのままWPFでライブラリを一切使わずやってみます。エッセンスだけを紹介しますのでライブラリを使ったら…,こんな時どうしたいか…,は他に譲りたいと思います。

 

データバインディング

WindowsFormsの場合同様、データバインドは【1.バインディング元になるデータ】,【2.バインディング先のUI】,【3.それらを接続するもの】の3つで構成されます。では【1.バインディング元になるデータ】は前回同様DisplayModelという単純クラスから始めていきます。

 DisplayModel.cs(1)

namespace WpfDataBindSample
{
    public class DisplayModel
    {
        private int a;
        private int b;

        public int A
        {
            get { return a; }
            set { a = value; }
        }
        public int B
        {
            get { return b; }
            set { b = value; }
        }
        public int Diff
        {
            get { return a - b; }
        }
    }
}

WindowsFormsの時と同じ単純なクラスから始まります。次は、【2.バインドする先のUI】を作ります。WPFなのでこんな感じ。

mainwindow

MainWindow.xaml(1)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161"/>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" Text="TextBox" VerticalAlignment="Top" Width="161"/>
        <Label Content="Label" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393"/>
    </Grid>
</Window>

ただコントロールを張り付けただけです。上から順にA, B, Diffをバインドするつもりで作りました。前回のWindowsFormsのエントリを読んでいただいた方はお気づきかもしれませんが、Aをバインドするコントロールは前回numericUpDownでした、それが今回はSliderになっています。その理由ですが、WPFにはnumericUpDownコントロールが存在しないためです。WPF上でもWindowsFormsのコントロールはWindowsFormsHostというWPFのコントロールを作ると使うことができますが、これからやろうとしているバインドができないのです!これをバインドさせる挑戦は今後やるとして、今回はSliderで代用しました。

最後に【3.それらを接続するもの】を作ります。WindowsFormsの場合BindingSourceでやりましたが、WPFではBindingクラスを使います。上のXAMLにBindingを施すとこんな感じ。

MainWindow.xaml(2)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161">
            <Slider.Value>
                <Binding Path="A" UpdateSourceTrigger="PropertyChanged"/>
            </Slider.Value>
        </Slider>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="161">
            <Binding Path="B" UpdateSourceTrigger="PropertyChanged"/>
        </TextBox>
        <Label Content="{Binding Path=Diff, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393" Height="28" Width="161"/>
    </Grid>
</Window>

いくつか書き方を変えてみましたが、全部同じです。Sliderに対してはSlider.Valueのプロパティ要素構文としてBindingを書いてみました。TextBoxに対してはTextBox.TextというプロパティにバインドしているのですがTextはコンテンツプロパティとして省略可能なのでこんな感じ。Labelに対してはマークアップ拡張という構文で書いてみました。上のSlider.Value, TextBox.Text, Label.Textはいずれも依存関係プロパティと呼ばれます。WPFのBindingはバインドできるのは”依存関係プロパティ”に限定されます、なのでWindowsFormsHost内のnumericUpDownはバインドできないのです。

ここではBindingを設定しましたが、UIは当然変わりません。Bindingを設定したら、実際にバインドするインスタンスを渡す必要があるので今回はDataContextを介してバインドします。

 MainWindow.xaml.cs

namespace WpfDataBindSample
{
    using System.Windows;
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            this.DataContext = new DisplayModel() { A = 5, B = 10 };
        }
    }
}

形だけはできました。これを実行してみるとわかりますが、UI→Modelへのバインドはできていますが、逆ができていません。前回やりましたね、INotifyPropertyChangedでModelからUIへ変更通知を送るんでした。

DisplayModel.cs(2)

namespace WpfDataBindSample
{
    using System.ComponentModel;
    public class DisplayModel : INotifyPropertyChanged
    {
        private int a;
        private int b;

        public int A
        {
            get { return a; }
            set {
                a = value;
                NotifyPropertyChanged("A");
                NotifyPropertyChanged("Diff");
            }
        }
        public int B
        {
            get { return b; }
            set {
                b = value;
                NotifyPropertyChanged("B");
                NotifyPropertyChanged("Diff");
            }
        }
        public int Diff
        {
            get { return a - b; }
        }

        public event PropertyChangedEventHandler PropertyChanged = (_, __) => {};
        private void NotifyPropertyChanged(string propertyName = "")
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

前回のWindowsFormsの場合ではDiffプロパティの変更通知を省略していました。これはバインド先のBindingSourceがINotifyPropertyChangedインターフェースとは無関係にデータを取得しなおすためでした。本来は今回のWPF版のように関連するプロパティ更新を通知する必要があります。これでバインドが動き出しました。

 

2.データ検証を追加する

WindowsFormsでのデータ検証はIDataErrorInfoを実装するんでした。WPFの場合も同じです!(上のINotifyPropertyChangedといい、同じことばかりですね)ただ、WindowsFormsの場合はerrorProviderコントロールがあったのですが、WPFにはありません。どうするのでしょうか?ま、実装していきましょう。

 DisplayModel.cs(3)

namespace WpfDataBindSample
{
    using System.ComponentModel;
    public class DisplayModel : INotifyPropertyChanged, IDataErrorInfo
    {
        private int a;
        private int b;

        public int A
        {
            get { return a; }
            set {
                a = value;
                NotifyPropertyChanged("A");
                NotifyPropertyChanged("Diff");
            }
        }
        public int B
        {
            get { return b; }
            set {
                b = value;
                NotifyPropertyChanged("B");
                NotifyPropertyChanged("Diff");
            }
        }
        public int Diff
        {
            get { return a - b; }
        }

        public event PropertyChangedEventHandler PropertyChanged = (_, __) => {};
        private void NotifyPropertyChanged(string propertyName = "")
        {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }

        public string Error{
            get { return string.Empty; }
        }
        public string this[string columnName]{
            get{
                if (columnName == "B" && B < 0){
                    return "負数は駄目だよ!";
                }
                return string.Empty;
            }
        }
    }
}

MainWindow.xaml(3)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161">
            <Slider.Value>
                <Binding Path="A" UpdateSourceTrigger="PropertyChanged"/>
            </Slider.Value>
        </Slider>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="161">
            <Binding Path="B" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True"/>
        </TextBox>
        <Label Content="{Binding Path=Diff, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393" Height="28" Width="161"/>
    </Grid>
</Window>

Xamlの(2)から(3)への変更点はTextBoxのBindingで、ValidatesOnDataErrosをTrueにしたことです。これをTrueにするとModelのIDataErrorInfoのエラーを受け付けられるようになるわけです。WindowsFormsでErrorProviderを付けた場合はこれがTrueになっているような状態と言えますね。これを実行するとこんな感じ。

idataerrorinfo_mainwindow

この状態でさらにWindowsFormsのErrorProviderのようにエラーメッセージを表示させるには、

 MainWindow.xaml(4)

<Window x:Class="WpfDataBindSample.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="187" Width="293" ResizeMode="NoResize">
    <Window.Resources>
        <Style TargetType="TextBox">
            <Style.Triggers>
                <Trigger Property="Validation.HasError" Value="true">
                    <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=(Validation.Errors)[0].ErrorContent}"/>
                </Trigger>
            </Style.Triggers>
        </Style>
    </Window.Resources>
    <Grid>
        <Label Content="A" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top"/>
        <Label Content="B" HorizontalAlignment="Left" Margin="10,53,0,0" VerticalAlignment="Top"/>
        <Label Content="Diff" HorizontalAlignment="Left" Margin="10,96,0,0" VerticalAlignment="Top"/>

        <Slider HorizontalAlignment="Left" Margin="77,16,0,0" VerticalAlignment="Top" Width="161">
            <Slider.Value>
                <Binding Path="A" UpdateSourceTrigger="PropertyChanged"/>
            </Slider.Value>
        </Slider>
        <TextBox HorizontalAlignment="Left" Height="23" Margin="77,53,0,0" TextWrapping="Wrap" VerticalAlignment="Top" Width="161">
            <Binding Path="B" UpdateSourceTrigger="PropertyChanged" ValidatesOnDataErrors="True"/>
        </TextBox>
        <Label Content="{Binding Path=Diff, UpdateSourceTrigger=PropertyChanged}" HorizontalAlignment="Left" Margin="77,96,0,0" VerticalAlignment="Top" RenderTransformOrigin="-0.78,0.393" Height="28" Width="161"/>
    </Grid>
</Window>

errorstring

エラーが1つもなかった状態から1つ発生すると、Validation.HasErrorがTrueになります。これをトリガーとしてTextBoxであればToolTipにエラーメッセージをバインドさせて表示させています。具体的なエラーはValidation.Errosコレクションに入ってくるので先頭要素を取り出しています。

さらに。WindowsFormsにもっと近づけようとエラー時にコントロールを赤枠で囲うんじゃなくて’‘アイコンを出したい場合は、Validation.ErrorTemplateを使いますがAdvancedな内容なのでここでは取り上げません。上記ではBindingのValidatesOnDataErrorsをTrueにすると、IDataErrorInfoのエラーが受け取れることがわかりました。WPFではバインド中に発生する例外も受け取ることができます(通常バインド中の例外は無視されます)。それにはValidatesOnExceptionsをTrueにするだけです。

さらにさらに。MSDN等を見ているとValidationRuleというクラスに気付くと思いますが、ValidatesOnDataErrorsをTrueにすることや、ValidatesOnExceptionsをTrueにすることはよく使うValidationRuleを適用しているということです。さらに凝ったことをしたい場合にValidationRuleを使うと覚えておけばよいかと思います。

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中