C#

【C#】ViewとViewModelを紐づける「データバインド」とは?

本記事は前回の続きとなっているため、初見の方は以下の記事を参照ください。

データバインドしない場合

データバインドの説明をする前に、データバインドをしない場合のコードを一緒に見ていきましょう。
それぞれFormのテキストボックスや、ラベルにViewModelより取得した値を設定します。

using System;
using System.Windows.Forms;

namespace FormTestSample
{
    public partial class Form1View : Form
    {
        private  Form1ViewModel _viewModel = new Form1ViewModel();
        public Form1View()
        {
            InitializeComponent();
        }

        private void CalculationButton_Click(object sender, EventArgs e)
        {
            _viewModel.InputTextBox1Text = InputTextBox1.Text;
            _viewModel.InputTextBox2Text = InputTextBox2.Text;

            _viewModel.multiplication();

            this.AnsLabel.Text = _viewModel.AnsLabelText;
        }
    }
}

実行すると…

一応期待通りに動作することが分かります。
しかし、この実装方法は非常にめんどくさいし、バグが入り込みやすいです。

ボタンを押すたびに、ViewModelの値を画面のコントロールと同じ値にしてmultiplicationの結果を画面のコントロールと同じ状態にしています。
かなりイケてない作りになってしまっています。

これを解消する方法が「データバインド」です。

データバインド

画面のInputTextBox1とViewModelのInputTextBox1Textを常に同期している状態をつくることが「データバインド」です。
それでは実装をしてみましょう。

using System;
using System.Windows.Forms;

namespace FormTestSample
{
    public partial class Form1View : Form
    {
        private  Form1ViewModel _viewModel = new Form1ViewModel();
        public Form1View()
        {
            InitializeComponent();

            InputTextBox1.DataBindings.Add("Text", _viewModel, "InputTextBox1Text");
            InputTextBox2.DataBindings.Add("Text", _viewModel, "InputTextBox2Text");
            AnsLabel.DataBindings.Add("Text", _viewModel, "AnsLabelText");
        }

        private void CalculationButton_Click(object sender, EventArgs e)
        {
            _viewModel.multiplication();
        }
    }
}

Form1Viewのコンストラクタにデータバインドのロジックを追加します。
DataBindings.Add([Formの値プロパティ], [ViewModelのインスタンス], [ViewModelのプロパティ名])より紐づけができます。

実行すると…

あれ?結果がでないんだけど…
実は、もう2step手順があります。。。

データバインドするためにはもう2つ仕掛けが必要です。

その1.IPropertyChanged

ViewModelにプロパティ変更時の処理を追加します。

using System;
using System.ComponentModel;

namespace FormTestSample
{

    public class Form1ViewModel :INotifyPropertyChanged
    {
        public string InputTextBox1Text { get; set; } = string.Empty;
        public string InputTextBox2Text { get; set; } = string.Empty;
        public string AnsLabelText { get; set; } = string.Empty;

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string propertyName) 
        {
            if(PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }

        }

        public void multiplication()
        {
            int left = Convert.ToInt32(InputTextBox1Text);
            int right = Convert.ToInt32(InputTextBox2Text);
            AnsLabelText = (left * right).ToString();
        }
    }
}

このように書くと、「OnPropertyChanged」が呼ばれたタイミングでPropertyChangedが発火して、テキストボックスやラベルの値と

public string InputTextBox1Text { get; set; } = string.Empty;
public string InputTextBox2Text { get; set; } = string.Empty;
public string AnsLabelText { get; set; } = string.Empty;

これらViewModelの内容と同期されます。
しかし、これだけ書いても「OnPropertyChanged」が呼ばれていないので何も起きません。

その2.OnPropertyChanged

OnPropertyChangedを呼ぶところですが、メソッドの中など処理の「動き」がある個所には、漏れや読みにくいソースとなるため書かないほうが良いです。
基本的にはプロパティに追加します。

このような場合にgettersetterを活用します。
値の取得時(getter)には初期値(string.Empty)を、値の設定時(setter)には画面で入力された値(value)を設定します。

private string _inputTextBox1Text = string.Empty;
public string InputTextBox1Text {
    get { return _inputTextBox1Text; }
    set 
    {
        if (_inputTextBox1Text == value) 
        {
            return;
        }
        _inputTextBox1Text = value;
        OnPropertyChanged("InputTextBox1Text");
    }
}

if (_inputTextBox1Text == value) としている理由は、値が変更されない場合も想定されるので、効率を考慮し、値が変わらない場合は変更しないようにしました。
このようにすることで、値に変更があれば動的にプロパティ変更されるようになりました。

他のプロパティも同様に修正しましょう。

using System;
using System.ComponentModel;

namespace FormTestSample
{

    public class Form1ViewModel :INotifyPropertyChanged
    {
        private string _inputTextBox1Text = string.Empty;
        public string InputTextBox1Text {
            get { return _inputTextBox1Text; }
            set 
            {
                if (_inputTextBox1Text == value) 
                {
                    return;
                }
                _inputTextBox1Text = value;
                OnPropertyChanged("InputTextBox1Text");
            }
        }

        private string _inputTextBox2Text = string.Empty;
        public string InputTextBox2Text
        {
            get { return _inputTextBox2Text; }
            set
            {
                if (_inputTextBox2Text == value)
                {
                    return;
                }
                _inputTextBox2Text = value;
                OnPropertyChanged("InputTextBox2Text");
            }
        }

        private string _ansLabelText = string.Empty;
        public string AnsLabelText
        {
            get { return _ansLabelText; }
            set
            {
                if (InputTextBox2Text == value)
                {
                    return;
                }
                _ansLabelText = value;
                OnPropertyChanged("AnsLabelText");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        public void OnPropertyChanged(string propertyName) 
        {
            if(PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }

        }

        public void multiplication()
        {
            int left = Convert.ToInt32(InputTextBox1Text);
            int right = Convert.ToInt32(InputTextBox2Text);
            AnsLabelText = (left * right).ToString();
        }
    }
}


正しく同期できていることが分かりました。

まとめ

今回は、ViewとViewModelを同期する「データバインド」について説明しました。
これはWindowsフォームアプリケーション開発では当たり前に使われる方法です。

データバインドするためには、IPropertyChangedのインタフェースを忘れないことと、getter, setterを活用することを忘れないでください。