This project provides a 101 tour of the Windows Forms
DataGrid Control, with emphasis on easy-to-use (and understand) customizations.
You don't have database connectivity? No problem, this project's for you.
We build a simple, memory-resident database using classes provided by ADO.NET, no external database required. Next, we employ a
DataGrid object to display the contents of a table within our database. Finally, we move along to customizing the columns in the
DataGrid. And yes, if you want a custom combobox column, then look no further. It’s robust, uncomplicated and it works!
This article was not developed in a vacuum. I would like to credit the excellence of authors Kristy K. Saunders and Dino Esposito. I'm going to elaborate on their work, tempered by personal experience, to present an article for the novice user.
We are going to model telephone number in just two database tables; one describes a set of countries, the other contains the phone numbers themselves:
For the purpose of this exercise, let’s assume any telephone number is a combination of a Country-Code, Area-Code, Office-Code, Phone Number and Extension. With the exception of Country-Code, each of these elements is stored under a corresponding column in the
Phone table. The columns are
PhExtension. Each telephone number is represented by a row in the
Phone table also contains a
PhCountryId column that we can use to look-up a matching entry in the
Country table. From this table, we can extract the name of the country, and the country-code. The relationship is modeled as an arrow pointing from the
PhCountryId column (in
Phone) to the
CyId column (in
Country). For this to work, two conditions must be met:
CyId column cannot contain empty or duplicate entries
- Each entry in the
PhCountryId column must contains a value that is also found in the
Beyond these rules, we do want to allow for an empty value in the
PhCountryId Column. This can happen when, for instance, the phone is part of an International Satellite Phone System. Clearly it makes no sense to assign a 'Country-Code' to this type of phone! In database parlance this is referred to as a null index (or
DBnull in ADO.NET). We can accommodate this value in the
Country Table by inserting a row with a corresponding
DBnull value in the
CyId Column. If you look into the code that generates the memory-resident database, you will see this is the very first row I add to the
Now, ADO.NET is based on the concept of ‘Connectionless Database’. The name is really a misnomer because we obviously must be connected when we read, write or update the underlying database. However, in-between times, we are working out of database subsets that reside purely in application memory space. Contrast this with classic ADO (Visual Studio 6) which puts heavy emphasis on a continuous connection to the underlying database!
As a part of this approach, ADO.NET provides classes that correspond to database tables, columns and rows. ADO.NET (within the
System.Data namespace) also provides a rich set of database-related classes to manage Filters, Constraints, Relationships etc.). However, to keep our example manageable, we’ll focus primarily on just the
If we have a live database (SQL Server, Oracle, Access), we can use ADO.NET-aware components to auto-generate these objects directly from the database structure and content. However, it is instructive to perform this task as a manual exercise.
Please note: The
DataGrid has a visual representation that allows us to navigate between related tables. However we are trying, instead, to portray a merger of the Phone and Country columns into a spreadsheet-like view. For this reason, I chose not to make use of the available ADO.NET
DataRelation class or the
DataSet.Relations collection that
DataGrid can hook into. However, you can uncomment the following lines of code (in PhoneDataSet.cs) if you would like to explore inter-table navigation:
DataColumn parentCol = _dsInfo.Tables["Phone"].Columns["PhCountryId"];
DataColumn childCol = _dsInfo.Tables["Country"].Columns["CyId"];
_dsInfo.Relations.Add("ByCountry", parentCol, childCol, false);
First, we generate structure for each
DataColumn objects. Only then can we populate our
DataTable objects with actual data stored in
DataRow objects. Finally, we move both
DataTable objects (Phone and Country) into a container defined by the
DataSet class. There is no obligation to perform this step; we are not making extensive use of
DataSet. However, since most database applications employ this class, I thought it was representative to include
DataSet in my example.
Database construction is handled in the PhoneDataSet.cs class file. It’s quite straight forward and heavily commented. I’ve created a few helper routines to assemble columns, tables, rows and primary keys (which incidentally don’t get used in this project). The only property exposed by this class is the
DataSet object (
_dsInfo) that contains our
DataTables. Except for the helper functions, this is mostly throw-away code, but if you’ve never tried manual construction of database, the
PhoneDataSet class you might enjoy a quick look.
DataColumn class has two properties which are of interest to us:
Now to quote from Microsoft ™ Help pages, "You can use the Caption property to display a descriptive or friendly name for a DataColumn.". Okay, what I’m hoping is that
ColumnName corresponds to the title of a column in a database table. Likewise, I expect
Caption will be grabbed for the displayed
DataGrid column header. So in my database, I’ve set a friendly name for the
Caption property and a hostile database descriptor for the
ColumnName. As we shall shortly see, my optimism is once again to be dashed against the rocks.
Display the Phone Table using a DataGrid Control
Connecting our memory-resident Phone table to the
DataGrid is simplicity itself; we use two properties of the
DataMember. However, we do get choices on how we use these properties:
First, we can connect directly to the Phone
DataTable like this:
grdPhone.DataSource = _pdsPhone._dsInfo.Tables["Phone"];
Alternately, we can connect to the
DataSet container, then identify the contained
DataTable by name:
this.grdPhone.DataSource = _pdsPhone._dsInfo;
this.grdPhone.DataMember = "Phone";
Both approaches work equally well, which hints at the true versatility of the
DataGrid looks quite sad and is, frankly, less than I was hoping for. At the very least, I thought the
DataGrid would pick-up and display the
Caption property from the
DataColumn objects. Instead, what I see is the
ColumnName property, which is not what I wanted. In addition, I really don’t have much use for the index representation of a country (
PhCountryId). I want to see the actual name of the country. So it’s time to beautify our
The DataGrid gets a Makeover
DataGrid control has a property called
TableStyles is a collection of
DataGridTableStyle objects, indexed by a something called the
MappingName. This name is used because
DataGrid can bind to many collection types; hence calling it "TableName" might be considered quite inappropriate in some circumstances. However, in our case, this will be the name we gave to a
After binding the
DataTable to the
DataGrid, I set a breakpoint to explore the
TableStyles property to see how it was constructed. Unfortunately, the
DataGrid is running off an internal, default collection of
DataGridTableStyles that we are not intended to access (it's
protected!). Fortunately, there is a published trick to exposing this collection:
DataGridTableStyle GridTableStyle = new DataGridTableStyle();
GridTableStyle.MappingName = "Phone";
Amazingly, internal code within the
DataGrid has kindly populated my
DataGridTableStyle object with all the information about
DataGrid columns that I could reasonably wish for. Here is what I get:
TablesStyles  (indexed by
Phone) yields a
DataGridTableStyle provides a property called
GridColumnStyles which is a collection.
GridColumnStyles  (indexed by the
ColumnName string property) yields a
A diagram makes these relationships a little clearer. Note, these objects can navigate up the hierarchy, as indicated by the arrows:
Finally, we have a set of objects (class
DataGridColumnStyle) that describe the appearance and performance of each column that appears in our
Once I have navigated down to this object, I can change just about anything I want. For instance:
- Cell text alignment
- Header text
- Null text (the value displayed when there is no corresponding entry in the
DataTable linked to the
- Column width
We can also delete or add columns, re-order them or even add custom columns. But let’s not get ahead of ourselves here!
My project contains a single button which is labeled "Press me". The first time you press the button, it re-labels the columns using the
Caption property of the
DataColumn objects that we used to create the database itself. This task is performed by the method:
To demonstrate how simple it is to change column properties, I have also expanded the width of the
PhCountryId column to 90 pixels. Finally, I've chosen to set the first column (
IdxPhone) as read-only and centre-aligned.
We are making progress but I want to see, and select, the country name for each telephone entry that appears in the
DataGrid. Unfortunately, all I have at the moment, is an index value (for instance the value "501" represents "America"). So what to do?
Creating a Custom Column (our friend the DataGridComboBoxColumn)
I’ve talked about the
DataGridColumnStyle object which governs the appearance and performance of a single column in the
DataGrid. However, in reality,
DataGridColumnStyle is an abstract class. Unfortunately (and I would love to know why), we are only offered two concrete subclasses which we can actually use. These are:
DataGridBoolColumn (which implements a
Boolean object displayed in a
DataGrid column); and
DataGridTextBoxColumn (which hosts a
TextBox object in a
Oops! Neither of these is going to help me much! What I really want is a
ComboBox which magically appears whenever I click within a cell under the "Country" column header. Okay, I’ve read several articles which offered a custom
ComboBox column and right here I’m offering you my interpretation of this useful class. Due to a failure in my imaginative-naming subroutines, I’ve called my Class
MyComboColumn and you can view the code in MyComboColumn.cs. If you want to understand how I arrived at this Class, then read on. However if you simply want to use the class "as is", then skip to the section entitled "Using the DataGridComboBoxColumn".
Now, the Visual Studio .NET Help files invite us to sub-class
DataGridColumnStyle to create our own custom columns. I’m told which methods I need to override, but when do they get called and why? What are my responsibilities as the coder of a new, robust sub-class? I spent several hours experimenting, then decided to take the path of least resistance. I simply sub-classed the
DataGridTextBoxColumn as others have done before me!
What I discovered during my experimenting was the following:
DataGridTextBoxColumn is hosting a single
DataGrid acts as a control container. The
TextBox is contained within the
DataGrid.Controls collection. This is important because it affects positioning, key handling, navigation and visibility.
TextBox control only appears when the column is selected for editing (you click in one of the cells belonging to the column).
- When the
TextBox is not visible, a
Paint() method is called which simply draws the appropriate text string into the cell boundaries.
- In the best traditions of parametric polymorphism, there are three signatures for the
Paint() method but it appears only one of these is used consistently.
- When a cell becomes active, the
Edit() method is called to make the
TextBox visible. We can override this method and substitute our own
My first action was to simply override the
Paint() methods and put a breakpoint in each before calling the
base.Paint() method. In this way, I was able to determine which signature was in-use. I could equally well have drawn a picture instead of a string! How cool, we are half way to a
Next I override the
Edit() methods. Again there are multiple signatures but only one appears to be in-use within the
DataGridTextBoxColumn. So I can now construct an override method to create and display a
ComboBox within the boundaries supplied on the
Edit() parameter list. I must also remember to add the
ComboBox to the
DataGrid.Controls property. I cannot overstress the importance of this step!
I attach a (
Leave) event handler, so whenever the
ComboBox loses focus, we execute code to make the
ComboBox invisible. And voila! A
DataGridComboBoxColumn control. Well almost. We have a few more tasks to take care of:
- We need to populate the
ComboBox control with all the countries from the "
ComboBox must be indexed by the country index (which is called
CyId in our "
- We use the country identifier (
PhCountryId) in the "
DataTable to find the corresponding country name in the
DataTable. This is the string which gets drawn when not editing the cell.
- When we start to
Edit() the cell, we need to set the
ComboBox to display the country which currently appears in the cell. Thereafter, the user can select a different country if they so desire.
- When the
ComboBox is dismissed, we need to write-back the country index to the underlying "
We do have one additional problem, and it is quite significant. I would like to thank my (almost) tame testers, Dave (I can break anything) and Baldev (I can terrorise any coder) for pointing out this issue to me. The
ComboBox Control may be implemented using either a
DropDownList or a
DropDown style. The two styles result in quite different behaviors:
With this style the navigation keys (up-arrow or down-arrow) select the previous or next row from the
ComboBox. We can also edit the selected entry (which in most cases is undesirable).
With this style the navigation keys (up-arrow or down-arrow) select the previous or next row in the
DataGrid. Editing of the selected entry is not enabled, however if you press a key, such as the letter 'A' the next entry in the
ComboBox that starts with the same letter is selected. This can be very useful! However, the drawbacks derive from behaviors inherited from the super-class (
DataGridComboBoxColumn). When we navigate using the up-arrow or down-arrow keys, we do NOT get a '
Leave' event generated on the
The Constructor for
MyComboColumn supports selection of either a
DropDown style; both have virtue in specific circumstances, although I suspect the
DropDownList style is the preferred choice.
The solution for the
DropDownList style is almost as bizarre as the problem itself. After much thought (and some experiments) I discovered that setting the '
ReadOnly' Property of the super-class (
DataGridComboBoxColumn) restores the missing '
Leave' events. Ouch!
To block editing on the
DropDown style I have added an event handler for the 'KeyPress'
ComboBox event. The handler does not impact the navigation keys, nor does it impact the 'delete' key. However editing with ascii characters is now blocked. The value of retaining the 'delete' key is that it can be used to select the
To see how the two styles are accommodate in code, look at the Constructor for
MyComboColumn. The rest of the code is oblivious to the style we choose.
And that, ultimately, is about all it takes to create a custom
ComboBox column. I’ve stripped the code to a minimum so don’t be outraged by the absence of parameter validation and error recovery code (
catch). I felt the subject matter was complex enough without including extraneous code that might cause confusion. But the code does work without throwing exceptions, provided it’s used as intended. Which brings me to the next topic:
Using the DataGridComboBoxColumn
Here is all the code you need to prepare the
MyComboColumn aCboCol = new MyComboColumn(
_pdsPhone._dsInfo.Tables["Country"], "CyName", "CyId", true);
aCboCol.Width = 129;
aCboCol.MappingName = "PhCountryId";
aCboCol.HeaderText = "Country";
The constructor takes four parameters:
DataSource which is used to populate the
ComboBox. In this case, we are using the "
- The name of the column in the
DataSource we want displayed in the
ComboBox. In this case, the
CyName column in the "
DataTable will do the trick.
- The object in the
DataSource used to bind the
ComboBox to a corresponding object in the "
DataTable. In this case, we are using the
boolean which selects the
DropDownList style of
ComboBox when set
true, and the
DropDown style when set
To allow either the
DropDown style to be chosen I have provide a
CheckBox on the
Form. When checked, the
DropDownList is employed.
We must also bind the
MyComboColumn itself to the appropriate column in the
DataTable that currently underlies the
DataGrid itself. In this case, we are binding the
PhCountryId column in the "
Width property is set purely for aesthetic value.
I’ve tried to illustrate the bindings in the following diagram. I hope it helps:
Bindings (a) and (b) and (c) are responsible for populating the
ComboBox control and are established in the constructor for
MyComboColumn. Binding (a) links to the
ValueMember property of the
ComboBox while binding (c) links to the
Binding (d) connects
MyComboColumn to the
PhCountryId column in the
Phone table and is established through the
MappingName property of
Binding (e) is provided by code in
MyComboColumn and synchronizes the
PhCountryId columns. On the
Edit() method, this binding is used to select the initial entry shown in the
ComboBox control when it receives focus. On the corresponding "lose-focus" event, the
ValueMember property from the current row of the
ComboBox is written back to the corresponding row and column in the
Before we insert
MyComboColumn, we must first remove the existing
DataGridTextBoxColumn that binds to
PhCountryId in the "
DataTable. We simply cannot bind a new
DataGridColumnStyle to the same
DataColumn in the same
this.InsertColumnAt(grdPhone.TableStyles["Phone"], aCboCol, 1);
RemoveAt() methods works on a zero-based item array. Consequently, we are actually removing the second
DataGridColumnStyle from the collection, and not the first. Now you would think that a collection which implements a
RemoveAt() method would have a corresponding
InsertAt() method. Well you’d be wrong. Instead I’ve had to kludge my own method to perform this onerous task.
InsertColumnAt() makes a copy of the current
DataGridColumnStyle collection. Then it clears the existing collection and repopulates it by sequentially adding objects across from the copy. At the appropriate point in this re-construction sequence, the new
DataGridColumnStyle is added. Simple, inelegant, but it works.
To see the results of adding a new
ComboBox column, press the button (now labeled) "Press again".
Points of Interest
If you find problems related to my implementation, please let me know. I will fix errors in the code (if I can). Changes will be incorporated if they have merit in the context of an article written for novices. Now on to a few points that might interest you:
When you’ve finished adding
DataRow objects to your
DataTable objects, remember to
AcceptChanges() on the
DataTable objects! Otherwise you may find they suddenly disappear en-masse if you reject recent updates using
In both the
Paint() methods that I have overridden, we must deal with the possibility of encountering a null value (
System.DBNull) for the "
Country" index. This is a normal occurrence when we are adding a new row to the underlying "
DataTable. In both cases, I default to using the first entry from the "
You might be wondering why I delay binding to the parent
DataGrid object until the first time the
Edit() method is called. This is because the
DataGrid object is not available until
MyComboColumn is added into the
DataGrid.TableStyles collection. As the
Edit() method only gets called after this necessary step has occurred, I can safely bind at this point.
Another issue which arose was an eye-opener! I discovered the
ComboBox does not get populated until the
ComboBox.Visible property is set for the first time. Consequently, the code to make the
ComboBox visible, in
Edit(), is called BEFORE we select an item in the
ComboBox. To avoid multiple
Paint events, I use the
It is important to note that a
ComboBox is taller than a
TextBox which uses the same font. Consequently, you should set the
PreferredRowHeight to a suitable value. I do this by creating a temporary
ComboBox populated with the same font used for the current
Quite a few implementations I’ve seen appear to intercept the
Scroll event for the
DataGrid. But if you bind the
ComboBox to the
DataGrid's Control's collection, I don’t see this as a necessary step. The
DataGrid scrolls quite nicely even when the
ComboBox is visible.
Another issue I encountered relates to the color of individual columns. The
DataGrid control provides support for alternate-row coloring, however I wanted to color the columns. So I've added code which allows me to override (if I choose) the default colors for the
MyComboColumn control, both background and foreground. To use the code, uncomment the following two lines in Form1.cs:
aCboCol.backgroundColour = System.Drawing.Color.Aquamarine;
aCboCol.foregroundColour = System.Drawing.Color.RoyalBlue;
You can question the South-Western color scheme, but what you should get is this:
We've arrived. You can
IdxPhone column, but this was as much as I set out to achieve. Incidentally, some people think the way to remove a column is to set the width of the column to zero (0). However the column still exists and the "Tab" key will require a second press to skip over the invisible column.
I hope you can now see how to build your own columns in a
DataGrid. A column class to display graphics (such as items from a Parts Bin) should now be within your grasp.
Tip of the Day: If you are buying books on C# and you're a novice, consider also buying a book or two written for Visual Basic. Because Visual Basic is often regarded as "The People's Program Language", authors are expected to write super-friendly material. So often times I can get easy-to-read guidance from Visual Basic books. C# and Visual Basic are truly convergent languages!
I am Borg. I have six computers, a Lego mindstorm with video camera and I'm currently building a Dalek.
Generally I spend so much time in front of computers I probably glow in the dark.
However I like skiing, flying aircraft and the company of old women and young wines. Hang on? Isnt' that the wrong way round? I forget.
I will answer any question about anything. I am particularly good at things I know nothing about.
Where is my gin and tonic?