Introduction
Property binding is one of the slick things that .NET supports. It allows you to easily bind a property of a control to a property of a container class, which in turn helps to separate the UI implementation from the business logic. However, to properly utilize data binding you need to be aware of what .NET expects in your container and you need to be aware of the limitations of .NET data binding. In the end, you may throw out .NET's implementation completely!
This article will discuss those issues with regards to simple property binding. By simple, I mean a 1::1 relationship between the container property and the control property. This article will not discuss more complicated binding involving the CurrencyManager
, data sets, etc. Simple property data binding turns out to be complicated enough!
First, a simple example
As the above screenshot illustrates, we're going to create a form with a first and last name TextBox
control, and we're going to expect that the full name TextBox
control will be properly updated when we edit the text. There are a couple of buttons that perform the following functions:
- updating the
TextBox
's Text
property directly,
- updating the container's property.
First, let's look at the code that sets this up (no, there's no MyXaml here, folks!).
The form
The form is defined straightforward enough, including the event handler that updates the control's text. I've made a few controls accessible outside of the form, as we'll use them later on.
public class MyForm : Form
{
protected TextBox tbFirstName;
protected TextBox tbLastName;
protected TextBox tbCompositeName;
protected Button btnChangeContainer;
protected Button btnChangeContainer2;
public TextBox FirstName
{
get {return tbFirstName;}
}
public TextBox LastName
{
get {return tbLastName;}
}
public TextBox CompositeName
{
get {return tbCompositeName;}
}
public Button ChangeContainer
{
get {return btnChangeContainer;}
}
public Button ChangeContainer2
{
get {return btnChangeContainer2;}
}
public MyForm()
{
** snip -- all sorts of boring initialization stuff **
btnChangeControls.Click+=
new EventHandler(OnChangeControls);
Controls.Add(lbl1);
Controls.Add(lbl2);
Controls.Add(tbFirstName);
Controls.Add(tbLastName);
Controls.Add(tbCompositeName);
Controls.Add(btnChangeControls);
Controls.Add(btnChangeContainer);
Controls.Add(btnChangeContainer2);
}
private void OnChangeControls(object sender, EventArgs e)
{
tbFirstName.Text="Marc";
tbLastName.Text="Clifton";
}
}
The container
The container contains fields for first, last, and full name and property getter/setters for each, plus it implements a ChangeContainer
method that sets the first and last name to my girlfriend, "Karen" and "Linder":
public class MyContainer
{
protected string firstName;
protected string lastName;
protected string fullName;
public string FirstName
{
get {return firstName;}
set
{
if (firstName != value)
{
firstName=value;
FullName=firstName + " " + lastName;
}
}
}
public string LastName
{
get {return lastName;}
set
{
if (lastName != value)
{
lastName=value;
FullName=firstName + " " + lastName;
}
}
}
public string FullName
{
get {return fullName;}
set
{
if (fullName != value)
{
fullName=value;
}
}
}
public void ChangeContainer(object sender, EventArgs e)
{
FirstName="Karen";
LastName="Linder";
}
}
Essentially, that's the usual implementation for getters and setters. Note that the FullName
property is set if you change the FirstName
or LastName
. Ideally, this is one of those cases where you'd like a protected setter but a public getter. Alas, such a thing is not possible. And whether FullName
should even have a setter rather than be computed on the fly depends on what you're doing. However, for the purpose of this article, we'll give FullName
a setter and be done with it.
Initializing the application
Initialization is straightforward:
public static void Main()
{
MyForm form=new MyForm();
MyContainer container=new MyContainer();
container.FirstName="Joe";
container.LastName="Smith";
form.FirstName.DataBindings.Add("Text",
container, "FirstName");
form.LastName.DataBindings.Add("Text",
container, "LastName");
form.CompositeName.DataBindings.Add("Text",
container, "FullName");
form.ChangeContainer.Click+=
new EventHandler(container.ChangeContainer);
Application.Run(form);
}
And we're all set to see what happens.
And the survey says...
On initialization, the form looks great:
If we type in a new first name and hit the tab key, the full name updates correctly:
Some problems
If you click on Change Controls, we notice the first problem - the full name didn't change.
It gets worse. If I click on the first name textbox, then tab to the second one, I get a real mess:
The control has updated the container for the first name but the last name control pulled the old value out of the container so now I have chaos.
And the final problem - if I update the container, the edit controls don't do anything:
Solving one of these problems
The first thing to recognize is that the data binding is essentially one way - we can update the container when we change the control, but if we change the container, the control isn't being updated (except that, during initialization, the container value is read from). So, let's get it working so that we can update the control when we make changes to the container.
In order to do this, we have to give the .NET data binding mechanism a tie-in. .NET uses some name munging to see if an event has been declared that it can use to tell if the value has changed. Essentially, it is the property name appended with "Changed". So let's do that. In the container, I'm going to add the following:
public event EventHandler FirstNameChanged;
public event EventHandler LastNameChanged;
public event EventHandler FullNameChanged;
And, following the .NET convention, I will add Onxxx protected methods that fire those events accordingly.
protected void OnFirstNameChanged(EventArgs e)
{
if (FirstNameChanged != null)
{
FirstNameChanged(this, e);
}
}
protected void OnLastNameChanged(EventArgs e)
{
if (LastNameChanged != null)
{
LastNameChanged(this, e);
}
}
protected void OnFullNameChanged(EventArgs e)
{
if (FullNameChanged != null)
{
FullNameChanged(this, e);
}
}
The Onxxx method is going to be invoked in the property setter. I'm going to use conditional compilation here so that you can easily switch between the mono-directional and the bi-directional implementations:
public string FirstName
{
get {return firstName;}
set
{
if (firstName != value)
{
firstName=value;
#if Bidirectional
OnFirstNameChanged(EventArgs.Empty);
#endif
FullName=firstName + " " + lastName;
}
}
}
public string LastName
{
get {return lastName;}
set
{
if (lastName != value)
{
lastName=value;
#if Bidirectional
OnLastNameChanged(EventArgs.Empty);
#endif
FullName=firstName + " " + lastName;
}
}
}
public string FullName
{
get {return fullName;}
set
{
if (fullName != value)
{
fullName=value;
#if Bidirectional
OnFullNameChanged(EventArgs.Empty);
#endif
}
}
}
Now when I change the values in the container, lo-and-behold, the controls update!
This really is an excellent example of why you should implement proper property setters!
However, I still have a problem. If I click on Change Controls (which, if you recall, changes the TextBox
's Text
property value), it is clear that the container is not being updated:
Why is this? It's because the data binding mechanism doesn't fire until the control loses focus, for example by tabbing off of it:
Now granted, it isn't necessarily "correct" for the control value to be updated programmatically. The correct way would be to work through the business or data object, in which case our bidirectional solution would work. This is a good case for why one uses an MVC pattern, for example. However, the point is, if you change the control programmatically, you would really expect that the container should update as well. Or, you would at least like to have the option for that to happen. Now, the UI probably doesn't (and shouldn't) have the container instance. After all, that's what data binding is for - a separation of concerns. So to accomplish the functionality we really desire, we need to throw out .NET's data binding.
Our better binding
Our better binding mechanism is going to expect xxxChanged events just like .NET, but it's not going to be triggered on losing focus. To begin with, we'll change how we do the binding during application initialization, using our own binder class:
BindHelper.Bind(form.FirstName, "Text", container,
"FirstName");
BindHelper.Bind(form.LastName, "Text", container,
"LastName");
BindHelper.Bind(form.CompositeName, "Text", container,
"FullName");
And the implementation uses a bit of reflection to find the event handlers and wire them up to a couple of generic handlers:
public class BindHelper
{
protected object src;
protected string srcProp;
protected object dest;
protected string destProp;
protected PropertyInfo srcPropInfo;
protected PropertyInfo destPropInfo;
public BindHelper(object src,
string srcProp, object dest, string destProp)
{
this.src=src;
this.srcProp=srcProp;
this.dest=dest;
this.destProp=destProp;
Type t1=src.GetType();
Type t2=dest.GetType();
srcPropInfo=t1.GetProperty(srcProp);
destPropInfo=t2.GetProperty(destProp);
}
public static void Bind(object obj1, string prop1,
object obj2, string prop2)
{
string event1=prop1+"Changed";
string event2=prop2+"Changed";
Type t1=obj1.GetType();
Type t2=obj2.GetType();
EventInfo ei1=t1.GetEvent(event1);
EventInfo ei2=t2.GetEvent(event2);
BindHelper bh=new BindHelper(obj1, prop1, obj2, prop2);
ei1.AddEventHandler(obj1,
new EventHandler(bh.SourceChanged));
ei2.AddEventHandler(obj2,
new EventHandler(bh.DestinationChanged));
bh.DestinationChanged(bh, EventArgs.Empty);
}
private void SourceChanged(object sender, EventArgs e)
{
object val=srcPropInfo.GetValue(src, null);
destPropInfo.SetValue(dest, val, null);
}
private void DestinationChanged(object sender,
EventArgs e)
{
object val=destPropInfo.GetValue(dest, null);
srcPropInfo.SetValue(src, val, null);
}
}
There's no exception handling or checking to see if the event handler exists. There certainly should be, but I want to keep the code pure. However, the comment illustrates an important problem. In .NET 1.1, if the event args is a class derived from EventArgs
, reflection will throw an assertion when you attempt to wire up an event handler that specifies the EventArgs
base class. It shouldn't, because this is a perfectly valid call, and in fact, in .NET 2.0, it is my understanding that this has been corrected.
Notice there's a small "trick" here - the method we're calling is static
, but it instantiates a BindHelper
class, and the events are wired up to that specific instance. Thus, the event handlers have the instance specific information they need to perform the "source = destination" or "destination = source" assignments.
So, let's see what happens. When I click on Change Controls, now the container is being updated!
But now I've created an interesting side affect. The Text
property of the control is updated every time I type a letter, so now my data binding fires while I'm editing the control!
Even better binding
This may, occasionally, be a desired behavior (for example, typing in a filename immediately updates the full text path displayed below it), or it may not be desirable. If it's not desirable, we need to simulate what the .NET data binding does, by tracking the control focus through the Leave
event. First, in the Bind static method, we check if the source is a Control
. I'm using a conditional flag here so you can swap this code in and out as well:
#if ControlBindingEmulation
if (obj1 is Control)
{
((Control)obj1).Leave+=new EventHandler(bh.OnLeave);
}
#endif
And the handler implementation is:
private void OnLeave(object sender, EventArgs e)
{
object val=srcPropInfo.GetValue(src, null);
destPropInfo.SetValue(dest, val, null);
}
And lastly, the SourceChanged
handler needs to check if the source is a Control
, and if so, is it focused. If those conditions are true, then it does not update the destination:
private void SourceChanged(object sender, EventArgs e)
{
#if ControlBindingEmulation
if (src is Control)
{
if (((Control)src).Focused)
{
return;
}
}
#endif
object val=srcPropInfo.GetValue(src, null);
destPropInfo.SetValue(dest, val, null);
}
Container - Container binding
Did you notice in our better binder, that there isn't anything that is specific to binding controls? We can, in fact, bind between container classes now, assuming that the containers implement the appropriately named events in their property setters. To illustrate this, I have a third button that, when activated through another conditional compilation flag, will illustrate how a second container is bi-directionally bound to the first container, and how changing the first and last name in the second container not only updates the first container but also the TextBox
controls that the first container is bound to. This is a very useful technique - instead of one container needing an instance of another container, you can use container-container binding, which significantly reduces the entanglement of your code (for a performance price, of course!).
The second container is instantiated and bound to the first container:
container2=new MyContainer();
BindHelper.Bind(container2,
"FirstName", container, "FirstName");
BindHelper.Bind(container2,
"LastName", container, "LastName");
BindHelper.Bind(container2,
"FullName", container, "FullName");
When the Change Container 2 button is clicked, the following event is fired:
private static void OnChangeContainer2(object sender,
EventArgs e)
{
container2.FirstName="Bob";
container2.LastName="The Great";
}
As you can see, it has updated the user interface, and you can verify that the first container has been updated as well using the debugger:
We now have truly superior data binding capabilities!
An exercise for the reader
If updating the container value updates the Text
property of the control, and updating the Text
property of the control updates the value of the container, why doesn't the application crash with a stack overflow?
Conclusion
Hopefully this article has illustrated some of the complexities regarding simple property data binding. If you use this code, please make sure to add error checking and exception handlers! You will probably also want to be able to specify whether you want real-time binding or "on lost focus" binding, rather than using the conditional compilation flags I've used for demo purposes.