September 5, 2013

Two-way binding in Windows Forms

Recently I've been involved in a small-scale project using Windows Forms, and I wondered if there's two-way binding implementation in Windows Forms, just kind of like what Knockout.js does in a web application.

Fortunately I do find what I wanted in Windows Forms. Controls such as TextBox, Combobox and others have a property called DataBindings which can help achieve two-way binding. I'm going to give a brief example below.

Here we have a view model, say, EmployeeViewModel, consisting of three properties.
public class EmployeeViewModel
{
    public string FirstName
    {
        get;
        set;
    }

    public string LastName
    {
        get;
        set;
    }

    public string PhoneNumber
    {
        get;
        set;
    }
}
Here's the form. We have three TextBoxes and two Buttons in the form. The three textboxes correspond to FirstName, LastName and PhoneNumber property in the EmployeeViewModel. The "Show me the view model" button will print out the values of the properties of the view model in the output window in Visual Studio, whereas the "Modify the view model" button will update the view model with hard-coded values.


In the form load event, we register the bindings between the Text property of texboxes and the FirstName/LastName/PhoneNumber property of EmployeeViewModel.
private void Form1_Load(object sender, EventArgs e)
{
    this.TextBox_FirstName.DataBindings.Add("Text", this._employeeViewModel, "FirstName");
    this.TextBox_LastName.DataBindings.Add("Text", this._employeeViewModel, "LastName");
    this.TextBox_PhoneNumber.DataBindings.Add("Text", this._employeeViewModel, "PhoneNumber");
}

Here's what the "Show me the view model" button does in the code. You can see that the code tries to print out the values of the properties of the view model in the output window.
private void Button_ShowMeTheViewModel_Click(object sender, EventArgs e)
{
    Console.WriteLine("First Name: " + this._employeeViewModel.FirstName);
    Console.WriteLine("Last Name: " + this._employeeViewModel.LastName);
    Console.WriteLine("Phone Number: " + this._employeeViewModel.PhoneNumber);
}

Compile the application and run it first. When you type in your first name, last name and phone number in those textboxes and click "Show me the view model", you see the magic. What does that mean? That means that you don't even need to grab values from the textboxes using TextBox.Text. You just get the values from the view model and print them out. With the binding, the modification on the textboxes will also apply to the view model.


Until now, I've actually achieved only one-way binding. What's the other "way"? Look at the code in the "Modify the view model" button.
private void Button_ModifyTheViewModel_Click(object sender, EventArgs e)
{
    this._employeeViewModel.FirstName = "Claire";
    this._employeeViewModel.LastName = "Chang";
    this._employeeViewModel.PhoneNumber = "+886928xxxxxx";
}

That simple? I just modify the view model and give it some hard-coded values. Hold on, what kind of result would I expect from just updating the view model? I would hope that when the values of the view model get changed, the values of textboxes will update accordingly. Let's compile and run the application again, then click "Modify the view model".


Oops! Nothing seems happened. Yes, you see nothing changed in the textboxes. In order to notify textboxes of the change in EmployeeViewModel, you will need to implement the INotifyPropertyChanged interface. Here's the code snippet for updated EmployeeViewModel that implements INotifyPropertyChanged.
public class EmployeeViewModel : INotifyPropertyChanged
    {
        #region INotifyPropertyChanged Members

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

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

        private string _firstName;

        public string FirstName
        {
            get
            {
                return this._firstName;
            }
            set
            {
                if (this._firstName != value)
                {
                    this._firstName = value;
                    NotifyPropertyChanged("FirstName");
                }
            }
        }

        private string _lastName;

        public string LastName
        {
            get
            {
                return this._lastName;
            }

            set
            {
                if (this._lastName != value)
                {
                    this._lastName = value;
                    NotifyPropertyChanged("LastName");
                }
            }
        }

        private string _phoneNumber;

        public string PhoneNumber
        {
            get
            {
                return this._phoneNumber;
            }
            set
            {
                if (this._phoneNumber != value)
                {
                    this._phoneNumber = value;
                    NotifyPropertyChanged("PhoneNumber");
                }
            }
        }
    }

Now run the application again then click "Modify the view model". You should see the magic again.


With two-way binding, you focus on your model or view model most of the time. When you update your controls, your model gets modified too and vice versa. However, registering the DataBindings for controls could be error-prone since you will have to hard-code the property name in your code as string. When you refactor the property names in the model in Visual Studio, those hard-coded property names will not be refactored accordingly. What's more, if you have a huge view model, you will to some degree have code duplication because of the INotifyPropertyChanged implementation. With the help of an advanced AOP tool like PostSharp, you can reduce the duplication gracefully. I'm not going to talk about AOP or PostSharp here because it's out of scope of this post.

No comments: