もうコードを書きすぎない!WPFのデータバインディングがWinFormsより楽な理由

.NET

WPFのレイアウトコントロールについて紹介してきました。
次にListBoxやGridコントロールの説明をしたいと思いますが、その前にWPFでは欠かせないデータバインディングについて紹介します。

データバインディングとは

データバインディングとは、「UIのプロパティ」と「データオブジェクトのプロパティ」を紐づける(バインドする)ことです。
これまでのWindows Forms等は、値が変わるとC#のコードで書き換える必要がありました。正確には、Windows Formsにもバインドの概念はありましたが、コードで書き換えるやり方をしているソースのほうが多かったかと思います。
WPFはデータバインディングの仕組みを使うことで、コントロールとオブジェクトを紐づけて相互に値(表示)を更新することが可能です。

早速、データバインディングについてみていきたいと思います。

依存関係プロパティ

データバインディングを理解するために、依存関係プロパティについて説明したいと思います。
依存関係プロパティとは、「WPFの高度な機能(バインディング、アニメーション、スタイルなど)を利用できるように設計された、特殊なプロパティ」のことです。
バインディングのターゲットになれるのは依存関係プロパティと呼ばれるプロパティになります。

  • UIコントロール
    TextBox.Text や Button.Content など、普段バインド先に指定しているものは、すべて内部で「依存関係プロパティ」として定義されています。
  • データソース
    通常のクラス(ViewModelなど)を使用することができます。ただし、プロパティをバインドする場合、値の変更をUIに伝えるには INotifyPropertyChanged インターフェースの実装が必要です。 このインターフェイスについては後程説明します。
項目通常のプロパティ依存関係プロパティ
定義場所一般的なC#のクラスDependencyObjectを継承いしたクラス
バインドのターゲット不可(ソースにはなれる)可能
主な役割データの保持UIの状態管理、アニメーション、継承

「自作のユーザーコントロールに値をバインドさせたい!」 というケースでは、必ずこの依存関係プロパティを自分で実装(DependencyProperty.Register)する必要があります。
依存関係プロパティの実装についてはまた別途サンプルを交えて説明します。

ここでは依存関係プロパティは「標準のプロパティでは手が届かない、UIリッチな表現や効率化が可能になる」と覚えておいてください。
そのうちの一つがデータバインディングであり、XAML上でBindingの記述を行うことができます。これにより、UI要素と背後のデータが自動で同期されるようになります。これこそがデータバインディングの最大のメリットと言えます。

これまでのデータ表示

WPFのデータバインディングの動きを見る前に、これまでのデータ表示のおさらいです。
Windows Formsの場合、コントロールのイベントハンドラで更新するロジックを書くというのが多かったと思います。下記のようなコードです。

        private void textBox1_TextChanged(object sender, EventArgs e)
        {
            _sample.Name = textBox1.Text;
        }

Windows Formsにもバインディングの機能はあり、下記のコードでデータのプロパティとコントロールのプロパティ(表示)をバインドすることができます。

        private void BindingForm_Load(object sender, EventArgs e)
        {
            textBox2.DataBindings.Add("Text", _sample, "Name", false, DataSourceUpdateMode.OnPropertyChanged);
        }

これでsampleのインスタンスのNameプロパティをテキストボックスのTextプロパティに紐づけることができます。さらにINotifyPropertyChangedインターフェイスを実装するとデータソース側の変更をコントロールに通知して双方向のデータバインディングをすることができます。
実際のところ私はWindows FormsでINotifyPropertyChangedインターフェイスまで実装したことはないです。上記のようなバインディングは行いますが、Windows FormsはどちらかというとC#のコード上でUIとデータソースの同期をとるソースのほうが多くなっていると思います。

これから紹介するWPFのように柔軟なデータバインディングができないのと、Windows Formsがイベント駆動の思想で設計されているので、どうしても、C#のソース上で修正してしまうというのが正直なところではないかと思います。

WPFでのデータ表示

WPFでデータを表示する場合は、データバインディングという考え方が重要になってきます。
もちろんこれまでのWindows Formsのようにイベント駆動で考え、コードビハインドで(C#のコードで)UIを更新していくことも可能です。この場合、下記のようなデメリットがあります。

  • データがかわる度にUI更新のロジックが必要
  • コントロールの数だけイベントハンドラが必要
  • UIコントロールの名前を把握しておかなければならず、UIとロジックが分離できない。

逆にデータバインディングを使うと上記のデメリットがなくなり、下記のようなメリットが生まれます。

  • いつどう更新するかより、何をどこにつなぐかという宣言的なUIになる。
  • XAMLの変更がデータのロジック側に影響を与えにくい、デザインとロジックの分離
  • 表示の際に変換や検証などの高度な機能を使用できる。

では、早速実例を挙げながら比べていきたいと思います。

バインディングサンプル ユーザー登録

今回は、ユーザー登録を行う画面を例にバインディングを紹介します。

データ(ユーザー情報)

ユーザーデータとしてクラスを定義します。簡単にID、名称、住所(都道府県)くらいの情報をもつユーザーとします。
SampleUserとして下記のように定義します。

    /// <summary>
    /// ユーザー情報
    /// </summary>
    public class SampleUser
    {
        private string _id = string.Empty;
        /// <summary>
        /// ユーザーID
        /// </summary>
        public string ID
        {
            get { return _id; }
            set { _id = value; }
        }

        private string _name = string.Empty;
        /// <summary>
        /// 名称
        /// </summary>
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        private string _prefecture = string.Empty;
        /// <summary>
        /// 都道府県
        /// </summary>
        public string Prefecture
        {
            get { return _prefecture; }
            set { _prefecture = value; }
        }
    }

このユーザ情報を画面にバインドするにあたり、画面(View)とデータ(Model)の仲介役みたいな役割が存在します。聞いたことがあるかもしれませんが、MVVMパターンと呼ばれ、M(Model)-V(View)-VM(ViewModel)といった3階層に分かれ、仲介役はVM(ViewModel)という層になります。

View-ViewModel-Modelという階層に分かれ、それぞれ記載したような役割を持ちます。
詳細は、今後紹介していくとして、このような役割分担があると覚えておいてください。
今回定義したSampleUserクラスはModelに該当します。このモデルをViewにバインドする仲介やくとして、ViewModelを定義します。

    /// <summary>
    /// BindingSampleウインドウのViewModel
    /// </summary>
    public class BindingSampleWindowViewModel
    {
        private SampleUser _user = new SampleUser();
        /// <summary>
        /// ユーザー情報
        /// </summary>
        public SampleUser User
        {
            get { return _user; }
            set { _user = value; }
        }

        public ICommand RegistrationCommand { get; set; } = new UserRegistrationCommand();
    }

    /// <summary>
    /// ユーザ登録用 コマンド
    /// </summary>
    public class UserRegistrationCommand : ICommand
    {

        public event EventHandler? CanExecuteChanged;

        public bool CanExecute(object? parameter)
        {
            return true;
        }

        public void Execute(object? parameter)
        {
            SampleUser? user = parameter as SampleUser;
            if (user != null)
            {
                MessageBox.Show($"{user}");
            }

        }
    }

ViewがBindingSampleWindowなので、それに対してViewModelをBindingSampleWindowViewModelと定義します。今回はモデルを保持したプロパティと、モデルを登録する用のコマンドを定義したクラスになります。コマンドはこれまでイベントハンドラで処理を記載していたものをICommandを実装したオブジェクトを定義することで、UIとコードビハインドの密な関係をUIに依存しないように処理を分離することができます。これによりUIの変更がコードビハインドに与える影響を抑え、UIとロジックを分離することに役立ちます。

それでは次にバインディングをします。
バインディングは親(Window)のDataContextプロパティを使用します。このプロパティにバインド元のデータ(ViewModel)を指定することで、親はもちろんその子に継承されて、XAMLから参照することができるようになります。

    /// <summary>
    /// ユーザー登録画面
    /// </summary>
    public partial class BindingSampleWindow : Window
    {
        private BindingSampleWindowViewModel _vm = new BindingSampleWindowViewModel();
        public BindingSampleWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            this.DataContext = _vm;
        }
    }

これで、ViewModelを通してサンプルユーザーへアクセスすることができます。
XAML側にバインディングの記述を行います。

<Window x:Class="HelloWpfApp.BindingSampleWindow"
        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:HelloWpfApp"
        mc:Ignorable="d"
        Title= "ユーザー登録" Height="250" Width="400" Loaded="Window_Loaded">
    <Grid>
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
            <RowDefinition Height="50" />
        </Grid.RowDefinitions>

        <Grid Grid.Row="1">
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="120" />
                <ColumnDefinition />
            </Grid.ColumnDefinitions>
            <Grid.RowDefinitions>
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
                <RowDefinition Height="30" />
            </Grid.RowDefinitions>

            <Border Grid.Row="0" Grid.Column="0" Background="DeepSkyBlue" BorderBrush="DarkGray" BorderThickness="1" Margin="1">
                <TextBlock FontSize="20" FontFamily="MS UI Gothic" 
                       Text="ID" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="White" />
            </Border>
            <TextBox Grid.Row="0" Grid.Column="1" Margin="1" VerticalContentAlignment="Center" 
                     FontSize="18" FontFamily="MS UI Gothic" Text="{Binding User.ID}" />

            <Border Grid.Row="1" Grid.Column="0" Background="DeepSkyBlue" BorderBrush="DarkGray" BorderThickness="1" Margin="1">
                <TextBlock FontSize="20" FontFamily="MS UI Gothic" 
                       Text="名称" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="White"/>
            </Border>
            <TextBox Grid.Row="1" Grid.Column="1" Margin="1" VerticalContentAlignment="Center" 
                     FontSize="18" FontFamily="MS UI Gothic" Text="{Binding User.Name}" />
            <Border Grid.Row="2" Grid.Column="0" Background="DeepSkyBlue" BorderBrush="DarkGray" BorderThickness="1" Margin="1">
                <TextBlock FontSize="20" FontFamily="MS UI Gothic" 
                       Text="都道府県" VerticalAlignment="Center" HorizontalAlignment="Center" Foreground="White"/>
            </Border>
            <TextBox Grid.Row="2" Grid.Column="1" Margin="1" VerticalContentAlignment="Center" 
                     FontSize="18" FontFamily="MS UI Gothic" Text="{Binding User.Prefecture}" />

        </Grid>

        <StackPanel Grid.Row="2" HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Horizontal">
            <Button FontSize="15" Content="キャンセル" VerticalAlignment="Bottom" Width="100" Height="28" Margin="0,0,20,0" />
            <Button FontSize="15" Content="登録" VerticalAlignment="Bottom" Width="120" Height="36" 
                    Command="{Binding RegistrationCommand}" CommandParameter="{Binding User}"/>
        </StackPanel>
        
    </Grid>
</Window>

ユーザーの属性である3つのプロパティはテキストボックスのTextプロパティにバインドされています。これにより、プロパティとテキストボックスのTextプロパティが結びつき、プロパティの値がテキストボックスに表示され、テキストボックスの変更がプロパティに反映されます。(プロパティの変更をTextプロパティに反映させるには、もう少し実装が必要になります、次回以降で実装方法を説明します。)

今回バインディングでもう一つ連結させているのは、ボタンのコマンドです。イベント駆動型のWindows Formsではボタンのクリックイベントにイベントハンドラを登録し、コードビハインドにクリック時の動作を記述してきました。もちらん、WPFでも同じように実装することは可能です。データバインディングの仕組みを利用することで、イベントハンドラではなく、ViewModel側に操作内容を記述し、ボタンのコマンドに結び付けることができます。これによりコントロール数分イベントハンドラを登録したり、処理がイベントハンドラに散らばったりすることなくなります。UIとロジックを分離させることで、可読性、保守性を上げることに繋がります。

実際にプログラムを実行します。

ID、名称、都道府県を入力して、登録ボタンを押下します。

バインドしたコマンド(UserRegistrationCommand)が呼び出され、入力した値がバインドした各プロパティの値に設定されていることがわかります。これまでは、テキストボックスのTextプロパティをSampleUserインスタンスのプロパティに設定する処理をコードビハインドにC#で記載していたものが、XAMLでバインドしておくだけで、自動的に設定されます。モデル側が、UIのどこに表示されているかを意識する必要は全くなく、仲介役として入ったViewModelがUIとモデルを結び付けてくれます。今回は、XAMLでデータバインディングを記載しているので、ViewModelにそこまでの仕事はありませんが、データ(SampleUser)と、SampleUserを登録するコマンドを公開して、UIへ結び付けれるようにしています。
どうでしょうか、Windows Formsで記載しているよりもコードがすっきりして、無駄なコードが存在していないことをお分かりいただけたのではないでしょうか。

これが一番単純なバインディングの機能です。この状態だと、たとえば一度すべての値をクリアしたいときに、SampleUserの値を初期状態にしても、UI側は更新されません。次回は、プロパティの値が変わったときに、UI側も更新されるように変更通知を行う機能について説明したいと思います。



コメント

タイトルとURLをコピーしました