In-Depth

5 Easy Windows DataGrid Optimizations

The Windows Forms DataGrid is the de facto standard for tabular data. But it''s limited. Improve presentation and highlight important data elements with these enhancements.

The Windows.Forms DataGrid class provides all the basics to display tabular data in your desktop applications. But this quick basic approach might not provide the features your users desire in a professional application. Lucky for you, it's easy to make the DataGrid (or any Windows control for that matter) bow to your wishes.

In this article, I'll show you five tips to enhance the DataGrid presentation based on the data values stored in your data source. You'll see how to highlight important data for your users, which will help them find the bits of information that need immediate attention.

Check out the first cut of a demo that uses different data types in a Windows Forms DataGrid (see Figure 1). I made this application by creating a Typed Data Set from an XML Schema and using that dataset as the data source for the DataGrid. It was quick and easy, but it's well below the standard for most user communities. The text alignment and number formatting use the system-generated defaults, and each data element is drawn exactly the same way. Your users have no visual cues to tell them what items are more important. I'll start by showing some simple changes, then I'll work up to some more advanced techniques to highlight the information your users need to see.

Enhancement 1: Create Your Own Table and Column Styles
Your first change is to update the format options on the numeric data. You do this work in the designer: Create your own table style, and add column styles for the columns you want displayed. The important changes for this version are to right-justify the numeric data and to set a format string on the floating-point data. I set the format string to "F2" so that all floating-point data contains exactly two decimal places. Finally, when I create a new table style, I always change the null string from "(null)" to the empty string for character values, or "0" for numeric values.

Enhancement 2: Initialize Row Data Programmatically
Of course, you probably know how to do that already. The remaining changes require some new code. Often, some columns in your grid should be initialized programmatically instead of by the user. I'll initialize the Row Label column to show you how.

Set the value response to a RowChanged event generated by the DataTable in your data source. (If you have a custom data source, you'll need your own event, but the concept is the same.) In that event handler, set the row label based on the current number of rows in the sample:

private void 
  Sample_SampleRowChanged( 
  object sender, 
  DataGridExtensions.
  SampleData.
  SampleRowChangeEvent e)
{
  if ( e.Action == 
    DataRowAction.Add )
  {
    e.Row.RowLabel = 
      string.Format( 
      "Row {0}", 
      ( e.Row.Table.Rows.Count + 
      1 ).ToString() ); 
  }
}

Enhancement 3: Create Non-Scrolling Row Labels
One feature I often need on DataGrids is to create row labels that don't scroll off the screen. The DataGrid does not do that, but it does provide some screen real estate you can use to draw row labels yourself. The downloaded sample contains a DataGridRowHeaderLabel class that draws text from one of the columns in your data table on the Row header (see Figure 2). You'll make a couple changes to your initialization code to add this feature.

First, you'll remove the "RowLabel" column from your table style. That removes the row label column from the body of the grid. To show the label in the Row Header, create the DataGridRowHeaderLabel and initialize it to use the RowLabel property as the name for each row:

DataGridRowHeaderLabel l = new 
  DataGridRowHeaderLabel( );
l.SetRowHeaderBindings( 
  this.dataGrid1, "RowLabel" );

The DataGridRowHeaderLabel uses reflection to find the value of the requested property. You'll use this same technique in your own extensions, so I'll spend a bit more time going over it here. In many cases, you'll use reflection to get the value of a property from the object that represents one row of data. Follow these steps to do that:

  1. Retrieve the CurrencyManager from the grid object.
  2. Retrieve the source object for this row from the CurrencyManager, using the CurrencyManager's List property.
  3. Using the Type information from the current row, get the PropertyInfo for the named property, using Type.GetProperty().
  4. Invoke the Get accessor through the PropertyInfo to retrieve the value of the specified property, using PropertyInfo.GetValue().
  5. Convert that value to text using ToString().

But you could also follow another path. DataTables don't have specific compile-time properties for each column. Rather, they have an indexed property that uses the column name as the parameter. To retrieve a named value from that path, you need to follow a different set of steps:

  1. Retrieve the CurrencyManager from the grid object.
  2. Retrieve the collection of properties from the CurrencyManager using CurrencyManager.GetItemProperties().
  3. Find the named property descriptor in the collection using PropertyDescriptorCollection.Find.
  4. Invoke the property using PropertyDescriptor.GetValue().
  5. Convert the value to text using ToString().

Your code will need to support both paths, because you can't know which is correct until run time. Otherwise, you limit your extension to a particular class of data source. Look at the code in DataGridRowHeaderLabel.cs for more details. The Grid Painter handler contains the code to retrieve the header text and draw the text in the row headers.

Final Enhancements: Highlight Text and Cells
I'm going to cover these two enhancements together because the code is similar for both features. The purpose is simple: Some cells have important information, and you want to draw your users' attention to those cells immediately. Color is your best option—both for text and cell background. You can draw any cells with bad data in red, to encourage users to look at them immediately, and you could use other colors to denote different conditions. In the sample, I'll use blue, green, and yellow for different states.

Anytime you want to change the appearance of a column in the DataGrid, you will create your own class that is derived from DataGridColumnStyle. In this instance, you'll create a new class derived from DataGridTextBoxColumn. The DataGridColumnStyle class provides several Paint() methods that you can override to provide your own custom draw routines. In most cases, the paint routines call the override with all the different parameters specified:

protected override void Paint( 
  System.Drawing.Graphics g, 
  System.Drawing.Rectangle 
    bounds, 
  System.Windows.Forms.
    CurrencyManager source, 
  int rowNum, 
  System.Drawing.Brush 
    backBrush, 
  System.Drawing.Brush 
    foreBrush, 
  bool alignToRight) 

By overriding this overload, you can change the appearance of the cells in your application. To provide different colors, you'll modify the values of the backBrush and foreBrush parameters before delegating the work to the base class.

The final task is to provide a means of determining the proper colors for a given cell. You could follow the same design strategy I used for the row labels, but I chose to pick a different and more agile strategy. I wanted to provide a mechanism that would work even if your data source did not have properties that defined the colors desired for each cell. So I created an interface that defined the methods necessary to return the proper color based on the data in the row:

public interface 
  ICustomGridCellProperties
{
  Color GetForegroundColor( 
    object row );
  Color GetBackgroundColor( 
   object row );
}

The row parameter on each method defines the object that is the current row in the grid. By examining the values in the current row, you can return the correct color. This design means I can create a new class to provide the color information for my grid, without modifying the data source.

Check out the full source for the styled textbox (see Listing 1). The constructor stores a reference to an object that evaluates and returns the correct color for each cell. The Paint method uses this object to retrieve the correct color. A new brush in that color is initialized, and the base method paints the text.

To use this class, create a class implementing this interface that returns the proper color by examining properties in the row. There's only one non-obvious bit of code to implement here. If your data source is a dataset, the row object derives from DataRowView, not DataRow:

DataRowView view = row as 
  DataRowView;
SampleData.SampleRow r = 
  view.Row as 
  SampleData.SampleRow;

After you make the proper conversions, you can examine the public properties of the row data and return the correct colors.

Finally, you need to create a column style and use a new object to determine the correct color brushes (see Figure 3 for the results):

c = new 
  DataGridTextBoxStyledColumn( 
  new FloatStatusRules() );

These five enhancements were designed so that you can add them to any project. They all work with the standard Windows Forms classes and interfaces, and they will go a long way toward making your data's presentation more compelling to your users. After you have used these enhancements a few times, add your own custom displays. I've shown that it's not difficult. The framework designers assumed you would enhance the classes provided.

About the Author

Bill Wagner, author of Effective C#, has been a commercial software developer for the past 20 years. He is a Microsoft Regional Director and a Visual C# MVP. His interests include the C# language, the .NET Framework and software design. Reach Bill at wwagner@srtsolutions.com.

comments powered by Disqus
Most   Popular
Upcoming Events

.NET Insight

Sign up for our newsletter.

Terms and Privacy Policy consent

I agree to this site's Privacy Policy.