WindowsForm自体にもデータバインディングの機能があるっちゃあるけど,クソザコナメクジちゃんでした.
でも,初めてWindowsFormじゃなくてwpfを使ってみて,データバインディングが使いやすくてコード書くのもめっちゃ簡単になりました!
wpfはレガシーなものになりつつありますが,新しく出てきたmauiちゃんなんかでもxamlの知識は役に立ちますもんね.
wpf以降のwindowsアプリ製作にはxaml理解しなきゃいけないけど,データバインディングさえ理解すれば,xamlは難しくないのだ!
0.概要
データバインディングとは
アプリのUIと、データとの間の紐づけを行ってくれる処理のことです.
簡単な例でいうと,TextBoxに値が入力された時,入力された文字列を,自動的にあらかじめ指定しておいた変数に保存しておくことができます.
つまり,自分でTextBoxのTextプロパティ値をわざわざ取りに行って,変数にプロパティ値を紐づけるするような面倒くさいロジックを書かなくてもよくなります.TextBoxが一つだけならいんですけど,100個とかあると地獄ですもんね.
バインディングソース
データの提供元のオブジェクトのことを指します.
例えば,標準ライブラリ(.net frameworkとか)のクラスオブジェクトですね.
自作のカスタムクラスオブジェクトを使うことももちろんできます.
プログラム上で使用する大抵のデータオブジェクトがこれにあたります..
バインディングターゲット
データ反映先のオブジェクトのことを指します.
要は,データを反映させる先の,UI要素のことを指します.
バインディングの方向
実はデータバインディングには4種類の方向があります.
値 | 説明 |
---|---|
OneWay | 「バインディングソース」から「バインディングターゲット」に対しての単方向バインディング.ソースの値が変更されると,自動的にターゲットの値も再バインディングが行われる. |
OneTime | 「バインディングソース」から「バインディングターゲット」に対しての単方向バインディング.OneWayと異なり,ソースの変更を考慮しないOneTimeは一回限りのバインディングとなる. |
OneWayToSource | 「バインディングターゲット」から「バインディングソース」への単方向バインディング.ターゲットの値が変更されると,ソースに変更が加えられる. |
TwoWay | 「バインディングソース」と「バインディングターゲット」間の双方向バインディング.ソースが変更されるとターゲットに,ターゲットが変更されるとソースに変更が反映される. |
今回は,「バインディングソース」→「バインディングターゲット」の基本的なデータバインディングの説明をしていきます.
簡単なことをする分には,OneWayとOneTimeだけで事足りますね.
ちなみに,残りの二つはこんな時に使えたりします.
- TwoWay:入力フォームを二つ用意しておいて,片方のフォームの入力をもう片方に反映させたい場合
- OneWayToSource:入力検証.ターゲットからソースの入力をチェックしてバリデーションを行いたい場合
(気が向けば,もう少し踏み込んだ内容の記事を書いてみます.)
1.まずは基本的なデータバインディングから
バインド元のテキストボックスの中身をバインド先のテキストボックスの中身に書き換えるサンプルです
<Window x:Class="wpf_databinding_practice.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:binding_sample"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<!-- バインディング元 -->
<TextBlock HorizontalAlignment="Left" Margin="100,77,0,0" Text="バインド元" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<TextBox Name="txtName" Margin="100,100,100,260"/>
<!-- バインディング先 -->
<TextBlock HorizontalAlignment="Left" Margin="100,177,0,0" Text="バインド先" TextWrapping="Wrap" VerticalAlignment="Top" Width="120"/>
<TextBox Name="tbName" Text="{Binding ElementName=txtName,Path=Text}" Margin="100,200,100,160"/>
</Grid>
</Window>
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
namespace binding_sample
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Binding binding = new Binding();
binding.Source = this.txtName;
binding.Path = new PropertyPath("Text");
tbName.SetBinding(TextBox.TextProperty, binding);
}
}
}
マークアップ拡張
xamlにおいてデータバインディングは,マークアップ拡張の機能で作られています.
マークアップ拡張は,要素のプロパティ(設定項目のこと)を柔軟に設定できる機能を指します.
xamlでは主に,「プロパティ属性構文」と「プロパティ要素構文」があります.
両方に対してマークアップ拡張を使うことができます.
- 「プロパティ属性構文」: Windowの「幅属性」Widthとか「高さ」Heightとかの要素プロパティに文字列で値を設定する記法
- 「プロパティ要素構文」:プロパティ値が文字でなくてオブジェクト型の場合など,要素の中に別要素を入れ子にする記法.
プロパティ属性構文のを拡張するときはなみかっこ「{〇〇}」で囲みます.
また,プロパティ要素構文に対して拡張する時は大なり小なり「<〇〇>」で囲みます.
{Binding}マークアップ拡張
バインド先のUIをバインド元のデータソースと紐づけるにはマークアップ拡張機能の一部である,「{Binding}マークアップ拡張」を使います.
{Binding 〇〇,△△,...}
ここで,{Binding}とは,実行時にBindingクラスのインスタンスを生成するための記法です.
このプログラムが実行されて,生まれたインスタンスの〇〇属性とか△△属性とかに値を代入していきます.
Text="{Binding ElementName=txtName,Path=Text}"
バインディングクラスの属性にもいくつか書式があります.今回は最も基本的な例を挙げてみます.
今回は「ElementName属性」と「Path属性」を利用しました.
-
ElementName属性:バインド元の要素の名前
-
Path属性:バインド元の要素におけるプロパティ要素
今回は,TextBoxにtxtNameと名前を付けたので,txtNameをElementName属性にしています.
また,txtName要素はTextBlockなので,TextBlockに入力されたTextプロパティの文字列をバインディングソースに設定しています.
※バインディングソースはあくまで,参照元のデータのことです.今回はバインディングソースへの入力値をリアルタイムでバインディングターゲットに反映させる例となっていますが,普段は適当なフィールドに格納します.
Bindingクラス
xaml上で定義した{Binding}によって,実行時にBindingクラスが生成されます.
実行時にBindingクラス君が働いてくれるおかげで,指定した「バインディングソース」と「バインディングターゲット」を結びつけられます.
Binding binding = new Binding();
実際には,わざわざプログラム側からBindingクラスを呼び出すことは稀ですが,内部の働きとしては以下のようになります.
(ちょっと紛らわしいですが,今節だけは,入力対象のUI要素のことをデータソース,UI要素中のプロパティ要素のことをバインディングソースとします.)
-
「Sourceプロパティ」:データソースオブジェクトを設定
- xamlで「ElementName属性」に指定した要素名の要素をデータソースオブジェクトとしています.
-
「Pathプロパティ」:データソースオブジェクトのプロパティ値を設定
- TextBlockのtxtName要素のTextプロパティがバインディングソースであることの意味の紐づけを行っています.
-
.setBinding()メソッド:バインド先のUI要素にバインド元の情報を紐づけるメソッド
- 第1引数に出力先であるバインド対象のプロパティを設定します.今回は入力された文字列を反映させたいので,バインディングソースと同じTextプロパティを指定します.
- 第2引数に入力元のBindingオブジェクトを設定します.
2.CLRオブジェクトのデータバインディング
標準ライブラリや自作クラス群をまとめてCLRオブジェクトと呼びます.
先ほどの例とは違って,Bindingクラスを直接CSファイルに書き込むわけではなく,自作クラスのフィールドへデータを保存します.
<Window 中略
xmlns:local="clr-namespace:binding_sample"
中略>
<Grid>
<Window.Resources>
<local:book x:Key="book_instance"
Title="猫でもわかるC#"
Author="C# 太郎" />
</Window.Resources>
<StackPanel Orientation="Vertical" Margin="50">
<Label Content="本詳細"/>
<StackPanel Orientation="Horizontal">
<Label Content="タイトル:" VerticalAlignment="Center" />
<TextBox Text="{Binding Source={StaticResource book_instance},Path=Title}" VerticalAlignment="Center"/>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Content="著者名:" VerticalAlignment="Center" />
<TextBox Text="{Binding Source={StaticResource book_instance},Path=Author}" VerticalAlignment="Center"/>
</StackPanel>
</StackPanel>
</Grid>
</Window>
namespace binding_sample
{
class book
{
public book(){}
public string Title { get; set; }
public string Author { get; set; }
}
}
自作クラスのインスタンス生成
1節では特に意識しませんでしたが,バインディングソースを利用するには,クラスのインスタンスを生成されていなければいけません.
一節のように要素が既に定義されているときは,txtName要素が実際のクラスインスタンス君になってくれていました.
もちろん,自作クラスのインスタンスを作成することもできます.
「local:book」によって,今回はbookクラスのインスタンスを生成しています.
< local:book x:Key="book_instance" Title="猫でもわかるC#" Author="C# 太郎" />
自作クラスを使いたい場合は,同一アセンブリ上の名前空間を実装している必要があります.
< xmlns:local="clr-namespace:binding_sample" >
今回は,プロジェクト全体の名前空間が「binding_sample」となっていますので,自作クラス側もbinding_sample名前空間に属させています.
「local:book」の前に何も書かかなくても動作しますが,実際は「xmlns:」を継承しています.
省かずに記述すると以下のようになります.
xmlns:local:book
「x:keyマークアップ拡張」を使うことで,データに名前をつけることができます.
今回は生成したbookクラスのインスタンスに”book_instance”と命名しています.
xmlns: x: key="book_instance"
{StaticResource]マークアップ拡張
staticResource属性を用いることによって,生成を宣言したbookインスタンスを参照することができます.
{StaticResource book_instance}
ちなみに,staticResouce以外にもdynamicResourceなるものもあるようです.
補足:DataContextプロパティ
例では,{Binding}の中に{staticResource}があり,属性の中に属性を埋め込んでいます.
はっきり言って,かっこよくないですね.
<TextBox Text="{Binding Source={StaticResource book_instance},Path=Title}" VerticalAlignment="Center"/>
元々はこんな感じでした.
DataContextプロパティには,バインディングソースオブジェクトを設定することができます.
この記法を用いることでソース属性の説明を省くことができ,もっと簡潔に書けます.
DataContexプロパティtはWindowとかPageとかめっちゃ大元の抽象度の高いクラスで作られてるので,こういうことができるんですよね.
<! -- ☟ 実はこんな感じに書き換えることができちゃうのだ! ☟ -->
<TextBox DataContext="{StaticResource book_instance}" Text="{Binding Path=Author}" VerticalAlignment="Center"/>
さらに,DataContextプロパティは,「親要素のDataContext値を子要素のDataContextに継承させる」機能をもっています.
ちなみにさらにネストを深めても,この継承は引き継がれていきます.
今回のように,StaticResource属性は他の要素が複数ある,というときに重宝します.
<! -- ☟ もっと簡潔にかけちゃうのだ! ☟ -->
<StackPanel DataContext="{StaticResource book_instance}" Orientation="Vertical" Margin="50">
中略
<StackPanel Orientation="Horizontal">
<Label Content="タイトル:" VerticalAlignment="Center" />
<TextBox Text="{Binding Path=Title}" VerticalAlignment="Center"/>
</StackPanel>
中略
</StackPanel>
3.コレクションのデータバインディング
表をこだわって作ってみました.表を作るだけならもっと楽に作れますが,DataTemplateを使いたかったので,冗長なコードになっちゃってます.
2節ではデータバインディングに自作クラスを使いました.今回も自作クラスを使いますが,データをまとめて扱ってみます.
<Window.Resources>
<local:booklist x:Key="booklist_instance" />
</Window.Resources>
<StackPanel DataContext="{StaticResource booklist_instance}" Margin="50">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="30"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Label Grid.Row="0" Grid.Column="0" Content="題名" Background="#FFD4D4D4"/>
<Label Grid.Row="0" Grid.Column="1" Content="著者名" Background="#FFB4B4B4"/>
<ListView Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{Binding Path=books}">
<ListView.ItemTemplate>
<DataTemplate>
<Grid >
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 本の題名 -->
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding Path=Title}" />
<!-- 本の著者名 -->
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Path=Author}"/>
</Grid>
</DataTemplate>
</ListView.ItemTemplate>
</ListView>
</Grid>
</StackPanel>
namespace binding_sample
{
class book
{
public book(){}
public string Title { get; set; }
public string Author { get; set; }
}
}
namespace binding_sample
{
public class booklist
{
public booklist()
{
book book1 = new book();
book1.Title = "猫でもわかるC#";
book1.Author = "C# 太郎";
_books.Add(book1);
book book2 = new book();
book2.Title = "犬でもわかるC#";
book2.Author = "C# 次郎";
_books.Add(book2);
book book3 = new book();
book3.Title = "兎でもわかるC#";
book3.Author = "C# 三郎";
_books.Add(book3);
}
public List<book> books
{
get { return this._books; }
}
private List<book> _books = new List<book>();
}
}
ItemsControlクラス
コレクションデータを表示するためには,コントロール側もコレクションデータ表示用の要素を使います.
具体的にはItemxControlクラスの派生クラスの必要があります.
今回はListView要素を使っていますが,他にもComboBocやTreeViewなどがあります.
<ListView Grid.Row="1" Grid.ColumnSpan="2" ItemsSource="{Binding Path=books}">
さらに,コレクションデータをバインディングするためには,ItemSourceプロパティにコレクションデータを設定します.
ItemsSource="{Binding Path=books}"
Datatemplateの利用
なんで,Datatemplateがあるのかな?
という話ですが,Datatemplate要素を子要素になっていないと,コレクションデータを表現する際に,オブジェクト名をToString()表現された形式になってしまいます.
Datatemplateを使うことによって,バインドしたコレクションデータの各項目の表示方法を変更できます
< DataTemplate >
<○○○○>
DataTemplate >
そもそも,ListViewのListView.Viewを使うときやDataGridを使うときなど.Datatemplateが既に実装おり,わざわざ書く必要がありません.
Datatemplateを使うときは,自由度が高い設計をしようとした時に必要になってくるイメージです.
- ListBoxでリストを作るとき
- DataGridで可変式の要素を使いたいとき
詳しくはItemControlクラスの継承図を辿ることになりそうですね.
さいごに
データバインディングを理解するだけで,できることの幅がぐーんと広がりますねー!