Export (0) Print
Expand All
Build Providers for Windows Forms
Draft a Rich UI: Ground Rules for Building Enhanced Windows Forms Support into Your .NET App
A New Grid Control in Windows Forms
Owner-Drawing in .NET
P2P Comm Using Web Services
Smart Clients: Build A Windows Forms Control To Consume And Render WSRP Portlets
Spice It Up: Sprinkle Some Pizzazz on Your Plain Vanilla Windows Forms Apps
Synchronizing Multiple Windows Forms
Text Rendering: Build World-Ready Apps Using Complex Scripts In Windows Forms Controls
Windows Forms Controls: Z-order and Copying Collections
Winning Forms: Practical Tips For Boosting The Performance Of Windows Forms Apps
Code Samples
Expand Minimize

Styling with the DataGridColumnStyle, Part 2

 

Chris Sano
Microsoft Developer Network (MSDN)

January 2005

Summary: Demonstrates how to manipulate the appearance of the Windows Forms DataGrid control using custom column styles. (20 printed pages)

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


Contents

Customizing the DataGrid
Creating a Table Style
Subclassing the DataGridColumnStyle
Creating Custom Column Styles
   DataGridLabelColumn
   DataGridProgressBarColumn
   DataGridComboBoxColumn
   DataGridButtonColumn
Refactoring
Putting It All Together
Issues Encountered
Conclusion
Links of Interest
Acknowledgments

Customizing the DataGrid

The first part of this two-part article provides a thorough explanation of the rendering infrastructure of the Windows Forms DataGrid control. I explain how all of the grid's display attributes are managed through the internal LayoutData data structure and demonstrate how the default table and column styles are created when the grid is bound to a data source. Additionally, I show how everything comes together as the individual parts of the grid are painted during the rendering process.

In this part, I'll focus on detailing how to leverage that knowledge to further enhance the interactive and presentation capabilities of the control. I've broken this article down into several different sections. First, we'll take a look at how to customize the data that the DataGrid displays using the column styles that exist in the Framework. Second, I'll take you through the abstract methods in DataGridColumnStyle, providing insight into when and where the DataGrid calls into those methods. Third, and most excitingly, I'll show you how you can create custom styles.

Creating a Table Style

When a DataGrid is created, a GridTableStylesCollection is initialized with a reference to the DataGrid. During initialization, no table styles are added to the collection. If this remains true when the grid is first bound to a data source, a default table style is created and the DataGrid displays the data using default color, width, and formatting values that are defined through internal properties. All the properties obtained through the list to which the grid is bound are displayed as columns. If you want to customize the appearance of those properties, you will need to create a custom table style and add it to the table styles collection.

A custom table style contains an assortment of configurable properties that allow you to override the default rendering configuration of the grid. Column styles play a huge role in the process. Most of the functionality in the DataGridColumnStyle is tailored to controlling the appearance of individual columns on the screen. It also gives you complete control over the behavior of the column. You can determine whether or not you want the data to be editable and if so, how you want the column to behave while its data is being changed.

Creating a custom table style is a fairly straightforward process that involves declaring and initializing an object of type DataGridTableStyle and adding the column styles to its column styles collection. The table style needs to be mapped to a data source and each column in the column collection needs to be mapped to a property descriptor name that exists in the underlying list in the currency manager. (See Part 1 of this article for an explanation of how property descriptors work.)

The following code block demonstrates how to create a custom table style and provides a clearer explanation of how the mappings work. For this example, I'm going to pretend that I have a data table that contains all the records that reside in the Authors table in the Pubs database. This table has nine fields and I only want to show three: the author's first name, last name, and whether or not they have a contract. First, I create a table style and set its mapping name to the name of the data table, which is Authors. Next, I create column styles for each of the three fields that I want to show, set their mapping names to the field names and add them to the grid's column styles collection. When all the columns are in place, the table style is added to the grid's table style collection and the grid is bound to the data table.

// : This example assumes that you have a DataTable populated with records from 
// : the Authors table in the Pubs database.

DataGridTableStyle dgts = new DataGridTableStyle();
dgts.MappingName = "Authors";

// DataGridTextBoxColumn
DataGridTextBoxColumn cFirstName = new DataGridTextBoxColumn();
cFirstName.MappingName = "au_fname";
cFirstName.HeaderText = "First Name";

// Add the column style to the column style collection
dgts.GridColumnStyles.Add( cFName );

// DataGridTextBoxColumn
DataGridTextBoxColumn cLastName = new DataGridTextBoxColumn();
cLastName.MappingName = "au_lname";
cLastName.HeaderText = "Last Name";

// Add the column style to the column style collection
dgts.GridColumnStyles.Add( cLName );
// DataGridBoolColumn
DataGridBoolColumn cContract = new DataGridBoolColumn();
cContract.MappingName = "contract";
cContract.HeaderText = "Contract";

// Add the column style to the column style collection
dgts.GridColumnStyles.Add( cContract );

// Add the table style to the grid's table styles collection
dataGrid.TableStyles.Add( dgts );

// Bind the grid to the DataTable
dataGrid.DataSource = authorsTable;

Subclassing the DataGridColumnStyle

As I mentioned in Part 1 of this article, the framework provides you with only two DataGridColumnStyles: the DataGridBoolColumn and DataGridTextBoxColumn. If neither strikes your fancy, you can create a custom style that suits your needs. In order to do that, you need to subclass the abstract DataGridColumnStyle class. The following is a list of the eight methods that you need to implement.

void Abort(int rowNum)

Determines how the active column cell is to react to the user's request to interrupt the editing procedure. This usually involves rolling back the changes that have been made and invalidating the cell so that the original data is shown. There are several different ways that this method is called, usually as a result of keyboard input such as the user hitting the escape key, or control-z.

bool Commit(CurrencyManager dataSource, int rowNum)

Initiates a request to complete an editing procedure. The implementation should include logic that updates the cell with the new value and prepares the updated data to be pushed back to the property descriptor using the base class SetColumnValueAtRow method. When focus is taken away from the active cell, the DataGrid calls either this method or the Abort method that I just explained, depending on the user input. This method is called in just about every user input case that the Abort method isn't, such as the user hitting the return key, or clicking on another part of the grid.

void Edit(CurrencyManager source, int rowNum, Rectangle bounds, bool readOnly, string instantText, bool cellIsVisible)

When a DataGrid cell obtains focus, this method is called to prepare the cell for editing. You receive a reference to a CurrencyManager object which, when used in conjunction with the rowNum parameter in a call to the base class GetColumnValueAtRow method, will give you the value for the current cell. The bounds parameter defines the cell region relative to the DataGrid, which is useful if you need to position and/or size a control in the DataGrid control collection over the cell, which is akin to what the DataGridTextBoxColumn does. I'll be showing you how you can do something similar with the ComboBox control in the DataGridComboBoxColumn sample in the next section.

int GetMinimumHeight()

Returns the desired minimum height of the cells in the grid. When the DataGrid is initially rendered, this method is invoked on all of the column styles that reside in the current table style's GridColumnStyleCollection. The row height for all of the rows in the grid is then set to the maximum value returned.

int GetPreferredHeight(Graphics g, object value)

Returns the preferred cell height. When the user double-clicks on a row border, this method is invoked on all of the column styles that reside in the current table style's GridColumnStyleCollection. The cells in the row directly above the border will have their heights resized to the maximum value returned.
You receive two different parameters in this method. The value parameter returns the value for the current cell and the graphics reference can be used to help determine the height of the cell. You can invoke either the MeasureCharacterRegion or the MeasureString methods on the graphics object to get the exact height and width of a string based on the size of the grid font. I do not recommend performing any painting in the implementation of this method. If you find yourself needing to do so, invalidate a region of the grid equal to the current cell (use GetPreferredSize to get the width), set a flag if necessary, and implement the desired painting logic in the Paint method(s).

Size GetPreferredSize(Graphics g, object value)

Returns the preferred cell size. This method does the same thing as the GetPreferredHeight method, only it is invoked when the user double-clicks on a column border. The column to the left of the border is then set to the width of the returned Size object.
See the description for GetPreferredHeight above for an explanation of the parameters.
On a side note, I'm unsure why the class designer chose to name this method GetPreferredSize instead of GetPreferredWidth. It would have made more sense, especially given the fact that there is already a method that returns the preferred height. Of course, they could also have eliminated the GetPreferredHeight method altogether and channeled all cell size-related inquiries through this method.
I recommend invoking this method in situations where you need to check the preferred cell size because you get both the width and height attributes through the Size object that is returned.

void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum)

I haven't figured out why inheritors are required to implement this method, as there don't seem to be any instances in which the DataGrid calls it. The best thing to do here is to delegate to the method below, passing in false for alignToRight.

void Paint(Graphics g, Rectangle bounds, CurrencyManager source, int rowNum, bool alignToRight)

This is the second of three overloaded (and second of two abstract) Paint methods in DataGridColumnStyle. This method paints the cells in the column, one cell at a time. Each invocation of this method focuses on the region defined by the bounds parameter. You also receive a reference to a CurrencyManager object that, as I mentioned earlier, represents the binding and contains, among a myriad of things, the data that is to be displayed in the grid. By invoking the DataGridColumnStyle's GetColumnValueAtRow method, you can obtain the value for the current cell.

Now that you have what I hope is a good understanding of how everything works, let's start putting all of that to good use by creating some custom column styles.

Creating Custom Column Styles

I've prepared four different custom column styles, all of which will be used to build the mock-up download manager shown in Figure 1.

Figure 1. The mock-up download manager with the DataGridLabelColumn, DataGridProgressBarColumn, DataGridComboBoxColumn, and DataGridButtonColumn columns discussed below

In an attempt to avoid cluttering this article with large blocks of code, and because there's a lot of code involved in those styles, I've decided to show only the most important and relevant code. The rest can be found amongst the code files in the DownloadManager directory that is bundled into the executable that accompanies this article.

DataGridLabelColumn

The DataGridLabelColumn is a simple style that renders text in the defined cell region represented by the bounds parameter in the Paint method below. This is very similar to DataGridTextBoxColumn, only the text does not get selected when the cell becomes active and the user cannot change the value. When the DataGrid invokes the method, the text layout is configured using a StringFormat object and the text rendered via the Graphics object's DrawString method as seen in the following code block.

public class DataGridLabelColumn : DataGridColumnStyle {

  protected override void Paint(Graphics g, Rectangle bounds, CurrencyManager 
source, int rowNum, Brush 
    backBrush, Brush foreBrush, bool alignToRight){
         
    using ( StringFormat sf = new StringFormat() ) {
      
      sf.Alignment = StringAlignment.Far;
      sf.LineAlignment = StringAlignment.Center;
      sf.FormatFlags = StringFormatFlags.FitBlackBox;
   
      g.FillRectangle( backBrush, bounds );
         
      g.DrawString( 
        this.GetColumnValueAtRow( source, rowNum ).ToString(), 
        this.DataGridTableStyle.DataGrid.Font, 
        foreBrush, 
        bounds, 
        sf );

    }
      
  }
}

DataGridProgressBarColumn

The DataGridProgressBarColumn provides a way to track the progress of something. It does nothing more than take the column value, which must be between 0 and 100, and draws a visual representation of the progress.

This class contains three properties, ControlSize, Padding, and StretchToFit. ControlSize allows you to define the size of the control that is painted in the grid cell, which in this case is the progress bar. The default size, as defined in the constructor code below, is 80 by 10. Padding exposes an object of type DataGridColumnStylePadding, which is a simple data structure that allows you to define values representing the amount of padding that should be applied to the sides of the progress bar when it's rendered. StretchToFit is used to indicate whether the progress bar should shrink or stretch to fit the column width or remain at its defined width when the column is resized. The default value is true.

public class DataGridProgressBarColumn : DataGridColumnStyle {
  public DataGridProgressBarColumn() {
   
    this.Padding.SetPadding( 4, 8, 4, 8 );
    this.ControlSize = new Size( 80, 10 );
    this.StretchToFit = true;
    this.Width = this.GetPreferredSize( null, null ).Width;

  }
}

Also defined above is the Width property, which belongs to DataGridColumnStyle. Earlier, I mentioned that when cells are rendered by the DataGrid, a call is made to the column style's GetPreferredHeight method to get the preferred cell height. The value of the width property, which is set to a default value of 75, is used to determine the column width. Since the width of the progress bar is 80, plus the left and right padding, the progress bar will be truncated. By making a call to the GetPreferredSize method, we're able to obtain an updated width value, which adds the left and right padding values to the control's width.

There are two parts to drawing the progress bar. First, a call is made to the style's GetControlBounds method, which returns a rectangle object representing the defined bounds of the control within the column cell. This value is used to draw the outline of the progress bar. If the StretchToFit property has been set to true, the width of the control bounds is changed. Next, a second rectangle object, represented by fillRect, is created with a slightly smaller region used to display the progress portion of the progress bar. The width of this rectangle is fitted based on the value returned when querying the column value.

public class DataGridProgressBarColumn : DataGridColumnStyle {
  protected override void Paint(Graphics g, Rectangle bounds, CurrencyManager 
source, int rowNum, Brush 
    backBrush, Brush foreBrush, bool alignToRight){

    ...

    Rectangle controlBounds = this.GetControlBounds( bounds );

    // check the StretchToFit property to determine whether or not the progress bar should span
    // across the entire width of the cell.
    if ( StretchToFit ) {
      controlBounds.Width = bounds.Width - ( this.Padding.Left + this.Padding.Right );
      controlBounds.X = bounds.X + this.Padding.Left;
    } 

    // establish a rectangle object that is slightly smaller than the one 
represented by controlBounds.
    Rectangle fillRect = new Rectangle( 
      controlBounds.X + 2, 
      controlBounds.Y + 2,
      controlBounds.Width - 3,
      controlBounds.Height - 3 );

      // calculate how wide each index will be 
      double indexWidth = ( (  double ) fillRect.Width ) / 100; 
      
      // find out what the progress value is
      int cValue = ( int ) this.GetColumnValueAtRow( source, rowNum );
      
      // re-align the fill rectangle width
      fillRect.Width = ( int )( cValue * indexWidth );

      g.DrawRectangle( Pens.Black, controlBounds ); 
      g.FillRectangle( Brushes.Red, fillRect );

  }
}

DataGridComboBoxColumn

The DataGridComboBoxColumn addresses one of the most common questions pertaining to control styles involving the ComboBox. There are many great examples that show how to add a ComboBox object to the DataGrid's control collection and display it when the column cell is in edit mode, but there are none that show how to display combo boxes in cells that do not have focus.

When the column is declared, a combo box object is created and the desired property values are set. I've also subscribed to the SizeChanged event because it's important that I be notified of any changes that are made to the ComboBox's size for reasons demonstrated at the end of this article.

public class DataGridComboBoxColumn : DataGridColumnStyle {
  public DataGridComboBoxColumn() {
   
    // define the DataGridComboBox object that will be used by this column style
    m_comboBox = new DataGridComboBox();
    m_comboBox.DropDownStyle = ComboBoxStyle.DropDownList;
    m_comboBox.Visible = false;
    m_comboBox.SizeChanged += new EventHandler( ComboBox_SizeChanged );

    // set the DataGridComboBox's size to the desired ControlSize
    this.ControlSize = m_comboBox.Size;
    ...

  }
}

When a table style containing this column style is added to the grid's table style collection, the column style's SetDataGridInColumn method is invoked to enable the style to address any special processing needs that it may have. This is where the TextBox is added to the DataGrid's control collection in the DataGridTextBoxColumn class. The same was done with the ComboBox.

public class DataGridComboBoxColumn : DataGridColumnStyle {
  protected override void SetDataGridInColumn( DataGrid value ) {
   
    base.SetDataGridInColumn( value );
   
    // add the combo box to the grid's Control collection if it doesn't already exist
    if ( !value.Controls.Contains( m_comboBox ) ) {
      value.Controls.Add( m_comboBox );
    }

  }
}

When the DataGrid detects that the left mouse button has been clicked above one of its cells, it calls the Edit method of the column style that the cell belongs to. This is the only column style discussed in this article that implements the Edit method. The other columns have their data values edited, but not by way of the Edit method. In this method, I position the hosted ComboBox control at the location that controlBounds represents. If the cursor position falls within the controlBounds region, the ComboBox displays its drop-down position.

public class DataGridComboBoxColumn : DataGridColumnStyle {
  protected override void Edit(CurrencyManager source, int rowNum, Rectangle bounds, 
    bool readOnly, string instantText, bool cellIsVisible) {

      // get cursor coordinates
      Point p = this.DataGridTableStyle.DataGrid.PointToClient( Cursor.Position );
   
      // get control bounds
      Rectangle controlBounds = this.GetControlBounds( bounds );

      // get cursor bounds
      Rectangle cursorBounds = new Rectangle( p.X, p.Y, 1, 1 );

      // position combobox
      m_comboBox.SelectedIndex = ( int ) this.GetColumnValueAtRow( source, rowNum );
      m_comboBox.Location = new Point( controlBounds.X, controlBounds.Y );
      m_comboBox.Visible = true;

      // has the cursor come within the bounds of the combo box?
      if ( cursorBounds.IntersectsWith( controlBounds ) ) {
        m_comboBox.DroppedDown = true;
      }

    }
}

If the user chooses to abort the edit (see the explanation for the Abort method in the Subclassing the DataGridColumnStyle section above for more details), the ComboBox is hidden and the column is invalidated. Otherwise, the property descriptor representing the cell is updated with the value of the selected ComboBox item.

With all that out of the way, let's take a look at the rendering process. The combo box control in this column is drawn with the help of the ControlPaint class. This is a sealed class with an assortment of static methods that, when called, paint some of the basic Windows controls and their elements. One of the methods in this class is the DrawComboButton method, which draws a button with a down-pointing arrow glyph, similar to what you see at the right end of a ComboBox. Using this in conjunction with the DrawBorder3D method will give you a replica of the ComboBox as demonstrated through the following block of code.

// : This example assumes that you have a reference to a graphics object and
// : a rectangle object named controlBounds representing the region of the
// : control.
 
ControlPaint.DrawBorder3D( g, controlBounds, Border3DStyle.Sunken );

Rectangle buttonBounds = controlBounds;
buttonBounds.Inflate( -2, -2 );

ControlPaint.DrawComboButton( 
  g, 
  buttonBounds.X + ( controlBounds.Width - 20 ), 
  buttonBounds.Y, 
  16, 
  17, 
  ButtonState.Normal );

The only thing that's left to do is to add the text. The code below represents a partial implementation of the Paint method with the above ControlPaint code omitted. The cell value is retrieved and used to obtain the selected item value. A text layout is configured in similar fashion as in the DataGridLabelColumn style and the text is drawn in the defined text region. Note that I've used the MeasureString method to define the height of the text region. This ensures that the text will not wrap if it exceeds the width of the region.

public class DataGridLabelColumn : DownloadManagerColumnStyle {
  protected override void Paint(Graphics g, Rectangle bounds, CurrencyManager 
source, int rowNum, 
    Brush backBrush, Brush foreBrush, bool alignToRight){

    g.FillRectangle( new SolidBrush( Color.White ), bounds );

    StringFormat sf = new StringFormat();
    sf.Alignment = StringAlignment.Near;
    sf.LineAlignment = StringAlignment.Center;

    // get control bounds
    Rectangle controlBounds = this.GetControlBounds( bounds );
   
    // get cell value
    int cellValue = ( int ) this.GetColumnValueAtRow( source, rowNum );

    // get selected item value   
    string selectedItem = m_comboBox.Items[ cellValue ].ToString();

    Rectangle textRegion = new Rectangle( 
      controlBounds.X + 1,
      controlBounds.Y + 4,
      controlBounds.Width - 3,
      ( int ) g.MeasureString( selectedItem, m_comboBox.Font ).Height );
   
    g.DrawString( selectedItem, m_comboBox.Font, foreBrush, textRegion, sf );

    // draw combobox control using ControlPaint
    ...

  }
}

DataGridButtonColumn

The DataGridButtonColumn is a column of buttons. This style is different from the previous styles in a few ways. In addition to subscribing to the grid's MouseDown and MouseOver events, it raises an event when it detects that one of the buttons in the column has been clicked.

Because the DataGridColumnStyle does not provide a way to detect mouse events (it does expose an internal MouseDown method that is used by the DataGridBoolColumn style, but because it's an internal method, it's not available for our use), my only option was to subscribe to the grid's events. Since the SetDataGridInColumn method that I mentioned in the previous column style is the first place where the column style is made aware of its hosting grid, I chose to subscribe to the events here as seen below.

public class DataGridButtonColumn : DataGridColumnStyle {
  protected override void SetDataGridInColumn( DataGrid value ) {

    base.SetDataGridInColumn( value );
   
    // subscribe to DataGrid events
    this.DataGridTableStyle.DataGrid.MouseDown += new MouseEventHandler(DataGrid_MouseDown);
    this.DataGridTableStyle.DataGrid.MouseUp += new MouseEventHandler(DataGrid_MouseUp);

  }
}

When a user clicks on the DataGrid, it raises its MouseDown event, which means that the event is handled even if the column wasn't clicked. The first condition in the MouseDown handler below first checks to verify that the left button was pressed, then makes sure that a cell was clicked and if so, that the clicked cell belongs to a column of this style.

public class DataGridButtonColumn : DataGridColumnStyle {
  private void DataGrid_MouseDown( object sender, MouseEventArgs e ) {
   
    DataGrid.HitTestInfo hti = this.DataGridTableStyle.DataGrid.HitTest( e.X, e.Y );

    // check to ensure that:
    //  1. the left mouse button was pressed
    //  2. the hit test type is a cell
    //  3. the column is of type DataGridButtonColumn
    if ( e.Button == MouseButtons.Left && 
      hti.Type == DataGrid.HitTestType.Cell && 
      this.DataGridTableStyle.GridColumnStyles[ hti.Column ] is DataGridButtonColumn ) {

        ...
   
    }

  }
}

If the above condition is true, the cursor position needs to be confirmed. It wouldn't make much sense if the button were to be put into a depressed state if the user didn't click within the control bounds. Instead of putting together a complicated and lengthy logical check in which the X and Y coordinates are checked to make sure they fall within certain bounds, I decided to just create a 1 pixel by 1 pixel rectangle (cursorRect) based on the cursor location and use the Rectangle object's IntersectsWith method to see if there's an intersection.

public class DataGridButtonColumn : DataGridColumnStyle {
  private void DataGrid_MouseDown( object sender, MouseEventArgs e ) {
   
    ...

    if ( ... ) {


      Rectangle cursorRect = new Rectangle( e.X, e.Y, 1, 1 );
      Rectangle cellBounds = this.DataGridTableStyle.DataGrid.GetCellBounds
( hti.Row, hti.Column );


      Rectangle buttonBounds = this.GetControlBounds( cellBounds );
      
      if ( cursorRect.IntersectsWith( buttonBounds ) ) {
        
        m_depressedBounds = cellBounds;
        this.DataGridTableStyle.DataGrid.Invalidate( cellBounds );

      }
    }
  }
}

If the IntersectsWith method returns true, then the m_depressedBounds field is set to the cell bounds and the cell is invalidated. When the Paint method is invoked, the cell bounds represented by the bounds parameter are compared to the value stored in m_depressedBounds to determine the button state. If the bounds are equal, then the button is placed into a depressed state (the Pushed enumeration value). ControlPaint's DrawButton method is then used to render the button.

public class DataGridButtonColumn : DataGridColumnStyle {
  protected override void Paint(Graphics g, Rectangle bounds, CurrencyManager 
source, int rowNum, 
    Brush backBrush, Brush foreBrush, bool alignToRight){

      ...

      ButtonState bs = ButtonState.Normal;

      if ( m_depressedBounds != Rectangle.Empty && m_depressedBounds == bounds ) {
        bs = ButtonState.Pushed; 
      } 

      ControlPaint.DrawButton( g, controlBounds, bs );

      ...

  }
}

If the cursor position shows that the cursor was still hovering over the button region when the MouseUp event is raised, then the cell value is changed. However, because we do not have the advantage of having a CurrencyManager reference provided to us through the method header, the currency manager is retrieved through the grid's BindingContext property to allow for this change to be made. If there are any subscribers to the DataGridButtonColumn's Click event, a ButtonColumnEventArgs object is created with the row and column numbers and the Click event is raised.

public class DataGridButtonColumn : DataGridColumnStyle {
  private void DataGrid_MouseUp( object sender, MouseEventArgs e ) {

    ...

    // check to see if the cursor is within the bounds of the button
    if ( cursorRect.IntersectsWith( buttonBounds ) ) {

      object ds = this.DataGridTableStyle.DataGrid.DataSource;
      string dataMember = this.DataGridTableStyle.DataGrid.DataMember;

      // retrieve the currency manager object from the form's binding context
      CurrencyManager cm = ( CurrencyManager ) 
        this.DataGridTableStyle.DataGrid.BindingContext[ ds, dataMember ];

      string buttonValue = ( string ) this.GetColumnValueAtRow( cm, hti.Row );

      // retrieve the new value of the button text
      if ( buttonValue.ToLower().Equals( "start" ) ) {
       buttonValue = "Stop";
      } else {
       buttonValue = "Start";
      }

      // set the new button text value
      this.SetColumnValueAtRow( cm, hti.Row, buttonValue );

      // raise the Click event.
      if ( Click != null && Click.GetInvocationList().Length > 0 ) {
       Click( new ButtonColumnEventArgs( hti.Row, hti.Column ) );
      }

    }

    ...

  }
}

Refactoring

After reading through the code for the aforementioned column styles, you've probably noticed that there is an excessive amount of overlapping functionality. Many of the column styles contain barebones implementations of the abstract DataGridColumnStyle methods and there are multiple common properties and helper methods among the different style classes including ControlSize and GetControlBounds. To alleviate this, I decided to create a generic style class called DownloadManagerColumnStyle from which all of the column styles used in the download manager would derive. In addition to implementing all of the abstract methods in DataGridColumnStyle, I added the common properties and helper methods, as well as some additional properties that are used during the rendering process.

public class DownloadManagerColumnStyle : DataGridColumnStyle {

  // properties
  public DataGridColumnStylePadding Padding;
  public virtual int MinimumWidth;
  public virtual int MinimumHeight;
  public virtual Size ControlSize;
  public virtual ControlHorizAlignment ControlHorizAlignment;
  public virtual ControlVertAlignment ControlVertAlignment;

  // method
  protected virtual Rectangle GetControlBounds( Rectangle cellBounds );

  // implementation of all of the abstract DataGridColumnStyle methods
  ...

}

// example derivation
public class DataGridComboBoxColumn : DownloadManagerColumnStyle { }

This results in a cleaner implementation of the column styles. For an explanation of all of the members, please take a look at DownloadManagerColumnStyle.cs in the DownloadManager_Refactored directory. The updated column styles are also available in this directory.

Putting It All Together

All the column styles that were recently discussed are brought together to build a simple prototype application. Let's pretend that a download manager is being built for a new and revolutionary company that is planning to stream movies via broadband connections for viewing on personal computers. They want an application that contains a grid that allows their customers to manage and view the progress of the movies that they have selected to download.

This grid will contain a table style hosting a collection of four different column styles: a DataGridLabelColumn to display the movie title, a DataGridProgressBarColumn to display the download progress, a DataGridComboBoxColumn to select the optimal location to download from, and a DataGridButtonColumn to initiate, pause, or reset the download.

To simulate the functionality of the download manager seen in Figure 1 and to demonstrate the versatility of the custom column styles that I created, I put together a quick application that subscribes to the DataGridButtonColumn's Click event and uses a timer to update the download progress. You can view this by running the DownloadManager executable in the executables directory or building the code in the DownloadManager directory.

Issues Encountered

While writing the code, I ran into two problems that I thought were worthy of a few extra paragraphs.

Figure 2 shows a screenshot of a grid that was the victim of some unpleasant rendering behavior, commonly known as flickering. As a result of this, several cells were not completely drawn, which as you can probably imagine, becomes a very unpleasant experience for the user. To fix this issue, I had to enable double buffering by extending the DataGrid and setting its DoubleBuffer control style bit.

Figure 2. Results of flickering

One of the things that the DataGrid does not provide is a mechanism that allows you to control the width of the columns when they are being resized. In the case of custom styles, this often becomes a problem, as shown in Figure 3.

Figure 3. Overlapping columns

Notice how the movie title is cramped up and the combo boxes in the location column overlap the preceding column. To fix this, I overrode the grid's OnMouseDown, OnMouseMove, and OnMouseUp methods and used the values returned by the MinimumHeight and MinimumWidth properties of the DownloadManagerColumnStyle to determine the minimum height and width of the cells that are being resized. This results in a much cleaner presentation of the data and jettisons the possibility of the end user inadvertently distorting the grid.

The DownloadManager_Refactored executable in the executables directory shows a grid with the above changes implemented. Figure 4 shows the form that appears when running the application.

Figure 4. Grid issues fixed

You can see that the user can no longer shrink the column to a width that is smaller than the column's minimum width. Also, on the bottom of the form is a button that when clicked will expand the width of the grid's hosted ComboBox object. Because I've subscribed to the ComboBox's OnSizeChanged event, the column style is notified of the change and is able to make the necessary changes and invalidate itself so the newly resized combo boxes are rendered accordingly.

Conclusion

In the second part of this two-part article, I provide you with details on how to leverage the adaptability of the DataGrid to enhance the interaction and presentation capabilities of the control. In the scope of both parts, I've tried to explain how everything comes together from the creation of the LayoutData data structure all the way to the generation of the column styles that ultimately dictate how the grid is presented on the client.

Now that you're armed with this invaluable DataGrid erudition, I hope that you will put it to good use when working with the control in the future.

Links of Interest

The following list contains links to several different MSDN articles that provide additional examples of DataGrid customization.

If you're looking for a way to add designer support for your custom column styles, check out the EasyDataGrid at datagridcolumnstyles.net.

Acknowledgments

I'd like to extend a token of gratitude to Scott Berry, Mark Boulter, Mark Rideout, Chris Sells, Stephen Toub, and Michael Weinhardt for all of their time and endless patience in helping me pull together everything I needed to get this article out the door.

 

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.

Show:
© 2014 Microsoft