Share via


DataGrid

Tailor Your DataGrid Apps Using Table Style and Custom Column Style Objects

Kristy Saunders

Code download available at:DataGrids.exe(424 KB)

This article assumes you're familiar with SQL Server and Windows Forms

Level of Difficulty123

SUMMARY

One of the most enduring challenges in writing user interfaces is figuring out how to display large amounts of data efficiently and intuitively without bewildering the user. The problem becomes particularly thorny when the interface must reflect hierarchical relationships within the data that the user needs to modify. The Windows Forms DataGrid control gives developers a powerful and flexible tool to meet this challenge. This article explains its basic operations and shows how to extend the DataGrid to display columns of data in an application-appropriate manner.

Contents

The Timesheet Application
Loading the DataSet
Displaying Client Data in a DataGrid Control
Parent-child Relationships in a DataSet
Customize the DataGrid with Properties
DataGridTableStyle and DataGridColumnStyle
Implementing a Custom Column Style
Windows Forms Data Binding in a Nutshell
The DataGridComboBoxColumn Class
The DataGridComboBoxColumn Column Style
Conclusion

The DataGrid is one of the most powerful Windows® Forms controls. By using default DataGrid property settings you can easily bind a DataSet to a DataGrid to create an impressive Windows Forms user interface—one that allows users to drill down through multiple levels of hierarchical data and sort and reorder columns. While it is easy to get started with the DataGrid control, leveraging its more sophisticated capabilities—for example, creating custom column displays—remains lightly documented and requires some trial and error. In this article I will explain how to bind to a DataGrid, illustrate the basic features of the control, and show how to exercise extensive control over the appearance and behavior of tables and columns. Finally, I'll demonstrate how you can extend the DataGrid control with custom column style objects by implementing a type that displays column values in a dropdown combobox.

The Timesheet Application

To illustrate various features of DataGrid controls, this article will walk through the implementation of a timesheet application for a fictional software consulting firm. Take a look at Figure 1 to see the complete application in action.

Figure 1 The Timesheet App

Figure 1** The Timesheet App **

The UI is a Windows Forms application that contains three DataGrid controls (see Figure 2). The first control provides a read-only listing of all clients. The second provides a read-only listing of all projects associated with the selected client. The third DataGrid control displays an employee's timesheet entries billable to the selected project. This DataGrid allows a user to input and edit time entries, but the user cannot modify items in the client and project DataGrid controls.

Figure 2 Relationships

Figure 2** Relationships **

Loading the DataSet

The DataGrid controls in the timesheet applications bind to a single DataSet object. While DataGrids can be bound to a variety of types, DataSets are their most common data source. The timesheet DataSet object is populated from a database using four SqlDataAdapter objects. The creation and configuration of the SqlDataAdapter objects is trivial when using the DataAdapter Configuration Wizard in Visual Studio® .NET. Note that I use stored procedures with the SqlDataAdapter objects to access rows from the Client, Project, TimeEntry, and Activity tables, and to insert, update, and delete rows from the TimeEntry table. This is because the timesheet app uses database-generated identity columns for primary keys—a common practice. After an insert operation, this database-generated value must be mapped back to the corresponding column in the DataTable for subsequent operations on that row. The most reliable method of retrieving this value is to use an INSERT stored procedure that returns it as an output parameter.

Code in the form's Load event handler, shown in Figure 3, populates the DataSet when the app starts. Note that the MissingSchemaAction property of the time entry SqlDataAdapter object is set to MissingSchemaAction.AddWithKey. This causes the return of more schema info, such as primary key information, constraint information, and column properties including whether null values are allowed. By default, only column name metadata is retrieved by the Fill method. When the application allows users to add and update rows in a DataTable object, additional schema information will help detect invalid values and constraint violations.

Figure 3 Loading Four Tables into a DataSet

private void Form1_Load(object sender, System.EventArgs e) { try { // Use client and project DataAdapters to fill // first two tables with clients and projects sqlDataAdapterClients.Fill(dataSetTimeTracker, "Client"); sqlDataAdapterProjects.Fill(dataSetTimeTracker, "Project"); // Hardcode EmployeeID parameter for _GetTimeEntries sproc sqlSelectCommand3.Parameters["@EmployeeID"].Value = 1; // Set AddWithKey to get primary key restraints sqlDataAdapterEntries.MissingSchemaAction = MissingSchemaAction.AddWithKey; // Use entries DataAdapter to fill 3rd table in DataSet sqlDataAdapterEntries.Fill(dataSetTimeTracker, "TimeEntry"); // Use activities DataAdapter to fill 4th table in DataSet sqlDataAdapterActivities.Fill(dataSetTimeTracker, "Activity"); } catch (System.Exception ex) { MessageBox.Show(ex.Message, Text); } }

Displaying Client Data in a DataGrid Control

Once you have populated a DataSet object, it is simple to get a DataGrid to display the data. To do this, you use the SetDataBinding method of the DataGrid to specify which DataSet, and which DataTable within that DataSet, to use. The following code binds the Client table in the DataSet to the dataGridClients object:

// Bind the Client table to the client's DataGrid dataGridClients.SetDataBinding(dataSetTimesheet, "Client");

Figure 4 shows the DataGrid for the Client table.

Figure 4 Client Table DataGrid

Figure 4** Client Table DataGrid **

Once the DataTable object is bound to the DataGrid, the DataGrid uses the column metadata for the Client table to set the column headers, then uses the data in the Client table to populate the DataGrid. At this point, you can click on column headers to sort by column. Though this is fairly basic, you will soon see how to perform more sophisticated tricks with a DataGrid.

Parent-child Relationships in a DataSet

You saw in Figure 2 that a parent-child relationship exists between the Client and Project tables based on the ClientID column. In addition, the ProjectID column defines a parent-child relationship between the Project and TimeEntry tables. Even though the DataSet tables contain the ClientID and ProjectID columns, the DataSet is unaware of these relationships. To relate tables in a DataSet you must explicitly define the relationship by adding a DataRelation object to the DataSet's Relations collection. The timesheet app uses these two statements to create the Client-Project and Project-TimeEntry relationships:

dataSetTimesheet.Relations.Add("Client2Projects", dataSetTimesheet.Tables["Clients"].Columns["ClientID"], dataSetTimesheet.Tables["Projects"].Columns["ClientID"]); dataSetTimesheet.Relations.Add("Project2TimeEntries", dataSetTimesheet.Tables["Projects"].Columns["ProjectID"], dataSetTimesheet.Tables["TimeEntry"].Columns["ProjectID"]);

To add a relationship, you provide a name for the DataRelation ("Client2Projects" and "Project2TimeEntries" in the example given) and specify the column in both the parent and child table that will relate the two tables.

Figure 5 DataGrid with Relations

Figure 5** DataGrid with Relations **

Figure 5 shows the DataGrid control after these DataRelation objects have been defined. Notice that the name of the child relationship ("Client2Projects") displays as a Web link beneath each row when the plus sign (+) is expanded. Clicking the first link navigates to the children of the client Woodgrove Bank in the Projects table (see Figure 6).

Figure 6 Children in Projects

Figure 6** Children in Projects **

You can click the back button in the DataGrid caption to return to the parent table. The right-most button allows you to toggle the display of the parent row. Notice that "Project2TimeEntries" Web links are displayed when a "+" icon is expanded.

While the built-in navigation support is useful, the drill-down model might not provide the ideal display of hierarchical information for your app. One alternative is to use multiple DataGrids to display related data. To display a table containing just the child rows of a parent that has been selected in another DataGrid control, you can bind the child DataGrid control to a relationship name rather than to a DataTable object. You specify a relationship by providing the parent table name followed by the relationship name, delimited by a period. Note that you can bind to multiple levels of DataRelations. For example, in the timesheet application, "Client.Clients2Project" specifies the Clients2Project DataRelation object and "Client.Clients2Project.Project2TimeEntries" specifies the Project2TimeEntries DataRelation object. Binding to this relationship will return the child rows of the project that was selected in the selected client.

The following code binds the first timesheet DataGrid to the Clients table, the upper-right DataGrid to the "Clients.Clients2Project" relationship, and the bottom DataGrid to Clients.Clients2Project.Project2TimeEntries:

// Bind the Client table to the client's DataGrid dataGridClients.SetDataBinding(dataSetTimesheet, "Client"); // Bind Client2Project to projects datagrid dataGridProjects.SetDataBinding(dataSetTimesheet, "Client.Client2Projects"); // Bind Project2TimeEntries to tasks datagrid dataGridEntries.SetDataBinding(dataSetTimesheet, "Client.Client2Projects.Project2TimeEntries");

Setting the AllowNavigation property to false for each DataGrid disables the built-in navigation shown in Figure 5, Figure 6, and Figure 7. Setting the ReadOnly property of the clients and project DataGrids to true ensures that the rows displayed in these DataGrids cannot be edited. See Figure 7 for these changes.

Figure 7 Three Grids

Figure 7** Three Grids **

Notice that the project DataGrid control displays only those child rows of the client row that are selected in the client DataGrid. Likewise, the time entry rows displayed in the time entry DataGrid are only those that are children of the row selected in the project DataGrid. When a different client or project is selected, the project and time entry DataGrid controls are automatically refreshed to reflect the corresponding child rows. In the time entries DataGrid control, users can delete a row by selecting it and hitting the Delete key and they can add a row by entering values at the last row, marked by an asterisk.

Customize the DataGrid with Properties

The timesheet app is now fully functional—users can make entries and persist changes to the database using the time entries SqlDataAdapter object. But the UI needs work.

Various DataGrid properties allow you to customize the DataGrid appearance. These properties are listed in Figure 8. Within Visual Studio .NET, you can choose from a number of pre-formatted schemes from the Auto Format dialog—much like you can auto-format cells in a Microsoft® Excel spreadsheet. This dialog is accessible from a link at the bottom of the DataGrid Properties window. I am partial to the "Professional 3" scheme, so I chose it here. Selecting this scheme sets the properties listed in Figure 8.

Figure 8 DataGrid Properties that Affect Appearance

Property Description
AllowSorting Indicates whether the grid can be re-sorted by clicking on a column header
AlternatingBackColor Background color of alternating rows for a ledger appearance
BackColor Background color of the grid
BackgroundColor Color of the non-row area of the grid
BorderStyle Grid's border style
CaptionBackColor Background color of the caption area
CaptionFont Font of the grid's caption
CaptionForeColor Foreground color of the caption area
CaptionText Text of the grid's window caption
CaptionVisible Indicates whether the grid's caption is visible
ColumnHeadersVisible Indicates whether the column headers in a table are visible
FirstVisibleColumn Index of the first visible column in a grid
FlatMode Value indicating whether the grid displays in flat mode
GridLineColor Color of the grid lines
GridLineStyle Line style of the grid
HeaderBackColor Background color of all row and column headers
HeaderFont Font used for column headers
HeaderForeColor Foreground color of headers
LinkColor Color of the text that you can click to navigate to a child table
LinkHoverColor Color a link changes to when the mouse pointer moves over it
ParentRowsBackColor Background color of parent rows
ParentRowsForeColor Foreground color of parent rows
ParentRowsLabelStyle Display style for parent row labels
ParentRowsVisible Indicates whether the parent rows of a table are visible
PreferredColumnWidth Default width of the grid columns in pixels
PreferredRowHeight Preferred row height for the SystemWindowsFormsDataGrid control
RowHeadersVisible Specifies whether row headers are visible
RowHeaderWidth Width of row headers
SelectionBackColor Background color of selected rows
SelectionForeColor Foreground color of selected rows

DataGridTableStyle and DataGridColumnStyle

Next I want to make some tables and columns read-only, hide some columns, show numeric columns in a particular format, label columns with different text, and choose appropriate default column widths. These changes require the use of DataGridTableStyles and DataGridColumnStyles objects.

With DataGridTableStyle objects, you can set appearance properties and selectively omit or reorder displayed columns. By adding multiple DataGridTableStyle objects to the TableStyle collection of a DataGrid, you can create a number of distinct appearances. This makes it possible to programmatically toggle between display schemes. In addition, when a single DataGrid control is structured to display multiple tables, you can map a different DataGridTableStyle object to each table.

With DataGridColumnStyle objects you can affect column appearance, behavior, and data formatting. DataGridColumnStyle is an abstract class for which the Microsoft .NET Framework provides two implementations: the DataGridTextBoxColumn class and the DataGridBoolColumn class. When you bind to a DataGrid without providing DataGridColumnStyle objects, the DataGrid automatically creates them. The type created depends on the data type of the associated column. For example, in the timesheet app, the Approved column is a System.Boolean. The DataGrid creates a DataGridBoolColumn to display this column (notice the checkbox in Figure 7). All other columns in the application are displayed using DataGridTextBoxColumn objects.

Next I'll use DataGridTableStyle and DataGridColumnStyle objects to refine the timesheet's appearance by:

  • Omitting the display of the database identity columns ClientID, ProjectID, and TimeEntryID in the DataGrid controls. Because these columns are identifiers, meaningful only to the database, this is okay.
  • Adjusting column widths in each DataGrid.
  • Omitting the display of the EmployeeID column in the time entries DataGrid control. It is not necessary to display the EmployeeID because all time entries will always belong to the same employee. The application will set the DefaultValue property of the related DataColumn to the current user's employee ID, giving new rows a value for this column.
  • Formatting the Duration column in the time entries DataGrid control such that the column values display with two digits behind the decimal point.
  • Ensuring that the Approved column in the time entries DataGrid control is read-only.

You can use the Properties window associated with a DataGrid control to create DataGridTableStyle and DataGridColumnStyle objects. Figure 9 shows the DataGridTableStyle Collection Editor dialog, accessible by clicking the TableStyles property in the DataGrid Properties window.

Figure 9 DataGridTableStyle Properties

Figure 9** DataGridTableStyle Properties **

When you add a DataGridTableStyle object, you associate it with one of the tables bound to the DataGrid control data source by setting the MappingName property to the name of the table. Figure 10 shows code generated by Visual Studio .NET to configure a DataGridTableStyle object and DataGridColumnStyle object in the timesheet application.

Figure 10 DataGridTableStyle and DataGridColumnStyle

// // dataGridClients // ••• dataGridClients.TableStyles.AddRange(new System.Windows.Forms.DataGridTableStyle[] { dataGridTableStyleClients}); ••• // // dataGridTableStyleClients // dataGridTableStyleClients.DataGrid = dataGridClients; dataGridTableStyleClients.GridColumnStyles.AddRange(new System.Windows.Forms.DataGridColumnStyle[] { dataGridTextBoxColumnClients}); dataGridTableStyleClients.HeaderForeColor = System.Drawing.SystemColors.ControlText; dataGridTableStyleClients.MappingName = "Client"; // // dataGridTextBoxColumnClients // dataGridTextBoxColumnClients.Format = ""; dataGridTextBoxColumnClients.FormatInfo = null; dataGridTextBoxColumnClients.HeaderText = "Clients"; dataGridTextBoxColumnClients.MappingName = "ClientName"; dataGridTextBoxColumnClients.Width = 200;

By adding a DataGridTextBoxColumn or DataGridBoolColumn object to the GridColumnStyle collection of the DataGridTableStyle object, you indicate which columns you want to display within the table style. To associate a particular DataGridColumnStyle object to a data source column, you set its MappingName property to the name of the column. Other important DataGridColumnStyle properties include the HeaderText, Width, ReadOnly, and Format properties. The HeaderText property controls the text to display in the column header; the default is the name of the column. You can set the Format property of DataGridTextBoxColumn objects to a .NET Framework format-specifier to display the text in a particular format. For example, setting the Format property of the DataGridTextBoxColumn object mapped to the Duration column in the TimeEntry table to "#.00" causes the value to display with two digits behind the decimal point.

There is one problem though—the display schemes selected with AutoFormat are lost. They are only applied to the DataGrid itself, not to any associated DataGridTableStyle objects. Because it's convenient to use AutoFormat to design the display and DataGridTableStyle objects to customize column display, I added the method in Figure 11 to copy default DataGrid properties to a DataGridTableStyle object.

Figure 11 CopyDefaultTableStyle Method

// Copy the display-related properties of the given DataGrid // to the given DataGridTableStyle private void CopyDefaultTableStyle(DataGrid datagrid, DataGridTableStyle ts) { ts.AllowSorting = datagrid.AllowSorting; ts.AlternatingBackColor = datagrid.AlternatingBackColor; ts.BackColor = datagrid.BackColor; ts.ColumnHeadersVisible = datagrid.ColumnHeadersVisible; ts.ForeColor = datagrid.ForeColor; ts.GridLineColor = datagrid.GridLineColor; ts.GridLineStyle = datagrid.GridLineStyle; ts.HeaderBackColor = datagrid.HeaderBackColor; ts.HeaderFont = datagrid.HeaderFont; ts.HeaderForeColor = datagrid.HeaderForeColor; ts.LinkColor = datagrid.LinkColor; ts.PreferredColumnWidth = datagrid.PreferredColumnWidth; ts.PreferredRowHeight = datagrid.PreferredRowHeight; ts.ReadOnly = datagrid.ReadOnly; ts.RowHeadersVisible = datagrid.RowHeadersVisible; ts.RowHeaderWidth = datagrid.RowHeaderWidth; ts.SelectionBackColor = datagrid.SelectionBackColor; ts.SelectionForeColor = datagrid.SelectionForeColor; }

To copy the display-related properties of a DataGrid control to a DataGridTableStyle object that will be used by the DataGrid, you can use the CopyDefaultTableStyle method. The following code shows how to copy the grid defaults to the associated table styles:

// Copy data grid defaults to associated table styles CopyDefaultTableStyle(dataGridClients, dataGridTableStyleClients); CopyDefaultTableStyle(dataGridProjects, dataGridTableStyleProjects); CopyDefaultTableStyle(dataGridEntries, dataGridTableStyleEntries);

Figure 12 shows the result of applying the table and column styles.

Figure 12 Result of Styles

Figure 12** Result of Styles **

Implementing a Custom Column Style

Applying table and column styles has improved the data display, but before calling it a day, take a look at the Activity column in Figure 12. Notice back in Figure 2 that the ActivityID column of the TimeEntry table is a foreign key into the Activity table. Using the associated activity description would be more helpful than showing this identifier, but the stored procedures that insert into and update the database require the identifier.

A typical solution is to display valid descriptions in a combobox control. By deriving a column style class from the abstract DataGridColumnStyle, you can create a column style that uses a combobox control to allow selection of an arbitrary group of column values. The last section of this article implements a custom combobox column style class. However, understanding it will require a basic understanding of Windows Forms data binding.

Windows Forms Data Binding in a Nutshell

The concepts you must understand include how both simple and complex data binding work, how Windows Forms handles synchronization between multiple controls bound to the same data source, and how binding contexts are used.

First, Binding objects are used to bind a control property to a single data element. A Binding object describes an association between a control property and a data source. You can, for example, create a Binding object to bind the Text property of a textbox control to the value of a particular column in a DataTable. Simple binding refers to a control's ability to bind to a single data element.

All Windows Forms controls expose a DataBindings property that represents a collection of Binding objects. An application typically binds a data source to the control property that displays some data to the user, such as the Text property of a textbox. However, the Binding class allows you to bind any control property to a data source member. For example, this adds Binding objects to the DataBindings collection of two textbox controls:

textBox1.DataBindings.Add("Text", userList, "Name"); textBox1.DataBindings.Add("BackColor", userList, "FavoriteColor"); textBox2.DataBindings.Add("Text", userList, "PhoneNumber");

In this example, userList is an ArrayList instance populated with objects of type User. The User type exposes Name, FavoriteColor, and PhoneNumber properties. The first statement binds the Text property of a textbox control to the Name property of an object in an ArrayList named userList. The second statement binds the BackColor property of the same textbox control to the FavoriteColor property. The last statement binds the Text property of a second textbox control to the PhoneNumber property.

Second, the DataSource and DataMember properties of the DataGrid, combobox, listbox, and ErrorProvider controls are used to support complex data binding. Controls that support complex data binding have the ability to bind to and concurrently display a set of items, such as all the rows in a DataTable. Set the DataSource property to a data source such as a DataTable or an ArrayList. When the object referenced by DataSource is a type that can contain multiple lists, mainly the DataSet, you can set the DataMember property to indicate which list is the binding target.

The DataGrid control includes a SetDataBinding method that sets the DataSource and DataMember properties. The timesheet application uses this method:

// Bind the Clients table to the clients datagrid dataGridClients.SetDataBinding(dataSetTimesheet, "Clients");

Third, binding manager objects manage synchronization between multiple controls bound to the same data source. A data source is usually an instance of an ADO.NET type such as DataSet, DataTable, DataView, or DataViewManager, or another type such as ArrayList, that implements the IList interface. None of these types implement internal mechanisms to denote an item in the list as current. In applications that use data binding, the lack of such an internal mechanism presents a problem: how do multiple controls bound to the same data source manage to stay synchronized so that all bound Windows Forms controls display a value from the same row or item? In the previous example, how is the application assured that the Name and PhoneNumber that is displayed in the textbox controls will always belong to the same user in the ArrayList?

Windows Forms data binding solves this problem with two types that derive from the abstract BindingManagerBase class—the CurrencyManager class and the PropertyManager class. The first time any control binds to a particular data source, a BindingManagerBase object is automatically created. This object is shared by controls that subsequently bind to the same data source.

CurrencyManager objects are used to set the current position in list-based data sources. For example, the following code sets the Position property of the CurrencyManager associated with the userList given in the first point to set the next item as current:

myCurrencyManager = (CurrencyManager)BindingContext[userList]; myCurrencyManager.Position += 1;

This causes the bound textbox control properties to change to property values given by the next User item in userList. I will explain the BindingContext object next.

Controls usually bind to list-based types such as the data sources I just mentioned. However, any class that exposes public properties can be databound. You could, for example, bind the Text property of a textbox control to a single instance of the User class just described, instead of to an ArrayList of User items. PropertyManager objects are used to manage data sources that are not list-based. PropertyManager objects have a Position property, but setting it has no effect. Finally, CurrencyManager and PropertyManager objects exist within a BindingContext. A BindingContext is simply a collection of CurrencyManager and PropertyManager objects. More than one BindingContext can be created for a single form, but by default a single BindingContext is automatically created for each Windows Form. Each control can only be associated with a single BindingContext.

Each Windows control has a BindingContext property that returns the BindingContext to which the control belongs. You can use this property to retrieve the CurrencyManager or PropertyManager related to the data source bound to the control. How you retrieve the CurrencyManager associated with the control's data source is shown in the following line of code:

CurrencyManager cm = (CurrencyManager)(BindingContext[DataSource]);

By using the List property of the CurrencyManager, you can then access the bound data source, in this case a DataView object:

DataView dataview = ((DataView)cm.List);

The ability to access the data source by using a control's BindingContext property can be useful. You will see how the implementation of the DataGridComboBoxColumn uses this technique to access the data source bound to the combobox control member.

The DataGridComboBoxColumn Class

Recall that the timesheet application requires a combobox column style with the following characteristics:

  • When the DataGrid cell in the column is edited, it displays a dropdown combobox control within the cell bounds. When not being edited, the cell displays the selected description.
  • Applications that use the column style class can bind a combobox class member to any DataTable object. By setting the DisplayMember and ValueMember combobox properties, the application can specify which DataTable column contains descriptions to display and which column contains a corresponding identifier to return to the DataGrid control.
  • Because this is a common need in DataGrid applications, the derived column style should be generic and reusable.

This section will provide an overview of a combobox column style implementation. The complete implementation is shown in Figure 13. The class derives from the DataGridTextBoxColumn class instead of from the DataGridColumnStyle class. This lets the column style class borrow useful DataGridTextBoxColumn functionality, such as the ability to easily display text when the DataGrid cell is not being edited.

Figure 13 DataGridComboBoxColumn Class

// Derive class from DataGridTextBoxColumn public class DataGridComboBoxColumn : DataGridTextBoxColumn { // Hosted combobox control private ComboBox comboBox; private CurrencyManager cm; private int iCurrentRow; // Constructor - create combobox, // register selection change event handler, // register lose focus event handler public DataGridComboBoxColumn() { this.cm = null; // Create combobox and force DropDownList style this.comboBox = new ComboBox(); this.comboBox.DropDownStyle = ComboBoxStyle.DropDownList; // Add event handler for notification when combobox loses focus this.comboBox.Leave += new EventHandler(comboBox_Leave); } // Property to provide access to combobox public ComboBox ComboBox { get { return comboBox; } } // On edit, add scroll event handler, and display combobox protected override void Edit(System.Windows.Forms.CurrencyManager source, int rowNum, System.Drawing.Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible) { base.Edit(source, rowNum, bounds, readOnly, instantText, cellIsVisible); if (!readOnly && cellIsVisible) { // Save current row in the DataGrid and currency manager // associated with the data source for the DataGrid this.iCurrentRow = rowNum; this.cm = source; // Add event handler for DataGrid scroll notification this.DataGridTableStyle.DataGrid.Scroll += new EventHandler(DataGrid_Scroll); // Site the combobox control within the current cell this.comboBox.Parent = this.TextBox.Parent; Rectangle rect = this.DataGridTableStyle.DataGrid.GetCurrentCellBounds(); this.comboBox.Location = rect.Location; this.comboBox.Size = new Size(this.TextBox.Size.Width, this.comboBox.Size.Height); // Set combobox selection to given text this.comboBox.SelectedIndex = this.comboBox.FindStringExact(this.TextBox.Text); // Make the combobox visible and place on top textbox control this.comboBox.Show(); this.comboBox.BringToFront(); this.comboBox.Focus(); } } // Given a row, get the value member associated with a row. Use the // value member to find the associated display member by iterating // over bound data source protected override object GetColumnValueAtRow(System.Windows.Forms.CurrencyManager source, int rowNum) { // Given a row number in the DataGrid, get the display member object obj = base.GetColumnValueAtRow(source, rowNum); // Iterate through the data source bound to the ColumnComboBox CurrencyManager cm = (CurrencyManager) (this.DataGridTableStyle.DataGrid.BindingContext[this.comboBox.DataSource]); // Assumes the associated DataGrid is bound to a DataView or // DataTable DataView dataview = ((DataView)cm.List); int i; for (i = 0; i < dataview.Count; i++) { if (obj.Equals(dataview[i][this.comboBox.ValueMember])) break; } if (i < dataview.Count) return dataview[i][this.comboBox.DisplayMember]; return DBNull.Value; } // Given a row and a display member, iterate over bound data source to // find the associated value member. Set this value member. protected override void SetColumnValueAtRow(System.Windows.Forms.CurrencyManager source, int rowNum, object value) { object s = value; // Iterate through the data source bound to the ColumnComboBox CurrencyManager cm = (CurrencyManager) (this.DataGridTableStyle.DataGrid.BindingContext[this.comboBox.DataSource]); // Assumes the associated DataGrid is bound to a DataView or // DataTable DataView dataview = ((DataView)cm.List); int i; for (i = 0; i < dataview.Count; i++) { if (s.Equals(dataview[i][this.comboBox.DisplayMember])) break; } // If set item was found return corresponding value, // otherwise return DbNull.Value if(i < dataview.Count) s = dataview[i][this.comboBox.ValueMember]; else s = DBNull.Value; base.SetColumnValueAtRow(source, rowNum, s); } // On DataGrid scroll, hide the combobox private void DataGrid_Scroll(object sender, EventArgs e) { this.comboBox.Hide(); } // On combobox losing focus, set the column value, hide the combobox, // and unregister scroll event handler private void comboBox_Leave(object sender, EventArgs e) { DataRowView rowView = (DataRowView) this.comboBox.SelectedItem; string s = (string) rowView.Row[this.comboBox.DisplayMember]; SetColumnValueAtRow(this.cm, this.iCurrentRow, s); Invalidate(); this.comboBox.Hide(); this.DataGridTableStyle.DataGrid.Scroll -= new EventHandler(DataGrid_Scroll); } }

The DataGridComboBoxColumnStyle class contains a private combobox member and a read-only property to access it. The class constructor also registers an event handler to receive notification when focus leaves the combobox.

By overriding the Edit method, the class can track when editing begins by saving the current row and CurrencyManager object. The code then registers an event handler for the DataGrid Scroll event. Finally, this method shows the combobox control within the bounds of the DataGrid cell, selecting the display value associated with the cell value.

The Leave event handler tracks when editing ends. This method calls the SetColumnValueAtRow method to save the value associated with the currently selected item in the combobox control. To do this, it uses the CurrencyManager object and row saved in the Edit method. Then this method hides the combobox control and unregisters the Scroll event handler.

The base class GetColumnValueAtRow and SetColumnValueAtRow methods retrieve and save a value to a DataGrid cell. By overriding the GetColumnValueAtRow method, the DataGridComboBoxColumnStyle code can translate a display value to a corresponding value to save. When used by the timesheet application, this method will switch the selected Activity description for an ActivityID value. To do this, the overridden method calls the base class method to get the DataGrid cell value. It uses the BindingContext property of the DataGrid to get the DataView object that is associated with the bound data source. The DataView object is used to look up and return the display member column associated with the cell value.

Overriding the SetColumnValueAtRow method does the opposite—a saved DataGrid cell value is translated to a value to display. A DataView object, retrieved using the BindingContext property of the DataGrid, is used to find the value member column associated with the display member. The base class SetColumnValueAtRow is then called to set the underlying DataGrid cell value to the found value member. Finally, the Scroll event handler hides the combobox when the DataGrid is scrolled.

The DataGridComboBoxColumn Column Style

To use the combobox in the time entries DataGrid, three changes to the existing form are required. First, all references to the existing DataGridTextBoxColumn object for the ActivityID column are replaced with a reference to the DataGridComboBoxColumn object in the form's InitializeComponent method.

Second, the following code is added to the form's Load event handler to bind the combobox to the Activity table data source:

// Bind activities to comboBoxActivities dataGridComboBoxColumnActivity.ComboBox.DataSource = dataSetTimesheet.Tables["Activity"]; dataGridComboBoxColumnActivity.ComboBox.DisplayMember = "Description"; dataGridComboBoxColumnActivity.ComboBox.ValueMember = "ActivityID";

The second and third statements instruct the combobox to display the Description column while using the value in the corresponding ActivityID column.

Finally, setting the row height for the DataGrid control to the height of the combobox plus space for a small margin around it guarantees that the combobox will display completely within the DataGrid control cells.

// Set task datagrid preferred height to combobox height dataGridEntries.PreferredRowHeight = dataGridComboBoxColumnActivity.ComboBox.Height + 1;

Conclusion

Built-in navigation, plus the ability to bind to the all-powerful DataSet and tailor all aspects of its appearance, make the DataGrid a compelling tool for Windows Forms data-centric applications. And the extensible architecture of column styles make them one of the most powerful features of the DataGrid. This article has provided a simple example of a custom column style. By expanding on this idea to implement your own column styles, you can allow users to view and edit data within the DataGrid in any manner you think is appropriate and intuitive to solve your data display and access problems.

For related articles see:
Windows Forms
Introduction to the Windows Forms DataGrid Control
Windows Forms Data Architecture

For background information see:
Introducing ADO+: Data Access Services for the Microsoft .NET Framework
Using ADO.NET
Data Points: Establishing Relationships Between Rowsets with ADO.NET

Kristy Saunders is a trainer and consultant for SoftSource Consulting (https://www.sftsrc.com). She is based in Portland, Oregon, and can be reached at kristy@sftsrc.com.