Introduction
The documentation for Windows Forms data binding is pretty sparse. How does it work, exactly? How much can you do with it? There are a lot of people who know how to use data binding, but probably few who really understand it. I had a hard time merely figuring out how to use it, so I began to investigate it in the hopes of understanding this mysterious beast.
I believe "data binding" has traditionally referred to automatic synchronization between controls and database rows or tables. In the .NET Framework (and Compact Framework 2.0), you can still do that, but the concept has been extended to other scenarios, to the extent that you can bind almost any property of any control to almost any object.
System.Windows.Forms.BindingSource
is new in the .NET Framework 2.0. I have the impression Microsoft wants us to use BindingSource
instead of older classes such as CurrencyManager
and BindingContext
, so this article will only help you to use BindingSource
.
Data binding can use Reflection, so you're not limited to database tables and rows in ADO.NET DataSet
s; rather, almost any object that has properties will work. For example, data binding would be useful for implementing an "Options" dialog box if your options are held in an ordinary .NET object.
This is not a tutorial for ADO.NET/DataSet
s or DataGridView
, but see Related Articles.
Note: In this article, I assume you are proficient in C# (and maybe ADO.NET), but you know nothing about data binding.
Disclaimer: I talk a little bit about .NET Compact Framework support for data binding, but I don't know whether everything described here is possible on that platform.
Contents
Introduction to data binding APIs
Note the following:
- The
Control.DataBindings
collection holds Binding
objects, each of which has a DataSource
property of type Object
. - The
DataSource
property of ListBox
, DataGridView
etc. is of type Object
. - The
BindingSource
class also has a DataSource
of type Object
.
So, what do these objects have to be? I find the documentation on this subject to be pretty confusing, which is why tutorials like this get written. In the real world, you will probably use a BindingSource
object as the the DataSource
property of list controls and of Binding
s. If you use databases, the DataSource
property of your BindingSource
is usually a DataSet
; otherwise, it may be an object of a class in your own application.
There seem to be a variety of different ways to do data binding, but I couldn't find them spelled out anywhere. So, I did a little experimentation to learn several of the things you can do.
Let's start with the typical case: you assign a BindingSource
object as the DataSource
of a control. You can think of BindingSource
as a "2-in-1" data source. It has:
- a single object named the
Current
object. A property of a Control
can be bound to a property of Current
. - a
List
object that implements IList
. The list should contain objects that have the same type as the Current
object. List
is a read-only property that either returns a BindingSource
's "internal list" (if the DataMember
string is not set), or an "external list" used if DataMember
is set. Current
is always a member of List
(or null
). When you set DataSource
to a single (non-list) object, your List
only contains that one item.
The way data binding works differs among different kinds of controls.
ComboBox
and ListBox
bind to the List
through their DataSource
and DisplayMember
properties. You normally set the DataSource
to a BindingSource
, and you set DisplayMember
to the name of one of the attributes of the Current
object.DataGrid
and DataGridView
bind to the List
through their DataSource
property. DataGrid
and DataGridView
do not have a DisplayMember
property because they can display several properties from the data source (one in each column), not just one. DataGridView
has an additional property called DataMember
, which seems to be analogous to the DataMember
property of BindingSource
. You would not normally set your grid's DataMember
to anything unless its DataSource
is not a BindingSource
(if you use a BindingSource
, you would set BindingSource.DataMember
instead.)- "Simple" controls such as
TextBox
, Button
, CheckBox
, etc., bind to individual properties of the Current
object through the Control.DataBindings
collection. Actually, even list controls have a DataBindings
collection, but it is not used as much. The DataBindings
list can be changed in the designer under the "(Advanced)" line under the "(Data Bindings)" node.
Tip: In this tutorial, we will often add a binding to the "Text
" property of TextBox
es. Other common bindings you may add to DataBindings
include:
- The "
Checked
" property of CheckBox
and RadioButton
. - The "
SelectedIndex
" property of ComboBox
, ListBox
, or ListView
. - The "
SelectedValue
" property of ComboBox
or ListBox
. - The "
Enable
" or "Visible
" property of any control. - The "
Text
" property of various controls.
Tip: On the desktop, Microsoft encourages you to use DataGridView
, which is a more powerful "upgrade" of DataGrid
. Only DataGrid
is available in the .NET Compact Framework, however.
Tip: The contents of ListView
and TreeView
cannot be data-bound (only simple properties such as "SelectedIndex
" and "Enabled
" can be bound). Some articles on CodeProject propose ways to overcome this limitation, however.
An airplane with passengers
Most examples here are given using an object-based data source, because maybe I'm prejudiced against databases. Suppose you have a class with some data in it:
class Airplane
{
public Airplane(string model, int fuelKg)
{
ID = ++lastID; Model = model; _fuelKg = fuelKg;
}
private static int lastID = 0;
public int ID;
private int _fuelKg;
public int GetFuelLeftKg() { return _fuelKg; }
public string Model;
public List<Passenger> Passengers = new List<Passenger>();
}
class Passenger
{
public Passenger(string name)
{
ID = ++lastID; Name = name;
}
private static int lastID = 0;
public int ID;
public string Name;
}
Well, sorry, but the above classes won't work. There's nothing to bind to here, because the data has to be provided in the form of properties, not methods or fields. And, I'm guessing they have to be public non-static properties, though I haven't checked. So, let's try again:
class Airplane
{
public Airplane(string model, int fuelKg)
{
_id = ++lastID; Model = model; _fuelKg = fuelKg;
}
private static int lastID = 0;
public int _id;
public int ID { get { return _id; } }
public int _fuelKg;
public int FuelLeftKg { get { return _fuelKg; } set { _fuelKg = value; } }
public string _model;
public string Model { get { return _model; } set { _model = value; } }
public List<Passenger> _passengers = new List<Passenger>();
public List<Passenger> Passengers { get { return _passengers; } }
}
class Passenger
{
public Passenger(string name)
{
_id = ++lastID; Name = name;
}
private static int lastID = 0;
public int _id;
public int ID { get { return _id; } }
public string _name;
public string Name { get { return _name; } set { _name = value; } }
}
That's better. Suppose you put this in your project, and you want to have a DataGridView
that shows a list of Airplane
s. And, you want a TextBox
where the user can change the Model
name of the currently selected Airplane
. How?
The designer approach
It is typical to set up data bindings in the Visual Studio designer. If you want to follow along, create a new Windows Forms project, and create a C# code file with the above Airplane
and Passenger
classes in it. Then, in the designer, put a DataGridView
(named "grid
") and a TextBox
(named "txtModel
") on your Form1
. If you select your DataGridView
, there's a little tiny arrow in the top-right corner that you can click to find a configuration box. From there, you can find the "Data Source Configuration Wizard" (to reach it, click "Choose Data Source", "Add Project Data Source").
Note: the wizard won't see Airplane
and Passenger
until the project is built.
You can tell this wizard to get the data from an "Object
", and on the second page, you can select Airplane
from a tree. The wizard will create a BindingSource
in the component tray and set its DataSource
to typeof(Airplane)
:
The wizard also created three columns (DataGridViewTextBoxColumn
objects) for three of the properties of Airplane
. There is no column for the passengers list, however; I guess the wizard knows you can't show a list of complex objects within a single cell. (You can show a dropdown list of strings within a cell, but I don't discuss that in this article.)
In the properties of the TextBox
, open up the "(Data Bindings)" node, click the "(Advanced)" line, then click "...". Choose the Text
property, and then open up the "Binding" list where you can select the Model
property of airplaneBindingSource
:
Then, click OK. Now, all you have to do is add some airplanes to the list. This is done by adding Airplane
s to the BindingSource
. So, make a Form1_Load()
handler (double-click an empty space on the form), and add some code like:
private void Form1_Load(object sender, EventArgs e)
{
airplaneBindingSource.Add(new Airplane("Boeing 747", 800));
airplaneBindingSource.Add(new Airplane("Airbus A380", 1023));
airplaneBindingSource.Add(new Airplane("Cessna 162", 67));
}
Run the program, and here's what you get:
Tip: Notice that the ID field is read-only, so the DataGridView
automatically prevents the user from changing it. Other controls are not so smart. For example, if the Model
were read-only, the user would still be allowed to change txtModel
. To prevent this, you would have to (manually) set txtModel.ReadOnly = true
.
All is well, but personally, I like to set up the binding in the code. So humor me. Let's start over. In the designer, delete airplaneBindingSource
, and the controls will suddenly be unbound again.
The manual approach
Now, change your Form1_Load()
to look like this (differences are marked with //**):
BindingSource bs = new BindingSource();
private void Form1_Load(object sender, EventArgs e)
{
bs.DataSource = typeof(Airplane);
bs.Add(new Airplane("Boeing 747", 800));
bs.Add(new Airplane("Airbus A380", 1023));
bs.Add(new Airplane("Cessna 162", 67));
grid.DataSource = bs;
grid.AutoGenerateColumns = true;
txtModel.DataBindings.Add("Text", bs, "Model");
}
Note that we have to define our own BindingSource
because we deleted the one in the designer. Anyway, run the program, and you should get the same thing as before:
Again, the DataGridView
knows it can't create a column for Airplane.Passengers
.
This was easier than using the wizards, wasn't it? With only five more lines of code, you no longer need to set it up in the designer. On the other hand, the designer makes it easier to customize the columns.
An interesting thing I've noticed about data binding is that it's fairly forgiving about the order in which you do things. You can put the above statements in Form1_Load()
in any order, and the program will still work perfectly. Another interesting thing is that we don't have to tell BindingSource
what kind of objects it will hold: you can take out the assignment to DataSource
(and the DataSource
will stay equal to null
), but internally, the BindingSource
will still record the fact that the first object was an Airplane
. If you then add a non-Airplane
to it, it throws InvalidOperationException
.
By the way, DataSource
is a weird property. Instead of typeof(Airplane)
, you could set it to an Airplane
instead. Can you guess what happens if you replace the three Add()
statements with this?
bs.Add(new Airplane("Boeing 747", 800));
bs.DataSource = new Airplane("Airbus A380", 1023);
bs.Add(new Airplane("Cessna 162", 67));
Spoiler: you'll end up with two airplanes, the Airbus and the Cessna. It's almost as though you had written:
bs.Add(new Airplane("Boeing 747", 800));
bs.Clear();
bs.Add(new Airplane("Airbus A380", 1023));
bs.Add(new Airplane("Cessna 162", 67));
By the way, if you modify txtModel.Text
in code, the Model
of the current Airplane
is not updated, at least not immediately (future events tend to trigger an update somehow). One workaround is to change the underlying data (Airplane.Model
) instead and then call bs.ResetCurrentItem()
to update the controls.
Anyway, moving on...
How does it work?
Admittedly, the Model
textbox is really not needed because you can edit cells in the Model
column directly on the DataGridView
. But, this example shows that the two controls are automatically synchronized:
- If you change the current row, the textbox automatically shows the model of the current row.
- If you change the model text on one of the controls and press Tab, the other control is updated to match.
Magic! How do the two controls communicate? What's going on behind the scenes? BindingSource
is the ringleader, actually. You can learn a little bit about it by reading the documentation, which says that BindingSource
"simplifies binding controls on a form to data by providing currency management, change notification, and other services between Windows Forms controls and data sources". Currency management? When I saw that, I wondered, "isn't NumberFormatInfo
in charge of that?" But, it turns out that Currency management has nothing to do with Yen and Euros. Rather, it's Microsoft's way of saying "currentness". In other words, BindingSource
keeps track of which object is the current one in its List
. Internally, BindingSource
uses a CurrencyManager
, which holds a reference to the list and keeps track of the current item.
When the user edits the model name, the control modifies the BindingSource.Current
object somehow, and the BindingSource
raises the CurrentItemChanged
event. Actually, a single modification raises multiple events, and if you want to find out which ones, just add this code to Form1_Load
(this is C# 3.0 syntax; use anonymous delegates in C# 2.0):
bs.AddingNew += (s, ev) => Debug.WriteLine("AddingNew");
bs.BindingComplete += (s, ev) => Debug.WriteLine("BindingComplete");
bs.CurrentChanged += (s, ev) => Debug.WriteLine("CurrentChanged");
bs.CurrentItemChanged += (s, ev) => Debug.WriteLine("CurrentItemChanged");
bs.DataError += (s, ev) => Debug.WriteLine("DataError");
bs.DataMemberChanged += (s, ev) => Debug.WriteLine("DataMemberChanged");
bs.DataSourceChanged += (s, ev) => Debug.WriteLine("DataSourceChanged");
bs.ListChanged += (s, ev) => Debug.WriteLine("ListChanged");
bs.PositionChanged += (s, ev) => Debug.WriteLine("PositionChanged");
But, how does the control notify the BindingSource
about the change to its Text
property? Remember, from a control's perspective, the DataSource
is just an Object
. Also, I wonder: do controls have some kind of special support for BindingSource
specifically, or would they accept other classes as long as they implement a certain interface? Does BindingSource
have some kind of special support for DataSet
specifically, or does it merely look for certain methods/properties? In other words, is data binding based on duck typing, or is it special-cased for certain classes or interfaces? And, what is expected from data sources, in general?
Well, by using Visual Studio 2008 and following these instructions, you can trace through the .NET Framework source code. And, there is a special tool with which you can do the same thing in other versions of Visual Studio, plus, it downloads the complete source code rather than only the part you need. It is also possible to examine the code by disassembling it with Reflector and the FileDisassembler add-in, but this approach does not let you trace through the code.
Unfortunately, it's a really enormous and complicated code. After several hours, I figured out how exactly a control notifies the BindingSource
that its Text
property changed, and how the DataGrid
is notified. I will now explain how, but the explanation is so convoluted you probably won't want to hear it. Feel free to skip a few paragraphs.
To make a long story shorter, it turns out that:
- The
Binding
you added via txtModel.DataBindings.Add()
has handlers attached to the TextBox
's TextChanged
and Validating
events. - The latter handler (
Binding.Target_Validate
) passes the new value through a couple of internal classes, BindToObject
and ReflectPropertyDescriptor
, the latter of which uses Reflection to actually change the value in the Airplane
and then call base.OnValueChanged
in its base class, PropertyDescriptor
. OnValueChanged
invokes a delegate associated with the same Airplane
that points to BindingSource.ListItem_PropertyChanged
.- This handler raises its
ListChanged
event (with a ListChangedEventArgs
that specifies the index of the changed item). CurrencyManager.List_ListChanged
is attached to that event. This handler, in turn, raises its own ItemChanged
event, to which BindingSource.CurrencyManager_CurrentItemChanged
is attached.- This event handler merely raises the
BindingSource.CurrentItemChanged
event, which calls our WriteLine("CurrentItemChanged")
handler. - Next,
CurrencyManager.List_ListChanged
raises its own ListChanged
event. - An inner internal class of
DataGridView
has a handler (DataGridViewDataConnection.currencyManager_ListChanged
) that responds to that event by refreshing the row that was changed. - Finally,
currencyManager_ListChanged
raises a DataBindingComplete
event, but nobody handles it.
Whew! This is complicated. Now, how did the BindingSource
manage to associate an event handler with the Airplane
in the PropertyDescriptor
associated with the Binding
in the TextBox
's DataBindings
collection? Well,
- When you add the new
Binding
to the DataBindings
collection, Binding.SetBindableComponent()
is given a reference to the control, and the control has a BindingContext
, which is sort of a collection of bindings (similar but different from the DataBindings
collection, apparently). So, - The
Binding
(let's call it "b
") passes itself to BindingContext.UpdateBinding()
. The documentation says that this method "Associates a Binding
with a new BindingContext
". But, that's only true in a very roundabout way. First of all, UpdateBinding()
calls BindingContext.EnsureListManager(),
which notices that the Binding
's DataSource
(which is a BindingSource
, remember) implements ICurrencyManagerProvider
, so it calls ICurrencyManagerProvider.GetRelatedCurrencyManager(dataMember)
(where dataMember
is associated with the Binding
and, in this case, is an empty string). This is the magic part I was looking for--the part where the DataSource
is treated as more than just an Object
. Here, we see that the .NET framework looks for a special interface rather than use duck typing (Reflection-based invocation). Reflection is only used to learn the properties of Airplane
.- At this point,
BindingSource
has the opportunity to return its own internal CurrencyManager
object (let's call it "c
") that is wired to the BindingSource
. - However, now my head explodes because
UpdateBinding()
does not add b
to the BindingContext
, nor does it assign the CurrencyManager
to b
. Instead, b
is "added" to the CurrencyManager
by calling c.Bindings.Add(b)
. c.Bindings
has the type ListManagerBindingsCollection
(an internal class). c.Bindings.Add(b)
calls a method that calls b.SetListManager(c)
. In this way, the BindingSource
's CurrencyManager
is finally associated with the Binding
b
and its corresponding BindToObject
object. So, when the user changes the TextBox
's Text
and tabs away, b
's BindToObject
has access to the CurrencyManager
, through which it acquires a ReflectPropertyDescriptor
(stored in BindToObject.fieldInfo
). The ReflectPropertyDescriptor
, in turn, was created by ListBindingHelper.GetListItemProperties()
on behalf of BindingSource
during the first call to bs.Add()
in our Form1_Load()
method. This ReflectPropertyDescriptor
contains a mapping from our Airplane
to BindingSource.ListItem_PropertyChanged
so that the BindingSource
can be notified of changes to the Airplane
.
Personally, I find this architecture highly unintuitive. And, the above was especially difficult to discover because the BCL (Base Class Library) is JIT-optimized, which means some functions are inlined (missing from the call stack), and most variables cannot be seen in the debugger. Plus, Intellisense doesn't work in it. Plus, most of the code lacks comments. But, I was at least able to determine that BindingContext
(remember, there is a BindingContext
for each control) has special-case code for data sources that implement ICurrencyManagerProvider
(which BindingSource
implements), IList
, and IListSource
, so you can expect that your data source must implement one of these interfaces in order to act like a list.
As for BindingSource
, it creates a List
property of type BindingList<T>
where T
is the kind of data you have given to it (e.g., BindingList<Airplane>
). It does not seem to be special-cased for DataSet
, although some code is special-cased for some interfaces, e.g.:
- If
T
implements INotifyPropertyChanged
, BindingList
will subscribe to its PropertyChanged
event. BindingSource
seems to work closely with CurrencyManager
, which holds an IList
and the current position on that list. CurrencyManager
has some special-case code for lists that implement IBindingList
, ITypedList
, and/or ICancelAddNew
, and for items on the list that implement IEditableObject
.
Unfortunately, the whole binding architecture seems tightly coupled to itself (i.e., there are many references and relationships between classes), so it is hard to follow at the source level. Therefore, I recommend trying to figure it out from experimentation and documentation. It would be nice to see all this laid out on a few UML diagrams, though.
What else can you do with data binding?
For one thing, you can bind to a list within a DataSource
. Try this new Form1_Load()
handler:
BindingSource bs = new BindingSource();
private void Form1_Load(object sender, EventArgs e)
{
Airplane a = new Airplane("Boeing 747", 800);
bs.DataMember = "Passengers";
bs.DataSource = a;
bs.Add(new Passenger("Joe Shmuck"));
a.Passengers.Add(new Passenger("Jack B. Nimble"));
bs.Add(new Passenger("Jane Doe"));
bs.Add(new Passenger("John Smith"));
grid.DataSource = bs;
grid.AutoGenerateColumns = true;
txtModel.DataBindings.Add("Text", bs, "Name");
label1.Text = "Name:";
}
Unlike last time, this time we set the DataMember
property. Now, you get a list of passengers for a single Airplane
, instead of a list of Airplane
s:
Tip: If you want to show an ADO.NET table, just replace Airplane
with your DataSet
and replace "Passengers" with the name of a DataTable
in that DataSet
.
Notice that we can either add items to the BindingSource
(bs
) or to a.Passengers
directly. But, changing a.Passengers
directly doesn't always work, as you'll see if you add this code to the end of Form1_Load()
:
a.Passengers.Insert(0, new Passenger("Oops 1"));
EventHandler eh = null;
Application.Idle += (eh = delegate(object s, EventArgs e2) {
a.Passengers.Insert(0, new Passenger("Oops 2"));
Application.Idle -= eh;
});
There will still only be four items in the list. At first, you see Oops 1 at the top; Oops 2 will suddenly appear when you move your mouse over the grid. It doesn't work correctly because the BindingSource
was not notified of the changes. If you must modify the underlying list, you can refresh the on-screen list by calling BindingSource.ResetBindings(false)
.
Binding without a BindingSource
You can bind to an object directly without a BindingSource
. For this example (and only this example), add a Button
(button1
) to the form. At run-time, it will look like this:
In this example, you can edit Airplane.Passengers
and Airplane.Model
.
To set it up, double-click button1, then replace Form1_Load
and button1_Click
, with the following code:
Airplane a = new Airplane("Boeing 747", 800);
private void Form1_Load(object sender, EventArgs e)
{
a.Passengers.Add(new Passenger("Joe Shmuck"));
a.Passengers.Add(new Passenger("Jack B. Nimble"));
a.Passengers.Add(new Passenger("Jane Doe"));
a.Passengers.Add(new Passenger("John Smith"));
grid.DataSource = a;
grid.DataMember = "Passengers";
grid.AutoGenerateColumns = true;
txtModel.DataBindings.Add("Text", a, "Model");
}
private void button1_Click(object sender, EventArgs e)
{
string msg = string.Format(
"The last passenger on this {0} is named {1}. Add another passenger?",
a.Model, a.Passengers[a.Passengers.Count-1].Name);
if (MessageBox.Show(msg, "", MessageBoxButtons.YesNo) == DialogResult.Yes) {
a.Passengers.Add(new Passenger("New Passenger"));
grid.ResetBindings();
}
}
button1
demonstrates two things:
- When the user changes the grid or text box, the underlying data source is still changed as you would expect.
- If you add a row to the underlying data source, you must call
ResetBindings()
on the grid (this is not required when adding a row to a BindingSource
).
This example seems to work perfectly fine, so why bother to use a BindingSource
at all?
- A
BindingSource
automatically synchronizes data between multiple controls that show the same data. This example only works right because the data on the two controls is independent. - A
BindingSource
automatically refreshes bound controls when you add or remove an item from its List
. BindingSource
s can be chained together (as discussed in the next section).- Something strange happens when you modify the model: the grid's
CurrentRow.Index
changes to zero. I have no idea why, but I doubt this occurs when you use a BindingSource
.
If a DataSet
or DataTable
is used as a DataSource
, I heard they provide some, but not all, of these features (I don't know the details).
Note: we won't use the button anymore, so you can delete it now.
Hierarchical data binding
You can show a list within a record in a ListBox
or ComboBox
. Let's say you want to show the passengers on the selected plane, in a new list named "lstPassengers
", so that your window looks like this:
You can do this with the following code:
BindingSource bs = new BindingSource();
private void Form1_Load(object sender, EventArgs e)
{
Airplane a1, a2, a3;
bs.Add(a1 = new Airplane("Boeing 747", 800));
bs.Add(a2 = new Airplane("Airbus A380", 1023));
bs.Add(a3 = new Airplane("Cessna 162", 67));
a1.Passengers.Add(new Passenger("Joe Shmuck"));
a1.Passengers.Add(new Passenger("Jack B. Nimble"));
a1.Passengers.Add(new Passenger("Jib Jab"));
a2.Passengers.Add(new Passenger("Jackie Tyler"));
a2.Passengers.Add(new Passenger("Jane Doe"));
a3.Passengers.Add(new Passenger("John Smith"));
grid.DataSource = bs;
grid.AutoGenerateColumns = true;
lstPassengers.DataSource = bs;
lstPassengers.DisplayMember = "Passengers.Name";
txtModel.DataBindings.Add("Text", bs, "Model");
}
Here, we tell the ListBox
to be populated with the Name
property of all Passenger
s using a dot-separated notation. (I am not sure whether you can use dot-separated names in other circumstances).
But, what if you want to use data binding with a TextBox
so the user can change passengers' names?
Design time
Runtime
This kind of data binding is known as Master-Details. For this, you need two BindingSource
s, because a BindingSource
only has one CurrencyManager
, so it can only keep track of one "current record". Here, you need two because you want txtModel
bound to the current Airplane
and txtName
bound to the current Passenger
. Luckily, the solution is easy because you can chain BindingSource
s together:
BindingSource bsA = new BindingSource();
BindingSource bsP = new BindingSource();
private void Form1_Load(object sender, EventArgs e)
{
Airplane a1, a2, a3;
bsA.Add(a1 = new Airplane("Boeing 747", 800));
bsA.Add(a2 = new Airplane("Airbus A380", 1023));
bsA.Add(a3 = new Airplane("Cessna 162", 67));
a1.Passengers.Add(new Passenger("Joe Shmuck"));
a1.Passengers.Add(new Passenger("Jack B. Nimble"));
a1.Passengers.Add(new Passenger("Jib Jab"));
a2.Passengers.Add(new Passenger("Jackie Tyler"));
a2.Passengers.Add(new Passenger("Jane Doe"));
a3.Passengers.Add(new Passenger("John Smith"));
grid.DataSource = bsA;
grid.AutoGenerateColumns = true;
txtModel.DataBindings.Add("Text", bsA, "Model");
bsP.DataSource = bsA;
bsP.DataMember = "Passengers";
lstPassengers.DataSource = bsP;
lstPassengers.DisplayMember = "Name";
txtName.DataBindings.Add("Text", bsP, "Name");
}
That's it, and it works perfectly.
The DataGridView
might not provide a way to add new rows* even though its AllowUserToAddRows
property is true
by default. This is because BindingSource
's list (BindingList<Airplane>
) has a property that must also be true
in order to add rows. If you add the following line at the end of Form1_Load
, DataGridView
will provide a row-adding interface:
((BindingList<Airplane>)bsA.List).AllowNew = true;
Similarly, you could let users delete rows with:
((BindingList<Airplane>)bsA.List).AllowRemove = true;
But, the user interface for this is not intuitive. The user must select the whole row by clicking the little rectangle to the left of the row, then press the Delete key. There is no way to delete a row using only the mouse or only the keyboard.
* I'm puzzled. The first time I ran this example, AllowNew
was false
by default. Later in the day, I tried the same code again (or so I thought), but AllowNew
was true
by default. Go figure.
Note: The sample project has this example in Form1.cs.
Hierarchical data binding with a DataSet
As scenarios become more complex, the more likely it is you'll want to use a database, or at least a DataSet
.
The following example is like the previous example, but uses a DataSet
instead of Airplane
and Passenger
classes. I've constructed the DataSet
's schema manually in order to spare you the trouble of setting up a database or a typed DataSet
. Simply copy and paste this method into Form1
:
DataSet CreateAirplaneSchema()
{
DataSet ds = new DataSet();
DataTable airplanes = ds.Tables.Add("Airplane");
DataColumn a_id = airplanes.Columns.Add("ID", typeof(int));
airplanes.Columns.Add("Model", typeof(string));
airplanes.Columns.Add("FuelLeftKg", typeof(int));
a_id.AutoIncrement = true;
a_id.AutoIncrementSeed = 1;
a_id.AutoIncrementStep = 1;
DataTable passengers = ds.Tables.Add("Passenger");
DataColumn p_id = passengers.Columns.Add("ID", typeof(int));
passengers.Columns.Add("AirplaneID", typeof(int));
passengers.Columns.Add("Name", typeof(string));
p_id.AutoIncrement = true;
p_id.AutoIncrementSeed = 1;
p_id.AutoIncrementStep = 1;
DataRelation relation = ds.Relations.Add("Airplane_Passengers",
airplanes.Columns["ID"],
passengers.Columns["AirplaneID"], true);
return ds;
}
And, use the following Form1_Load()
code (lines changed from the previous example are marked with //**):
BindingSource bsA = new BindingSource();
BindingSource bsP = new BindingSource();
private void Form1_Load(object sender, EventArgs e)
{
DataSet ds = CreateAirplaneSchema();
DataTable airplanes = ds.Tables["Airplane"];
DataTable passengers = ds.Tables["Passenger"];
bsA.DataSource = ds;
bsP.DataSource = ds;
bsA.DataMember = airplanes.TableName;
bsP.DataMember = passengers.TableName;
DataRow a1, a2, a3;
a1 = airplanes.Rows.Add(null, "Boeing 747", 800);
a2 = airplanes.Rows.Add(null, "Airbus A380", 1023);
a3 = airplanes.Rows.Add(null, "Cessna 162", 67);
passengers.Rows.Add(null, a1["ID"], "Joe Shmuck");
passengers.Rows.Add(null, a1["ID"], "Jack B. Nimble");
passengers.Rows.Add(null, a1["ID"], "Jib Jab");
passengers.Rows.Add(null, a2["ID"], "Jackie Tyler");
passengers.Rows.Add(null, a2["ID"], "Jane Doe");
passengers.Rows.Add(null, a3["ID"], "John Smith");
grid.DataSource = bsA;
grid.AutoGenerateColumns = true;
txtModel.DataBindings.Add("Text", bsA, "Model");
bsP.DataSource = bsA;
bsP.DataMember = "Airplane_Passengers";
lstPassengers.DataSource = bsP;
lstPassengers.DisplayMember = "Name";
txtName.DataBindings.Add("Text", bsP, "Name");
}
When you run the program, it behaves the same as before, except that the user can sort the list, add new rows, and delete rows, by default:
(If you don't think the user should be able to add rows, just set the grid
's AllowUserToAddRows
property to false
. While you're at it, you might want to change AllowUserToDeleteRows
, AllowUserToOrderColumns
, AllowUserToResizeColumns
, and AllowUserToResizeRows
also. But I digress: this is not a DataGridView
tutorial.)
Binding when there is no current item
Normally, bsP.Current
points to a DataRowView
if your DataSource
is a DataSet
; in the earlier object example, it normally points to a Passenger
. But, when you create a new row, it has no passengers, so bsP.Current
is null
. I have not seen any documentation about how the binding architecture is supposed to behave when there is no "current" item, but at least the architecture is smart enough to clear the "Name" textbox. However, you might notice that the user can still change the name (which has no effect).
If you want to disable the textbox when there are no passengers, you could add a handler for the binding source's ListChanged
event:
...
...
private void Form1_Load(object sender, EventArgs e)
{
bsP.ListChanged += new ListChangedEventHandler(bsP_ListChanged);
...
}
void bsP_ListChanged(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.Reset)
txtName.Enabled = bsP.Current != null;
}
I don't know whether this is the simplest solution, but it works fine.
Filtering
In a "real-world" application, there may be hundreds of rows to display. What if you want to filter the list according to some criteria given by the user?
BindingSource
provides a "Filter
" property that allows you to specify a boolean expression that controls which rows are displayed on bound controls. However, BindingSource
itself does not evaluate this expression; it just passes it along to the underlying List
, which must implement IBindingListView
. In our object-based example, the list of Airplane
s was BindingList<Airplane>
and the list of Passenger
s was a List<Passenger>
. Neither of those classes implement IBindingListView
, so that example can't use filtering (although filtering and sorting support could be added with BindingListView, an open-source library.)
You can, however, filter DataTable
s and DataView
s (DataTable
itself doesn't implement IBindingListView
, but its DefaultView
, returned by IListSource.GetList()
, does.)
To demonstrate DataSet
-based filtering, I created a new Form
, Form2
, based on the hierarchical DataSet
example above. Then, I added two new TextBox
es, txtAirplaneFilter
and txtPassengerFilter
(and some labels) to get this:
Next, I added the following TextChanged
event handlers for the TextBox
es:
void txtAirplaneFilter_TextChanged(object sender, EventArgs e)
{
try {
bsA.Filter = txtAirplaneFilter.Text;
txtAirplaneFilter.BackColor = SystemColors.Window;
} catch(InvalidExpressionException) {
txtAirplaneFilter.BackColor = Color.Pink;
}
}
private void txtPassengerFilter_TextChanged(object sender, EventArgs e)
{
try {
bsP.Filter = txtPassengerFilter.Text;
txtPassengerFilter.BackColor = SystemColors.Window;
} catch(InvalidExpressionException) {
txtPassengerFilter.BackColor = Color.Pink;
}
}
Here it is in action:
Note: the sample project has this example in Form2.cs.
As the screenshot shows, DataSet
supports an SQL-style syntax for filter expressions. If the expression is not understood, the TextBox
will have a pink background. By the way, setting the filter to an empty string clears the filter, almost as though you had called RemoveFilter()
on the BindingSource
. And, the DataSet.CaseSensitive
property controls whether string tests are case sensitive.
Tip: if you have two lists that are bound to different BindingSource
s, but each BindingSource
is attached to the same DataTable
, then both lists share the same filter (or so I've heard). To give them independent filters, create two DataView
s attached to the DataTable
(via the Table
property), and set each BindingSource
's DataSource
to a different DataView
.
A substring filter
Normally, you will not make your user input complete filter expressions. Instead, you might show all records that contain a substring that the user inputs. How could this be done? It would be nice if you could use a delegate as a filter, but the DataView
does not support it. You must work with the operators that the filter string offers, and the closest thing to a substring search is the "LIKE
" operator. An obvious choice of filter strings would be:
bs.Filter = string.Format("Name like '*{0}*'", txt.Text);
Unfortunately, this may not work as the user expects, e.g., if the user puts an apostrophe in his/her filter string. I offer this escaping routine to help:
static string EscapeSqlLike(string s_)
{
StringBuilder s = new StringBuilder(s_);
for (int i = 0; i < s.Length; i++) {
if (s[i] == '\'') {
s.Insert(i++, '\'');
continue;
}
if (s[i] == '[' || s[i] == '*' || s[i] == '?') {
s.Insert(i++, '[');
s.Insert(++i, ']');
}
}
return s.ToString();
}
Then, you can construct a substring filter like this:
bs.Filter = string.Format("Name like '*{0}*'", EscapeSqlLike(txt.Text));
What can't you do with data binding?
- You can't show calculated attributes that are not present in the original data source. For example, the grids above have a FuelLeftKg column; I don't think it is possible to show the same field in pounds without adding a new
FuelLeftLbs
attribute in the underlying data source. ADO.NET's DataColumn
supports calculated attributes through its "Expression
" property, but I assume such columns must be read-only. However, a DataGridView
can contain "unbound" columns that you set manually. - When populated with a list of objects (as in the first few examples above),
BindingSource.List
is a BindingList<T>
, which does not support sorting, nor searching by PropertyDescriptor
. If you want to support sorting or searching, one approach is to create a class derived from BindingList<T>
and to override the relevant methods and properties whose name ends with "Core" (for sorting, see the documentation of BindingList.ApplySortCore
for details.) Then, assign your custom list to bs.DataSource
(where bs
is your BindingSource
).
What can't I do with data binding?
There are some things I still don't know how to do.
- Maybe, I want to have a Cancel button that aborts all changes to a record. Or maybe, changes shouldn't be kept unless a "Save" button is clicked. How can this be accomplished?
- Maybe, I want to let the user edit multiple records simultaneously, taking advantage of the multiple selection feature of
DataGridView
, ListBox
, and other bindable controls. How could I, for example, let the user select multiple rows and then set the model of several airplanes simultaneously (to the same string)?
Related articles