Basic Data Binding
Almost every control in .NET supports data binding in one form or another. However, different controls support data binding in different ways. For example, when binding to a text box, button, or image control, you will usually bind to the TextBox.Text, Button.Text, or PictureBox.Image property (although there are other possibilities, as you'll discover shortly). Each of these properties can bind to a single piece of information at a time. On the other hand, a control like ListBox or CheckedListBox can hold an entire list of data or the contents of a single field from a database. Last, there are rich controls like DataGrid that can display all the information from a DataSet on their own.You don't need to create any database code to start working with data binding. .NET allows controls to bind to any class that implements the IList interface. Possible data sources include the following:
Arrays. You can use the ArrayList, Visual Basic's default Collection class, and a custom collection class that derives from System.Collections.CollectionBase. Other collection types (like queues and hashtables) are not supported.
ADO.NET data objects. Technically, you can only directly bind to a DataColumn, DataView, or DataViewManager object. However, when you bind to a DataTable, .NET automatically uses the corresponding default DataView it provides. Similarly, when binding to a DataSet .NET automatically uses the default DataViewManager.
Your custom classes. You have two options-you can create custom collections that implement IList or, more likely, you can just add your objects to an array or collection class, and use the collection object as a data source.
Figure 9-1 shows the relationship between data providers and data consumers.
Figure 9-1: Information flow in .NET data binding
Simple List Binding
Binding to a list is one of the most common data binding tasks. All the basic .NET list controls supply a DataSource property that accepts a reference to any IList data source. To test out simple data binding, create and fill an array, and bind it to a list using the DataSource property:
string[] cityChoices = {"Seattle", "New York", "Tokyo", "Montreal"};
lstCity.DataSource = cityChoices;
The list appears with the items from the array preloaded (see Figure 9-2).
Figure 9-2: Binding a list to an array of strings
There are two caveats: First, the Items collection of the list control is now read-only and can't be modified in your code. Second, if you change the contents of the array, the modifications do not appear in the list unless you clear the current binding and then rebind the list to the array.
string[] cityChoices = {"Seattle", "New York", "Tokyo", "Montreal"};
lstCity.DataSource = cityChoices;
// This change will not appear in the list.
cityChoices[3] = "Toronto";
// To update the list, you must rebind it.
lstCity.DataSource = null;
lstCity.DataSource = cityChoices;
If you want to provide for more flexibility, you can circumvent data binding and just copy the array items into the list:
string[] cityChoices = {"Seattle", "New York", "Tokyo", "Montreal"};
lstCity.Items.AddRange(cityChoices);
Though this approach appears to be equivalent, there are several differences. First, existing entries in the list remain in place. Second, you are free to modify the Items collection of the list. However, the most important differences may not appear until you begin to bind multiple controls simultaneously, as you see a little later in this chapter.
Binding Lists to Complex Objects
You can also bind a list control to a more complex object that provides several different fields of data. In this case, you still bind to the entire data source, but the DisplayMember property configures what text is used for each list entry.DisplayMember accepts a string that identifies a property in the data source. For example, you could create an array of special City objects and bind it to a list. You would then specify the property from the City class that should be used for the text. Note that the DisplayMember cannot be a public member variable. Instead, it must be a full property procedure. Consider the sample City class shown in the code that follows. It defines two properties and a constructor for easy initialization.
public class City
{
private string name;
private string country;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public string Country
{
get
{
return country;
}
set
{
country = value;
}
}
public City(string name, string country)
{
this.Name = name;
this.Country = country;
}
}
You could bind this in an array as follows:
City[] cityChoices = {new City("Seattle", "U.S.A."),
new City("New York", "U.S.A."), new City("Tokyo", "Japan"),
new City("Montreal", "Canada")};
lstCity.DataSource = cityChoices;
lstCity.DisplayMember = "Name";
The list looks and behaves exactly the same as the simple array example. The only difference is that when you retrieve the currently selected item, you find that it's a full City object, complete with all the City properties. That allows you to store your data directly in a control, without needing to worry about retaining other collections. To test this out, add the following code, and attach it to the lstCity.DoubleClick event that fires when an item in the list is double-clicked:
private void lstCity_DoubleClick(object sender, System.EventArgs e)
{
MessageBox.Show(((City)lstCity.SelectedItem).Country);
}
One interesting thing to note is what happens if you don't set the DisplayMember property. In this case, .NET simply calls the ToString() method of each object, and uses that to provide the text. Typically, this is the fully qualified class named, which means that every list appears exactly the same, as shown in Figure 9-3.
Figure 9-3: Binding to an array of objects without DisplayMember
However, you can put this behavior to good use by creating an object with an overriden ToString() method. This method could return some more useful information or a combination of different properties. Here's an example:
public class City
{
private string name;
private string country;
public string Name
{
get
{
return name;
}
set
{
name = value;
}
}
public string Country
{
get
{
return country;
}
set
{
country = value;
}
}
public City(string name, string country)
{
this.Name = name;
this.Country = country;
}
public override string ToString()
{
return Name + ", " + Country;
}
}
You then bind it without setting the DisplayMember property.
City[] cityChoices = {new City("Seattle", "U.S.A."),
new City("New York", "U.S.A."), new City("Tokyo", "Japan"),
new City("Montreal", "Canada")};
lstCity.DataSource = cityChoices;
The result of this code, using the overridden version of the ToString() method, is shown in Figure 9-4.
Figure 9-4: Overriding ToString() in a data bound object
Tip
The advantages that can be gained by these two techniques are remarkable. You can bind data without being forced to adopt a specific data access technology. If you don't like ADO.NET, it's easy to design your own business objects and use them for binding. Best of all, they remain available through the Items collection of the list, which means you don't need to spend additional programming effort tracking this information.
Single-Value Binding
.NET list controls are designed for this type of data binding and provide a helpful DataSource property that's inherited from the base ListControl class. Other controls, like text boxes and buttons, don't add this feature. However, every control gains basic single-value data binding ability from the Control.DataBindings collection.Using this collection, you can link any control property to a field in a data source. To connect a text box to an array, you can use the following syntax:
string[] cityChoices = {"Seattle", "New York", "Tokyo", "Montreal"};
txtCity.DataBindings.Add("Text", CityChoices, ");
The first parameter is the name of the control property as a string. (.NET uses reflection to find the matching property, but it does not detect your mistakes at compile time.) The second parameter is the data source. The third parameter is the property or field in the DataSource that is used for the binding. In this case, the data source only has one set of information, so an empty string is used.The results of this code are a little perplexing. The text for the first city appears in the text box, but there won't be any way to move to other items.Programmers who are familiar with traditional data binding will probably expect that they need to add a clumsy workaround to the form, like a special navigation control. This isn't the case. Instead, you have two options-controlling navigation programmatically, which you look at a little later, or adding a list control to provide simple navigation. For example, you can combine the list control example and the text box example to try out multiple control binding. Whatever item is selected in the list box appears in the text box. You'll also notice that the text in the text box is still editable, although the changes have no effect (see Figure 9-5).
Figure 9-5: Binding to two controls
Tip
The .NET list controls also provide a DataBindings collection.You can use this collection with single-value data binding. Just fill the list manually, and then bind to the SelectedValue property. This allows you to create a list control that can be used to update data (instead of one that is used for navigation).The nicest thing about single-value binding is that it can be used with almost any property. For example, you could set the background color of a text box, or specify the font. Unfortunately, there is no implicit type conversion when setting these specialized properties, which means you can't easily convert a string representing a font name into an actual font object. The code example that follows demonstrates some of the extra effort you need to go through if you want to bind one of these properties. It makes for an interesting example of extreme data binding. For it to work it requires that the System.Drawing namespace be imported.
// These are our final data sources: two ArrayList objects.
ArrayList fontObjList = new ArrayList();
ArrayList colorObjList = new ArrayList();
// The InstalledFonts collection allows us to enumerate installed fonts.
// Each FontFamily needs to be converted to a genuine Font object
// before it is suitable for data binding to the Control.Font property.
InstalledFontCollection InstalledFonts = new InstalledFontCollection();
foreach (FontFamily family in InstalledFonts.Families)
{
try
{
fontObjList.Add(new Font(family, 12));
}
catch
{
// We end up here if the font could not be created
// with the default style.
}
}
// In order to retrieve the list of colors, we need to first retrieve
// the strings for the KnownColor enumeration, and then convert each one
// into a suitable color object.
string[] colorNames;
colorNames = System.Enum.GetNames(typeof(KnownColor));
TypeConverter cnvrt = TypeDescriptor.GetConverter(typeof(KnownColor));
foreach (string colorName in colorNames)
{
colorObjList.Add((KnownColor)Color.FromKnownColor(
cnvrt.ConvertFromString(colorName)));
}
// We can now bind both our list controls.
lstColors.DataSource = colorObjList;
lstColors.DisplayMember = "Name";
lstFonts.DataSource = fontObjList;
lstFonts.DisplayMember = "Name";
// The label is bound to both data sources.
lblSampleText.DataBindings.Add("ForeColor", colorObjList, ");
lblSampleText.DataBindings.Add("Font", fontObjList, ");
You'll notice that the ForeColor and Font properties of the text box are simultaneously bound to two different data sources, which doesn't require any additional code. Some work is involved, however, to retrieve the list of currently installed fonts and named colors. The application is shown in Figure 9-6.
Figure 9-6: Data binding with other text box properties