Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

A Detailed Data Binding Tutorial

4.99/5 (145 votes)
25 Mar 2008MIT25 min read 6   14.5K  
Demonstrates a variety of Windows Forms data binding features through several simple examples.

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 DataSets; 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/DataSets 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 Bindings. 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:

  1. a single object named the Current object. A property of a Control can be bound to a property of Current.
  2. 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 TextBoxes. 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:

C#
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:

C#
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 Airplanes. 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").

Image 1

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):

Image 2

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:

Image 3

Then, click OK. Now, all you have to do is add some airplanes to the list. This is done by adding Airplanes to the BindingSource. So, make a Form1_Load() handler (double-click an empty space on the form), and add some code like:

C#
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:

Image 4

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 //**):

C#
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; // create columns automatically //**
    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:

Image 5

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?

C#
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:

C#
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):

C#
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:

C#
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")); // this happens to work also
    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 Airplanes:

Image 6

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():

C#
a.Passengers.Insert(0, new Passenger("Oops 1"));

EventHandler eh = null;
Application.Idle += (eh = delegate(object s, EventArgs e2) {
    // Window is now visible
    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:

Image 7

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:

C#
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.
  • BindingSources 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:

Image 8

You can do this with the following code:

C#
BindingSource bs = new BindingSource();

private void Form1_Load(object sender, EventArgs e)
{
    // Create some example data.
    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"));
    
    // Set up data binding
    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 Passengers 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?

Image 9

Design time

Image 10

Runtime

This kind of data binding is known as Master-Details. For this, you need two BindingSources, 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 BindingSources together:

C#
BindingSource bsA = new BindingSource(); // Airplanes
BindingSource bsP = new BindingSource(); // Passengers

private void Form1_Load(object sender, EventArgs e)
{
    // Create some example data.
    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"));

    // Set up data binding for the parent Airplanes
    grid.DataSource = bsA;
    grid.AutoGenerateColumns = true;
    txtModel.DataBindings.Add("Text", bsA, "Model");

    // Set up data binding for the child Passengers
    bsP.DataSource = bsA; // chaining bsP to 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:

C#
((BindingList<Airplane>)bsA.List).AllowNew = true;

Similarly, you could let users delete rows with:

C#
((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:

C#
DataSet CreateAirplaneSchema()
{
    DataSet ds = new DataSet();
    
    // Create Airplane table
    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;

    // Create Passengers table
    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;

    // Create parent-child relationship
    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 //**):

C#
BindingSource bsA = new BindingSource(); // Airplanes
BindingSource bsP = new BindingSource(); // Passengers

private void Form1_Load(object sender, EventArgs e)
{
    // Create DataSet and connect it to the BindingSources  //**
    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;                  //** 
 
    // Create some example data in the DataSet.             //** 
    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");      //** 

    // Set up data binding for the parent Airplanes
    grid.DataSource = bsA;
    grid.AutoGenerateColumns = true;
    txtModel.DataBindings.Add("Text", bsA, "Model");

    // Set up data binding for the child Passengers
    bsP.DataSource = bsA; // chaining bsP to 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:

Image 11

(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:

C#
...
...
private void Form1_Load(object sender, EventArgs e)
{
    bsP.ListChanged += new ListChangedEventHandler(bsP_ListChanged);   //** 

    ... // same as before
}

void bsP_ListChanged(object sender, ListChangedEventArgs e)            //** 
{                                                                      //** 
    // ListChangedType.Reset indicates that the entire list changed.   //** 
    // ListChanged is also raised when rows/columns are added/removed. //** 
    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 Airplanes was BindingList<Airplane> and the list of Passengers 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 DataTables and DataViews (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 TextBoxes, txtAirplaneFilter and txtPassengerFilter (and some labels) to get this:

Image 12

Next, I added the following TextChanged event handlers for the TextBoxes:

C#
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:

Image 13

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 BindingSources, 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 DataViews 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:

C#
// assuming bs is your BindingSource and txt is a TextBox
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:

C#
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:

C#
// assuming bs is your BindingSource and txt is a TextBox
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

License

This article, along with any associated source code and files, is licensed under The MIT License