Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DataGrid Zen Novice

0.00/5 (No votes)
14 Jan 2004 12  
Add style to DataGrid columns, custom columns.

Sample Image - finalshot.jpg

Introduction

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.

The Database

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:

Access Database

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 PhAreaCode, PhOfficeCode, PhPhoneNo and PhExtension. Each telephone number is represented by a row in the Phone table.

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:

  • The CyId column cannot contain empty or duplicate entries
  • Each entry in the PhCountryId column must contains a value that is also found in the CyId column

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 Country Table.

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 DataSet, DataTable, DataColumn and DataRow classes.

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:

//code to create a parent-child data relationship 

//and add this to the DataSet object

DataColumn parentCol;
DataColumn childCol;
// get a handle to the parent-child datacolumns

DataColumn parentCol = _dsInfo.Tables["Phone"].Columns["PhCountryId"];
DataColumn childCol = _dsInfo.Tables["Country"].Columns["CyId"];
// Add the relationship to the DataSet but do not create constraints

// (because not all child table entries are used by the parent table)

_dsInfo.Relations.Add("ByCountry", parentCol, childCol, false);
//end of code to create a parent-child relationship

First, we generate structure for each DataTable using 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.

The DataColumn class has two properties which are of interest to us:

// corresponds to a column name in a database

aColumn.ColumnName; 
// supposed to be a 'friendly' name for the column

aColumn.Caption;

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 DataGrid called DataSource and 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 control.

However, our 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 DataGrid.

The DataGrid gets a Makeover

The DataGrid control has a property called TableStyles. 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 DataTable object.

After binding the DataSet and 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";
// adding the table style corresponding to the Phone table induces the grid to 

// populate our DataGridTableStyle object with the corresponding column styles

grdPhone.TableStyles.Add(GridTableStyle);

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.
  • The DataGridTableStyle provides a property called GridColumnStyles which is a collection.
  • GridColumnStyles [] (indexed by the ColumnName string property) yields a DataGridColumnStyle object.

A diagram makes these relationships a little clearer. Note, these objects can navigate up the hierarchy, as indicated by the arrows:

Connections

Finally, we have a set of objects (class DataGridColumnStyle) that describe the appearance and performance of each column that appears in our DataGrid!

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

// revise the column headers to match the Caption field of each table column

CopyCaptionToHeader(grdPhone);

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

  1. DataGridBoolColumn (which implements a Boolean object displayed in a DataGrid column); and
  2. DataGridTextBoxColumn (which hosts a TextBox object in a DataGrid column)

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:

  1. The DataGridTextBoxColumn is hosting a single TextBox control.
  2. The 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.
  3. The TextBox control only appears when the column is selected for editing (you click in one of the cells belonging to the column).
  4. When the TextBox is not visible, a Paint() method is called which simply draws the appropriate text string into the cell boundaries.
  5. 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.
  6. When a cell becomes active, the Edit() method is called to make the TextBox visible. We can override this method and substitute our own ComboBox Control.

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 DataGridPictureColumn!

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:

  1. We need to populate the ComboBox control with all the countries from the "Country" DataTable. The ComboBox must be indexed by the country index (which is called CyId in our "Country" DataTable).
  2. We use the country identifier (PhCountryId) in the "Phone" DataTable to find the corresponding country name in the Country DataTable. This is the string which gets drawn when not editing the cell.
  3. 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.
  4. When the ComboBox is dismissed, we need to write-back the country index to the underlying "Phone" DataTable.

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:

DropDown Style

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

DropDownList Style

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 ComboBox Control.

Style Solutions

The Constructor for MyComboColumn supports selection of either a DropDownList or 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 DBnull object.

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

// define my custom combobox column style

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:

  • The DataSource which is used to populate the ComboBox. In this case, we are using the "Country" DataTable.
  • The name of the column in the DataSource we want displayed in the ComboBox. In this case, the CyName column in the "Country" DataTable will do the trick.
  • The object in the DataSource used to bind the ComboBox to a corresponding object in the "Phone" DataTable. In this case, we are using the CyId column.
  • A boolean which selects the DropDownList style of ComboBox when set true, and the DropDown style when set false.

To allow either the DropDownList or 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 "Country" DataTable. The Width property is set purely for aesthetic value.

I�ve tried to illustrate the bindings in the following diagram. I hope it helps:

ConnectionsII

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 DisplayMember property.

Binding (d) connects MyComboColumn to the PhCountryId column in the Phone table and is established through the MappingName property of MyComboColumn.

Binding (e) is provided by code in MyComboColumn and synchronizes the CyId and 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 Phone table.

Before we insert MyComboColumn, we must first remove the existing DataGridTextBoxColumn that binds to PhCountryId in the "Phone" DataTable. We simply cannot bind a new DataGridColumnStyle to the same DataColumn in the same DataTable:

// remove old column containing the unhelpful index value

grdPhone.TableStyles["Phone"].GridColumnStyles.RemoveAt(1);
// and replace add my custom column at the same location

this.InsertColumnAt(grdPhone.TableStyles["Phone"], aCboCol, 1);

The 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 DataSet or DataTable objects! Otherwise you may find they suddenly disappear en-masse if you reject recent updates using RejectChanges().

In both the Edit() and 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 "Phone" DataTable. In both cases, I default to using the first entry from the "Country" DataTable.

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 BeginUpdate() and EndUpdate() methods.

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 DataGridTableStyle.

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:

// uncomment these next two lines if you would like

// some south-west colors in your column

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:

Final screen shot

We've arrived. You can RemoveAt() or Remove() the 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!

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here