Share via


Styling with the DataGridColumnStyle, Part 1

 

Chris Sano
Microsoft Developer Network (MSDN)

December 2004

Summary: Explains the rendering infrastructure of the Windows Forms DataGrid control, including the various resources that the DataGrid utilizes to display its data. (9 printed pages)

Download a sample, in both Visual Basic .NET and C#, of the code that is discussed in this article.

Contents

Introduction
Architecture
Configuring the Layout
The Column Styles
Painting
Conclusion

Introduction

The Windows Forms DataGrid control is a powerful, functionality-laden control with a very complicated architecture. There are a lot of different painting operations that come together to build the grid. In this two-part article, we will start from the bottom by developing an understanding of how the default DataGrid is rendered, and work our way up to using styles to improve on the data interaction and presentation capabilities of the control.

Architecture

A majority of the rendering functionalities of the DataGrid have been abstracted away, so in most cases, you don't really need to concern yourself with what goes on under the hood. However, if you're like me and are interested in understanding the underlying rendering foundation of the control, this part of the article covers that. Otherwise, you can move (or maybe you can mosey) on to Part 2 where the focus shifts towards customization.

The DataGrid is derived from System.Windows.Forms.Control, so it initially has all the rendering capabilities of a very basic control. When the control is first loaded on the form, the WndProc method in the base Control class handles all the painting-related system messages and calls upon the associated methods in the DataGrid class to perform certain actions For example, when a WM_PAINT message arrives, it is handled by Control.WndProc, which results in the invocation of the Control's private WmPaint method. This leads to the invocation of the DataGrid's OnPaint method and eventually to the raising of the Paint event. Let's zoom in on what happens between the time the DataGrid's OnPaint method is invoked and the Paint event is raised.

Configuring the Layout

To paint the DataGrid, various attributes associated with the grid layout such as the caption, parent rows, and column header regions are defined. The grid contains an internal nested data structure called LayoutData that is used to store these values. Default attribute values are established at runtime, and are checked—and changed if necessary—at various points throughout the grid's lifetime whenever the grid is deemed "dirty." In the process of initializing LayoutData, the DataGrid looks for a DataGridTableStyle that can represent the currently bound data. If one has not been set, a default instance-level DataGridTableStyle is created and its properties are lined up with the property values of the DataGrid (RowHeadersVisible, ParentRowsVisible, ColumnHeadersVisible, among others). It's important to note that the default DataGridTableStyle is not added to the DataGrid's GridTableStylesCollection; the only table styles that are added to the GridTableStylesCollection are those that are explicitly added to the collection by the developer.

The Column Styles

When the grid is bound to a data source and data member pair, a CurrencyManager object representing this binding is added to the hosting form's binding context. The currency manager has several different responsibilities, one of which includes managing currency (that is, keeping track of the current item) in the underlying IList representation of the data retrieved from the data source for all the controls that are bound to this particular pair.

When the default DataGridTableStyle is created, if a currency manager has been defined, the default DataGridColumnStyles are added to the DataGridTableStyle's GridColumnStylesCollection. First, the DataGrid needs to figure out how it's going to display the data contained by the currency manager. In order to do this, it calls the currency manager's GetItemProperties method, which uses a TypeDescriptor to obtain a collection of properties that exist within the data source referenced by the currency manager. TypeDescriptor uses .NET reflection to gather information about an object's properties. However, before delving into reflection, it will check and see whether the object has a custom property set. Custom properties are exposed through the ICustomTypeDescriptor interface as a collection of PropertyDescriptor-based objects.

If the data source is a DataTable, the currency manager contains a list of DataRowView objects. The DataRowView is one of several classes in the framework that implement ICustomTypeDescriptor. Through this interface, the DataRowView exposes a method named GetProperties that returns a PropertyDescriptorCollection representing an abstraction of custom-defined properties in the DataTable. When this PropertyDescriptorCollection is requested, the DataRowView invokes the internal GetPropertyDescriptorCollection method on the DataTable object that it holds a reference to. The DataTable holds a private collection of DataColumnPropertyDescriptor objects, which is returned to the caller.

The DataColumnPropertyDescriptor is derived from PropertyDescriptor. Its sole constructor expects a reference to a DataColumn. The class overrides several different members of PropertyDescriptor, most importantly, the PropertyType property. The overridden PropertyType returns the data type of the referenced data column, which is an important part of what I will be discussing next.

The property collection is enumerated and the property type of each PropertyDescriptor is checked. If the property type is of type bool, then a DataGridBoolColumn object is created and added to the DataGridTableStyle's GridColumnStylesCollection. Otherwise it's a DataGridTextBoxColumn that gets added to the collection.

The following block of code is a simple method implementation that depicts part of the column type detection process.

public void ListPropertyDescriptors( object dataSource, string dataMember ) {

  // BindingManagerBase is explicitly cast to a CurrencyManager. 
  CurrencyManager cm = ( CurrencyManager ) 
    dataGrid1.BindingContext[ dataSource, dataMember ];

  // Output the data type name of the current object in the list
  Debug.WriteLine( "Current object: " + cm.Current.GetType().ToString() );
  Debug.Write( Environment.NewLine );

  // Obtain the PropertyDescriptorCollection
  PropertyDescriptorCollection pdc = cm.GetItemProperties();

  // Iterate over the collection, checking the Name and PropertyType 
  // properties and determining the DataGridColumnStyle that will be
  // added to the GridColumnStylesCollection.
  foreach( PropertyDescriptor pd in pdc ) {

    Debug.WriteLine( "PropertyName: " + pd.Name );
    Debug.WriteLine( "PropertyType: " + pd.PropertyType );

    if ( pd.PropertyType.Equals( typeof( bool ) ) ) {
      Debug.WriteLine( "DataGridBoolColumn" );
    } else {
      Debug.WriteLine( "DataGridTextBoxColumn" );
    }

    Debug.Write( Environment.NewLine );

  }

}

Figure 1 shows a DataGrid embedded on a form. It has been bound to a data source containing the results of a query on the Authors table in the Pubs database.

Figure 1. A DataGrid embedded on a form

A small portion of the output of the code in the ListPropertyDescriptors method is shown below. Take note of how the column header text values are equal to the property names. This is because the HeaderText property of each column defaults to the value of the PropertyDescriptor's Name property.

Current Object: DataRowView

PropertyName: au_id
PropertyType: System.String
DataGridTextBoxColumn

PropertyName: au_fname
PropertyType: System.String
DataGridTextBoxColumn

PropertyName: au_lname
PropertyType: System.String
DataGridTextBoxColumn

PropertyName: contract
PropertyType: System.Boolean
DataGridBoolColumn

...

Next, we'll take a look at how the TypeDescriptor uses .NET reflection when a custom property set is not available. In the following code block, I create a custom type called HockeyPlayer that contains a few different properties, add a few instances of this to an IList-based collection, and bind the DataGrid to the list.

// custom data type
public class HockeyPlayer {
   
  ...

  public string Name { ... }
  public int Goals { ... }
  public int Assists { ... }
  public int Points { ... }
  public int PIM { ... }
  public string PlusMinus { ... }
  public bool IsActive { ... }

  public HockeyPlayer( string name, int goals, int assists, int points, 
    string plusMinus, int pim, bool isActive ) {
    
    // set the property values
    ...   

  }

}

...

// bind the data grid to a collection of hockey players
IList players = new ArrayList();

players.Add( new HockeyPlayer( "Johnson White", 18, 62, 80, "+51", 256 ) );
players.Add( new HockeyPlayer( "Marjorie Green", 34, 51, 85, "+19", 32 ) );
players.Add( new HockeyPlayer( "Cheryl Carson", 0, 1, 1, "0", 1010 ) );
...

dataGrid1.DataSource = players;

// Invoke the ListPropertyDescriptors method presented earlier
ListPropertyDescriptors( players, "" );

Figure 2 shows the result of the DataGrid being bound to the collection of HockeyPlayer objects. Since the HockeyPlayer class does not have a custom set of properties, .NET reflection traverses the code, looks for the type name, and digs out all of its public properties. The DataGrid then goes through each property, checks its type, creates a DataGridColumnStyle, and adds it to the GridColumnStylesCollection.

Figure 2. The result of the DataGrid being bound to the collection of HockeyPlayer objects

The following block shows the column style types that are assigned for each property as outputted by the ListPropertyDescriptors method. Again, note that the header text values are equal to the value returned by the PropertyDescriptor's Name property.

Current object: HockeyPlayer

PropertyName: Points
PropertyType: System.Int32
DataGridTextBoxColumn

PropertyName: PIM
PropertyType: System.Int32
DataGridTextBoxColumn

PropertyName: PlusMinus
PropertyType: System.String
DataGridTextBoxColumn

PropertyName: Name
PropertyType: System.String
DataGridTextBoxColumn

PropertyName: IsActive
PropertyType: System.Boolean
DataGridBoolColumn

...

Painting

All of the painting action in the grid originates in its OnPaint method. Through this method, the grid receives a reference to a PaintEventArgs object, which has two accessible properties, ClipRectangle and Graphics. The ClipRectangle property gives you a Rectangle object representing the clip region of the control. The Graphics parameter is a little bit tricky since the reference it holds onto is contingent upon the control style bits that the control has set. If the UserPaint and DoubleBuffer bits are set, indicating that double buffering support is preferred, you get a reference to a Graphics object belonging to an internal graphics buffer. Otherwise, you receive a reference to a Graphics object representing the GDI+ drawing surface on the display device. In the former case, you get a separate, nonvisible, off-screen canvas on which painting operations are performed. This is synchronized with the on-screen buffer once drawing has completed. In the latter case, painting operations are performed on the on-screen buffer as the runtime walks through the rendering instructions.

The DataGrid sets only the AllPaintingInWmPaint control style bit, so unless you extend it and set the DoubleBuffer and UserPaint bits, it will perform all of its rendering on the on-screen buffer as explained above.

When the grid is invalidated, its various painting operations utilize the data provided by the LayoutData structure to render the grid in individual pieces. Figure 3 shows a somewhat sketchy visual rendition of the various steps involved in rendering a basic grid.

Figure 3. The various steps involved in rendering a basic grid

The first stage of the grid's rendering process starts with a region that is equivalent in size and location to the control's clip region. The caption region is drawn along with its assigned text. This is followed by the rendering of the column headers. Once this has completed, focus is shifted to rendering the rows, one by one, cell by cell. The cells are rendered by accessing the column style collection and invoking the Paint method on each style. The rest of the grid is then painted, including the border and, if necessary, the scroll bars. Finally, the Paint event is raised to allow for further visual customization.

Conclusion

Part 1 of this article provides a thorough explanation of the rendering infrastructure of the Windows Forms DataGrid control. You had the opportunity to learn about and understand the various resources that the DataGrid utilizes to display its data. In Part 2 of this article, to be published in early January 2005, I will take a look at the different ways that the DataGrid can be adapted to meet your data interaction and presentation needs and guide you through the creation of several different custom column styles.

 

About the Author

Chris Sano is a Software Design Engineer with MSDN. When he's not working like mad on his next article, he can be found wreaking havoc with his hockey stick at the local ice rink. He welcomes you to contact him at csano@microsoft.com with any questions or comments that you may have about this article, or things that you'd like to see in future pieces.