徒然電脳

日々のプログラミング(とその他)忘備録 このサイトは独自研究のみに基づきます マサカリ歓迎しますというかよろしくお願いします

WPFの簡単なまとめ Binding編

1回:WPFの簡単なまとめ Resorce編 - 徒然電脳
2回:WPFの簡単なまとめ Style編 - 徒然電脳
3回:WPFの簡単なまとめ Template編 - 徒然電脳

はい、4回目

代入ではなく結びつける

バインディングとは「一箇所の変更が、全体に伝わる」ということのようです。
つまりプロパティの変更をするだけで、それを参照する全てにその変更が伝わるということです。

ここでは、「なぜ値を直接代入するのではなく、バインディングを使うのか」という趣旨で書こうとした節でしたが、どうも難しい(←十分理解していない証拠)。
とりあえず、一言二言の理解でしか無いのだけれども
「プロパティ一つの変更のために、(複数の)オブジェクトへの代入を変更のたびに実行しなくても良くなるよ」
というところで終わらせていただきます。

具体的に書く

とりあえず、テキストボックスの値をラベルにバインディングするXAMLを書いてみましょう。

MainWindow.xaml

<Window x:Class="Binding.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">
    <StackPanel>
        <TextBox Name="test"/>
        <Label Content="{Binding Text}" DataContext="{x:Reference Name=test}"/>
    </StackPanel>
</Window>

TextBoxにはtestという名前が付けられています。LabelのDataContextではx:Referenceマークアップ拡張によってtestという名前のインスタンスを参照するように指定してあります。もちろん、TextBoxのことです。そして、ContentにはBindingマークアップ拡張でTextが指定されていますね。

DataContextはバインディングを行なうためのインスタンスを一つ代入します。
そしてBindingで、そのインスタンスのプロパティを指定してあげます。
つまり、Labelに書かれている意味は
「testというインスタンスのTextプロパティを私(Label)のContentプロパティにバインディングして!」
という感じです。

自作のデータで

実際のアプリケーションでは、上のようなコードを書くことは稀のような気がします。多くの場合、外部の自作クラスのプロパティを呼び込んだりするんだろうと思います。そこで、次のようなコードを書いてみました。usingディレクティブは省略します。

MainWindow.xaml

<Window x:Class="Binding.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">
    <StackPanel>
        <Label Content="{Binding Age}" Name="ageLabel"/>
        <Button Click="Button_Click" Content="Age++"/>
    </StackPanel>
</Window>

MainWindow.xaml.cs

namespace Binding {
    public partial class MainWindow : Window {
        Person person;

        public MainWindow() {
            InitializeComponent();
            person = new Person() { Age = 18 };
            ageLabel.DataContext = person;
        }

        //ボタンクリックのたびに年をとる魔法のイベント
        private void Button_Click(object sender, RoutedEventArgs e) {
            person.Age++;
        }
    }

    class Person {
        int age;
        public string Name { get; set; }
        public int Age { 
            get { return age; }
            set { age = value >= 0 ? value : age; }
        }
    }
}

さぁ実行しましょう。最初はきちんと18って表示されますね!
予想通り、ボタンをクリックするたびにラベルの年齢はあがり・・・ません。

ボタンをクリックするたびAgeは1つずつ増加していくはずです。それなのにラベルの表示値は18のまま。これは、「変更の通知」がされてないためです。

変更通知

変更を通知する機能を持つためには、INotifiyPropertyChanged(System.Compornent名前空間)インターフェイスを実装しなきゃいけません。INotifiyPropertyChangedインターフェイスはPropertyChangedというイベントハンドラしか持っていない質素なインターフェイスです。プロパティが変更された時に、こいつを発生させるようにしてやればいいですね!
それじゃぁ、Personクラスを次のように書き換えましょう。

MainWindow.xaml.cs Personクラス

class Person : INotifyPropertyChanged{
    int age;
    string name;

    public event PropertyChangedEventHandler PropertyChanged;

    public string Name {
        get { return name; }
        set { name = value; NotifiyPropertyChanged("Name"); }
    }
    public int Age { 
        get { return age; }
        set {
            if (value >= 0) {
                age = value;
                NotifiyPropertyChanged("Age");
            }
        }
    }

    //イベントが実装されてれば実行する。
    private void NotifiyPropertyChanged(String propertyName) {
        if (PropertyChanged != null) {
            PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

PropertyChangedEventHandlerの第一引数はsenderです。基本的にはthisを入れてやります。第二引数はPropertyChangedEventArgsのインスタンスを入れてやります。そのインスタンスを生成するときには、変更されたプロパティ(今回ならAgeやName)の名前を渡してやります。実はこの名前がBinding時に指定するプロパティ名です。もしこの時AgeではなくToshiって書いてしまったら、XAMLでも{Binding Toshi}って書き直さなきゃいけません。混乱を避けるために、できるだけ実際のプロパティ名と一致させたいですね。
なおPropertyChangedイベントハンドラには、バインディング先(今回ならLabelのこと)でPersonクラスでの変更が起きた際の処理が追加されているはずです。

方向指定

これまでのバインディングはAの変更をBに適応させるという一方通行的なバインディングでした。ここではその方向を操ってあげましょう。といっても、かなり簡単です。xamlを次のように書き換えてください。

MainWindow.xaml

<Window x:Class="Binding.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">
    <StackPanel>
        <!-- Binding Mode -->
        <TextBox Text="{Binding Age, Mode=TwoWay}" Name="ageLabel"/>
        <Button Click="Button_Click" Content="Age++"/>
    </StackPanel>
</Window>

ただ単にBindingにModeが追加されただけです。はい。
実行してみると、先ほどと変わりませんね。18からボタンをおすたびに加算されていきます。
違うのはここからです。LabelではなくTextBoxに変わっているので、その値を変えてみましょう。
そしてボタンを押します。すると、その変えた値から1加わりました。

・・・
だからどうした。という声が聞こえてきそうです。いえいえ、これはとても大事です。1加算された、ということはテキストボックスの値だけでなく、Ageの値も変更されていたということなのですから。もしAgeの値が変更されていなければ、テキストボックスの値を変更する前の値に対して1加算されたものが表示されるだけだったでしょう。
なぜこんなことが起きたか。それはMode=TwoWayのおかげだということはわざわざ言わなくても大丈夫ですね。

モードの種類です。
OneWay・・・バインディング元からバインディング先への変更のみ適応
TwoWay・・・バインディング元からの変更もバインディング先からの変更も適応
OneWayToSource・・・バインディング先からバインディング元への変更のみ適応
OneTime・・・一度だけバインディング元からバインディング先への変更を適応
Default・・・初期設定を使う。要素によって異なる

Modeを指定しないとDefaultとなりますが、これは要素によって異なります。たとえば、LabelならOneWay、TextBoxだとTwoWayです。混乱を避けるために、できるだけModeは明示したいところです。

コンバート

なんか歳なのにただの数値として出力するのはそっけないですね。数値の横に「歳」の漢字を挿入してやりたいところです。これはコンバータを使って実装できます。とりあえずコンバータクラスを作りましょう。

MainWindow.xaml.cs AgeConverterクラス

    class AgeConverter : IValueConverter {
        object IValueConverter.Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            return ((int)value).ToString() + "歳";
        }

        object IValueConverter.ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) {
            int a = 0;
            int.TryParse(((string)value).Substring(0, ((string)value).Length - 1), out a);
            return a;
        }
    }

コンバータクラスはIValueConverterインターフェイスを実装してやります。
Convertメソッドバインディング元のプロパティが仮引数valueに、その返り値がバインディング先のプロパティへとデータが流れます。ConvertBackメソッドはその逆です。一方のXAMLは以下のように変更します。

MainWindow.xaml

<!-- xmlns:thisに注意 -->
<Window x:Class="Binding.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:this="clr-namespace:Binding"
        Title="MainWindow" Height="350" Width="525">
    
    <StackPanel>
        <StackPanel.Resources>
            <this:AgeConverter x:Key="AgeConv"/>
        </StackPanel.Resources>

        <TextBox Text="{Binding Age, Mode=TwoWay, Converter={StaticResource AgeConv}}" Name="ageLabel">
        </TextBox>
        <Button Click="Button_Click" Content="Age++"/>
    </StackPanel>
</Window>

リソースにAgeConverterのインスタンスを用意します。そして、Bindingの方でConverterが取得。こうすることでコンバータの実装ができます。




バインディングは記事を探すともっともっといろんなことがあります。そいつらも追々勉強してまとめていきたいとは思っています。(・。・;
疲れた・・・