C#

【WPF】データ・バインディング(Binding)とは?扱い方を理解する

C#のWPFを検索すると、データバインディングというワードを目にする事が結構あるのではないでしょうか?

WPFでは画面(UI)とロジック(UI以外の処理)を分けて開発する MVVM(Model-View-ViewModel)パターンが推奨されています。このパターンによる開発を実現する機能の1つがデータバインディングです。他にもコマンドという機能があります。

このデータバインディングはWPFで重要な役割を担っているので、アプリケーションを作成する前に理解しておきたい内容の1つです。

この記事はデータ・バインディング(Binding)を理解する手助けとなることでしょう。ぜひ最後まで読んでみてください。

データ・バインディング(Binding)とは?

データ・バインディングとは、MVVM(Model-View-ViewModel)パターンによる開発を実現するための機能の1つで、ViewとViewModelを結びつける役割を持っています。

バインディング(Binding)という単語には、「結びつける」という意味があるように、Viewで変更されたコントロールの値はViewModelへ反映されます。また、ViewModel で変更された値は View へ変更が通知されてViewのコントロールへ反映されます。

もう少し理解を深める為に例として、「ボタンを押すと値が更新されて、その結果を画面に表示する」という場合で考えてみましょう。まずはMVVM パターンを採用しない場合は次のようになります。

この設計方法では、画面とロジックが密結合になってしまいます。例えば、ロジック側の値を変更するメソッドの名前を変更した場合、メソッドの呼び出しをする画面も変更しなければなりません。このような設計方法を用いて開発する WinForms は画面とロジックを一体的に記述しているので、コードビハインドのコード量が肥大化してしまう傾向にあります。

続いて、MVVM パターンを採用する場合は次のようになります。

画面にあるボタンをクリックするとコマンドが実行されて、 中継する ViewModel がロジックにあるメソッドを呼び出します。その後、変更された値を中継する ViewModel へ通知し、ViewModel から画面のラベルへ変更を通知します。このように、ViewModel が画面とロジックを中継することで、画面とロジックを切り離して開発を進めことができます。

データバインディングの使い方

WPFで画面を開発する際、 XAMLというマークアップ言語を用いて開発を行います。

このXAMLを使って、ViewとViewModelを結びつけるためにはいくつかのルールがあります。

ターゲットとなるコントロールのプロパティにデータバインディングを記述する

ViewのDataContextプロパティにデータソース(ViewModel)を設定する

事前準備として、プロジェクトに「ViewModel」フォルダを作成し、フォルダの中に「MainWindowViewModel」クラスを作成します。

ターゲットとなるコントロールのプロパティにバインディングするプロパティをMainWindowViewModelに記述します。

ここではプロパティの名前をInputにします。

namespace Sample.ViewModel
{
    class MainWindowViewModel
    {
        public string Input { get; set; } = "初期値";
    }
}

それでは、それぞれのルールについてみてみましょう。

データバインディングを記述する

MainWindow.xamlを開いて、ターゲットとなるコントロールのプロパティに”{Binding プロパティ名}”を記述します。UIコントロールのプロパティのほとんどがバインディングをサポートしています。

ここでは、TextBoxのTextプロパティにViewModelのInputプロパティをバインディングします。

<Window
    x:Class="Sample.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:local="clr-namespace:Sample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="100"
    mc:Ignorable="d">
    <Grid>
        <TextBox Text="{Binding Input}" />
    </Grid>
</Window>

DataContextを設定する

ViewとViewModelはDataContextプロパティを介して双方向のデータのやり取りをします。

DataContextプロパティにはMainWindowViewModelのデータソース(オブジェクト)を設定します。データバインディングがXAMLで記述されている場合は、DataContext(MainWindowViewModel)プロパティを参照して値がセットされます。

XAMLもしくはコードビハインドのどちらかで設定することができます。参照先となるMainWindowViewModelは名前空間を定義しておきます。

XAMLで設定する場合
xmlns:vm="clr-namespace:Sample.ViewModel"

<Window.DataContext>
    <vm:MainWindowViewModel/>
</Window.DataContext>
コードビハインドで設定する場合
using Sample.ViewModel;

namespace Sample
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();
            DataContext = new MainWindowViewModel();
        }
    }
}

これで最低限の設定は完了しました。

動作確認をする

設定したデータバインディングがどのように作用するのかを見てみましょう。

ViewとViewModelの状態を可視化したいので、画面にTextBlockとButtonを追加し、MainWindowViewModelのInputプロパティをTextBlockのTextプロパティにバインディングします。Buttonがクリックされたら、MainWindowViewModelのInputプロパティに「初期化」を代入する処理を記述します。

<Window
    x:Class="Sample.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:local="clr-namespace:Sample"
    xmlns:vm="clr-namespace:Sample.ViewModel"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="160"
    mc:Ignorable="d">
    <Window.DataContext>
        <vm:MainWindowViewModel />
    </Window.DataContext>
    <Grid>
        <StackPanel Orientation="Vertical">
            <TextBox Margin="10" Text="{Binding Input}" />
            <TextBlock Margin="10" Text="{Binding Input}" />
            <Button Margin="10" Click="Button_Click" Content="プロパティの初期化" />
        </StackPanel>
    </Grid>
</Window>

コードビハインド(MainWindow.xaml.cs)には次のように記述します。

using System.Windows;
using Sample.ViewModel;

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

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            ((MainWindowViewModel)DataContext).Input = "ボタンをクリックしました";
        }
    }
}

ではアプリケーションを動かしてみましょう。

動作確認してみると分かるのですが、2つの改善点があります。

  1. Buttonをクリックしても、TextBoxとTextBlockの文字が「初期化」に更新されない。
  2. TextBoxへ入力した直後に、TextBlockの値が更新されない。

動的変更を通知する

まず①ですが、ViewModelで変更した値がViewへ通知がされていません。

動的変更をViewへ通知するために、プロパティの値に変更があったことを通知するイベントを用意します。WPFではINotifyPropertyChangedインターフェイス(System.ComponentModel名前空間)を使ってイベントの実装を行います。

INotifyPropertyChangedを実装する方法ですが、PropertyChangedイベントを宣言し、OnPropertyChangedメソッドを作成する必要があります。

これをViewModelを作る度に記述するのは手間なので、ViewModelBaseという基底クラスをつくっておくと実装が楽になります。プロジェクトにある「ViewModel」フォルダの中に「ViewModelBase」クラスを作成します。

ViewModelBaseには次のように記述します。

using System.ComponentModel;
using System.Runtime.CompilerServices;

namespace Sample.ViewModel
{
    class ViewModelBase : INotifyPropertyChanged
    {
        // INotifyPropertyChanged を実装するためのイベントハンドラ
        public event PropertyChangedEventHandler PropertyChanged;

        // プロパティ名によって自動的にセットされる
        private void OnPropertyChanged([CallerMemberName] string propertyName = null) =>
            PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
    }
}

次に、MainWindowViewModelにViewModelBaseを継承して、変更を通知する必要のあるプロパティについて、そのプロパティが更新されるたびにOnPropertyChangedを呼び出します。

ViewModelを以下のように修正します。

namespace Sample.ViewModel
{
    internal class MainWindowViewModel : ViewModelBase
    {
        private string input = "初期化";

        // 入力テキスト用のプロパティ
        public string Input
        {
            get { return input; }
            set 
            {
                if (input != value)
                {
                    input = value;
                    // 値をセットした後、画面側でも値が反映されるように通知する
                    OnPropertyChanged();
                } 
            }
        }
    }
}

これで変更が通知されるようになりました。

更新トリガーの設定をする

②ですが、動作確認のGifを確認するとTextBoxからフォーカスから外れた(ボタンをクリックした)タイミングでInputプロパティと同期しているTextBlockが更新されています。

これは、UpdateSourceTriggerのデフォルト値がLostFoucusになっているからです。updateSourceTriggerはバインディングの更新タイミングを決定します。

UpdateSourceTriggerプロパティでは以下の値を設定できます。

説明
LostFocusフォーカスが外れたタイミングでソースの値を更新します。
PropertyChangedプロパティの値が変化したタイミングでソースの値を更新します。
ExplicitUpdateSourceメソッドを呼び出して明示的にソースの更新を指示したときのみソースの値を更新します。

今回はTextBoxへ入力した直後にプロパティを更新したいので、UpdateSourceTriggerプロパティをLostFoucusからPropertyChangedへ変更します。

上記内容をXAMLに反映させると次のようになります。

<Window
    x:Class="Sample.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:local="clr-namespace:Sample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="160"
    mc:Ignorable="d">
    <StackPanel Orientation="Vertical">
        <TextBox Margin="10" Text="{Binding Input, UpdateSourceTrigger=PropertyChanged}" />
        <TextBlock Margin="10" Text="{Binding Input}" />
        <Button Margin="10" Click="Button_Click" Content="プロパティの初期化" />
    </StackPanel>
</Window>

再度動作確認をする

修正したプログラムを動作させてみましょう。

変更前後の違いが分かるように、はじめと同じ動作をさせてみました。

上図をみて頂ければ、全く異なる動作をしている事が分かると思います。

TextBoxに入力した値がすぐさまTextBlockへ反映されています。また、Buttonをクリックした時にTextBoxとTextBlockの値が「初期化」になっています。

これでデータバインディングの最低限の設定が完了しました。

DataContext以外のBindingの使い方

Bindingは、DataContentプロパティによるデータソースの指定以外に、いくつかの使い方があります。

ElementNameを指定して参照する方法

ElementNameは、UIコントロール間で直接プロパティの値の同期を取りたい時に使用します。コントロールのName(x:Name)プロパティで指定された名前をElementNameに指定します。

使い方の例として、Slider(スライダー)のValueプロパティの値をTextBlockのTextプロパティに表示します。ElementNameにSliderのNameを指定して、同期させるプロパティ名をPathに指定します。

<Window
    x:Class="Sample.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:local="clr-namespace:Sample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="100"
    mc:Ignorable="d">
    <Grid>
        <Slider Name="sliderName" Margin="10,10,10,40" Value="0" />
        <TextBlock Margin="10,40,10,10" Text="{Binding ElementName=sliderName, Path=Value}" />
    </Grid>
</Window>

上記のXAMLは次のような動作になります。

スライダーを動かすとTextBlockの値が更新されます。

RelativeSourceを指定して参照する方法

RelativeSourceは、UIコントロールのプロパティ値やUIコントロールの親となる要素まで遡ってプロパティ値を取得したい場合に使います。

UIコントロールのプロパティ値を取得したい場合は、プロパティ名を指定し、RelativeSourceのModeプロパティを「Self」にします。

<Window
    x:Class="Sample.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:local="clr-namespace:Sample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="80"
    mc:Ignorable="d">
    <Grid>
        <TextBox Margin="10,10,10,10"
                 Text="{Binding Margin, RelativeSource={RelativeSource Mode=Self}}" />
    </Grid>
</Window>

上記のXAMLは次のような動作になります。

Marginの設定値が表示されます。

今度はUIコントロールの親となる要素まで遡ってプロパティ値を取得します。

この場合、AncestorTypeプロパティに親となる要素の情報を記入します。ここではWindowのTitleを取得しています。

<Window
    x:Class="Sample.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:local="clr-namespace:Sample"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    Title="MainWindow"
    Width="300"
    Height="80"
    mc:Ignorable="d">
    <Grid>
        <TextBox Margin="10,10,10,10"
                 Text="{Binding Title, RelativeSource={RelativeSource AncestorType={x:Type Window}}}" />
    </Grid>
</Window>

上記のXAMLは次のような動作になります。

Windowのタイトルである「MainWindow」がTextBoxに表示されました。

BindingのMode設定

Bindingには、データバインディングの向きを指定する為のModeプロパティというのがあります。

モード説明
OneWayソースからターゲットへの一方通行の同期になります。
TwoWayソースとターゲットの双方向の同期になります。
OneWayToSourceターゲットからソースへの一方通行の同期になります。
OneTimeソースからターゲットへ初回の一度だけ同期されます。

双方向の同期が必要なコントロール(TextBox等)は、TwoWayを指定しておく必要があります。

まとめ

この記事では、WPFのデータバインディングについてサンプルを交えながら使い方を紹介しました。

ターゲットとなるコントロールのプロパティにデータバインディングを記述する

ViewのDataContextプロパティにデータソース(ViewModel)を設定する

INotifyPropertyChangedを継承して変更通知のイベントを発生させる

データバインディングという仕組みは、WPFで推奨されているMVVMパターンで設計開発を行う上で必要不可欠なものです。ぜひ扱えるようになりましょう。

他にもコマンドという仕組みがあり、この内容については以下の記事を参照してみて下さい。

【WPF】コマンドの使い方(ICommandインターフェース)WPFでコマンドを使う方法を解説しています。ICommandインターフェースを継承したDelegateCommandクラスを作成してコマンドを使いやすくしています。このクラスを使った使用例を紹介していますので、これからWPFでアプリを開発する方は是非参考にしてみて下さい。...

以上、最後まで読んでいただきありがとうございました。

プログラミングを学習したいなら…

プログラミングスキルを身に付けるなら、プログラミングを効率良く学べる
プログラミングスクール」がオススメです。

特にこんな方にオススメ!!
これからエンジニアを目指したい
プログラミングの専門性を高めたい
プログラミングを学んで副業をしたい
エンジニアに転職して年収をアップさせたい

プログラミングを触ったことがない未経験からでも、プログラミングスクールで学習すれば、エンジニアへ就職・転職することも可能です。

あなたの「行動力」と「やる気」で、あなたの人生を大きく変えるチャンスになることでしょう。

プログラミングスクールに興味がある方は是非チェックしてみてください。

> プログラミングを学ぶ <

COMMENT

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

CAPTCHA