Down the Rabbit Hole

 

Michael Weinhardt
www.mikedub.net

December 23, 2003

Summary: Michael Weinhardt takes over for Chris Sells and begins with a tour of the GridView control in Whidbey Windows Forms, including data binding, styles, custom cell formatting and painting, and extensibility. He also takes a quick look at nifty features such as automatic Windows themes support, column freezing, and column reordering. (18 printed pages)

Note This article is based on an alpha release of the .NET Framework, and all information contained herein is subject to change. This document was developed prior to the product's release to manufacturing and, as such, we cannot guarantee that any details included herein will be exactly the same as those found in the shipping product. The information represents the product at the time this document was printed and should be used for planning purposes only. Information subject to change at any time without prior notice

Download the winforms12182003.msi sample file.

From the moment I first saw "Whidbey," the next version of the .NET Framework and Visual Studio® .NET, I knew there was going to be a lot to explore and say about Windows Forms, and I wanted to say it. As fate would have it, Chris Sells approached me with an offer to take over this column at around the same time as he has focuses his energies on "Longhorn," the next release of the Windows® operating system. Suddenly, I found myself with a Neo-like choice. Take the blue pill and the story ends. Take the red pill, stay in Wonderland and see how deep the rabbit hole goes.

The Red Pill

A Technology Preview of Whidbey was provided to LA 2003 PDC attendees. If you were at the PDC, or have pored over the PDC materials (http://microsoft.sitestream.com/PDC2003/Default.htm) online, you're aware of the scope of the Whidbey release, which sees Microsoft fixing bugs, making modifications, incorporating community feedback, and adding a host of new features. The scope of these changes definitely encompasses Windows Forms, with the System.Windows.Forms assembly almost doubling in physical size, and more than doubling the number of public types. It's my intention to explore our beloved namespace and the rich set of supporting technologies from other namespaces that Windows Forms developers rely on, including ADO.NET, ClickOnce Deployment, and ObjectSpaces. Along the way, I reckon we can turn a few ASP.NET developers back from the dark side.

Enter the GridView

I view writing as the act transcribing the information stored in my brain into a document that effectively presents that information to the audience. My process for achieving this goal is quite simple:

  1. Compose the desired information into a Word document.
  2. Customize the document's formatting, styles, and layout to improve readability.
  3. Extend Word where native support does not suffice (custom styles, for example)/

This happens to be quite similar to my process for displaying data in the DataGrid control:

  1. Compose the desired data into a DataGrid.
  2. Customize the DataGrid's formatting, styles, and layout to improve readability.
  3. Extend the DataGrid where native support does not suffice, such as custom column/cell implementations.

Thanks to data binding, the DataGrid makes light work of tabular data composition. Customization, on the other hand, can require a little more effort depending on your requirements, as the DataGrid's feature-set supports a more broad brush stroke level of tailoring. Of course, more intricate customization scenarios are possible through hand-rolled code and extensibility features.

One of the major goals of Windows Forms in Whidbey is to increase developer productivity through richer functionality, an improved design-time experience, and less hand-written code. Consequently, Microsoft captured common DataGrid code customizations and combined them with community feedback on the DataGrid control to create System.Windows.Forms.GridView, a rich grid implementation exposed through a fine-grained set of properties, methods, and events that includes a host of nifty features. The GridView will see you customizing your data with more aplomb and in much less time than the DataGrid, which is still a great choice for displaying and navigating multi-level hierarchical data.

One type of UI that could certainly take advantage of the GridView is an at-a-glance view of data, where users need to quickly ascertain key information. The sample provides an example of this in the form of an employee contact list that displays a name, phone number, location, hire date, and photo for each employee, as shown in Figure 1.

Figure 1. Employee contact list in action

The Northwind database conveniently captures this information in the Employees table and while it contains more employee-related fields than we need, it is a great starting point for both an employee contact list and exploring the GridView control's composition, customization, and extensibility features.

Composing Data

The goal is to end up with the desired employee information composed into a GridView control. The first step is to drag a GridView control from the Toolbox onto a form, shown in Figure 2.

Figure 2. Adding a GridView to a Windows Form

The next step is to bind the GridView control to employee data by setting its DataSource and DataMember properties with the appropriate data source details. The GridView control provides a (New ...) option from its DataSource list to shortcut this process.

When selected, the Add New DataSource dialog appears (shown in Figure 3), allowing you to:

  • Create and select an appropriate database connection.
  • Select one or more tables, views, stored procedures, or user-defined functions as required, or select fields from within these options.
  • Give this new data source a name.

Figure 3. Configuring a data source

After making the appropriate choices, click Add to continue the process, which in this case displays the Primary Key Columns Missing dialog box that asks whether the designer should automatically create a primary key column since EmployeeID was not selected.

Click No since the Employees list is read-only and consequently doesn't need one. This completes the process and results in a typed data set being added to the project on your behalf. It takes its name from the Add New Datasource dialog's Datasource name field and exposes only the database objects you selected. The GridView's DataSource property is also set to the typed data set, leaving you to simply choose the appropriate data member, in this case Employees as illustrated in Figure 4.

Figure 4. Result of adding a new data source to the project

The typed data set has undergone some improvements of its own under Whidbey, particularly with respect to code generation. One such change allows typed data sets to be selected from the DataSource property of any data bound control. The Add New Datasource process does it for us, leaving us to choose the appropriate data member and complete the data binding process.

One benefit of using a typed data set for the data source is that it supplies the designer with enough metadata to automatically create a column for each field in the data member, resulting in Figure 5.

Figure 5. Design-time default GridView

Behind the scenes, the GridView by default assigns a GridViewCheckBoxColumn for a Boolean field type, a GridViewImageColumn for an image field type or a GridViewTextBoxColumn for any other type. GridViewButtonColumns and GridViewComboBoxColumns are also available, although you need to write code to create and add them to your GridView at runtime.

Typed Data Set DefaultInstance

At runtime, the typed data set needs to be loaded before its data can be rendered to the GridView. Another typed data set, code-generated enhancement, DefaultInstance, keeps this to one line of code:

public class Form1 : System.Windows.Forms.Form {
  ...
  private void Form1_Load(object sender, System.EventArgs e) {
    // Fill typed data set
    Northwind.DefaultInstance.Employees.LoadData();
  }
}

DefaultInstance is provided by Microsoft to simplify access to typed data sets, which are often used in one place in an application. DefaultInstance creates and manages an instance of the typed data set's generated root object, which shares the same name as the .xsd file:

public static Northwind DefaultInstance {
  get {
    // Create an instance the first time I'm called and retain reference in memory
    if ((_defaultInstance == null)) {
      _defaultInstance = new Northwind();
    }
    // Return instance
    return _defaultInstance;
  }
}

Composing your GridView from scratch using the (New ...) technique in conjunction with improvements to the typed data set, provides simple alternative to dragging data adapter components onto a form that you might have used in the past to enjoy the one line of code experience.

By the way, it is possible to achieve the same effect with no lines of code by placing the GridView inside the new DataContainer control. Unfortunately, there's not enough room this installment to give it the respect it deserves, but look for a discussion in the future.

Windows Themes Support, and It's Free

One thing you may notice about the runtime GridView is how its column headers look similar to Explorer or Outlook. This is due to the built-in GridView support for Windows Themes, such as Windows XP or Windows Classic, which are picked from the Display Properties dialog box, shown in Figure 6.

Figure 6. Picking a Windows Theme

The GridView dynamically alters its appearance to suit the selected theme. Developers may find Windows Themes support advantageous simply because they don't have to write a single line of code to enjoy it. A majority of the Whidbey controls provide such support, although the DataGrid does not.

Read-Only Configuration

We've come this far using only the default settings of the GridView. However, there are a host of features we can exploit to construct a more visually effective UI. We can start by ensuring the GridView is actually visual, and not interactive, which means configuring it for read-only operation because a GridView is read-write enabled by default.

The table below details the properties you should consider when configuring for read-write or read-only operation, plus the values I've set for read-only mode through the Property Browser.

Property Description Set To
ReadOnly Specifies whether users can edit cell values. True
EditCellOnEnter Automatically puts a cell into edit mode when selected if True, otherwise users must click F2. Not used if ReadOnly is true. False
AllowUserToAddRows When true, users can add a new row to the GridView, whether ReadOnly is true or false. If ReadOnly is true, the new row cannot be edited, which is not particularly useful. False
AllowUserToDeleteRows When true, users can delete the selected row(s), whether ReadOnly is true or false. False

Automatic/Manual Column Generation

Read-Only or not, the GridView's columns are quite bland from a formatting, alignment, and size perspective. To customize these types of settings, you need to access the GridView's property pages, which can be selected from the GridView's context menu, or from the Property Browser by clicking either the Property Pages icon or the GridView Properties link.

Then, click Columns to select the Columns property page, which contains the Create columns automatically at runtime check box, the Column list groupbox to manually select columns and the Column Properties groupbox to edit individual column properties.

Create columns automatically at runtime is checked by default for bound GridView controls although you can uncheck this for manual intervention. If you do, the designer automatically adds one entry to the Selected columns list for each column in the bound data member, resulting in Figure 7.

Figure 7. Manually selecting columns

The BoundColumns tree expands to a list containing all columns in the data source, as well as an All option to add the entire set at once. Since the GridView supports inter-mixed bound and unbound columns, you can expand the Unbound column types tree and select additional unbound columns to include in the Selected columns list, including the following: GridViewButtonColumn, GridViewCheckBoxColumn, GridViewComboBoxColumn, GridViewTextBoxColumn, and GridViewImageColumn. This process might be different in future versions of Whidbey as there are plans to improve the GridView's design-time experience, although the concepts discussed here should remain the same.

Column Properties

Once Create columns automatically at runtime is unchecked, click an item from the Selected columns list to edit its properties in the Column Properties group box, shown in Figure 8.

Figure 8. Column properties

The left margin of the Column Properties group box houses properties available to all GridView columns, while the property grid on the right is dedicated to column-type specific features. ColumnType is currently disabled in the PDC build of Whidbey, a situation that may also change as part the design-time improvements discussed earlier. However, Sort Mode does work and offers three settings, including Automatic (the default for most columns, excluding image and button columns), NotSortable, and Programmatic. Programmatic sorting instructs the GridView that you intend to write custom code to trigger and handle sorting. One technique for doing so would be to call the GridView's Sort method on a column header click:

private void employeesGridView_ColumnHeaderMouseClick(
  object sender, 
  System.Windows.Forms.GridViewCellMouseEventArgs e) {
    GridViewColumn   column = this.employeesGridView.Columns[e.ColumnIndex];
    this.employeesGridView.Sort(column, ListSortDirection.Ascending);
}

The Sort method has one override that allows a custom IComparer implementation to be passed in. However, this is not supported for bound GridViews because the underlying bound data must perform the sort to ensure binding consistency. If you like breaking the rules, expect to see a System.InvalidOperationException raised at runtime.

GridView Column Freezing

Each column also has a Frozen property that, when checked, ensures the column remains visible as users horizontally scroll the grid, much like the Freeze Panes feature in Microsoft Excel. This is a great way to make sure key information, such as LastName and FirstName, is always visible no matter how wide the GridView. Set the ExtraVerticalEdge value to highlight for the user exactly which columns are frozen.

GridView Column Reordering

Freezing columns is useful in some situations, while being able to reorder columns is practical in others, particularly when you need to compare columns side-by-side that may not reside together by default. You might have benefited from this type of feature in applications such as File Explorer and Microsoft Outlook®. You can enable this feature by clicking the General property page and checking AllowUserToOrderColumns. At runtime, users need to press the ALT key and drag a column to the desired location as shown in Figure 9. Additionally, column freezing is also displayed in the figure below.

Figure 9. Reordering columns at runtime

GridView Styles

Back on the Columns property page, you can set each column's style by clicking the Cell style ellipses button. This opens open the CellStyle Builder dialog box shown in Figure 10, which gives you access to various alignment, color, formatting, and font properties.

Figure 10. Setting column cell styles

This dialog also displays a preview of your settings in the Preview group box, for both unselected and selected cells. Figure 11 shows a few of these properties in action, including cell coloring, date formatting, and converting column header text to something more readable.

Figure 11. Column cell styles in action

Columns are not the only arbitrary cell grouping that can be configured through styles. Rows, cells, column headers, and row headers have the same support, although they are set through single properties using the Property Browser, detailed in the table below.

Style Description
GridView.ColumnHeadersDefaultCellStyle Only works if not themed. Applies to the header cells of all columns.
GridView.RowHeadersDefaultCellStyle Works, themed or not. Applies to the header cells of all rows.
GridView.DefaultCellStyle Applies to all cells. Does not apply to Column and Row Headers.
GridView.RowsDefaultCellStyle Applies to all cells in all rows, excluding Row Header cells.
GridView.AlternatingRowsDefaultCellStyle If used, always appears, but only on all cells in alternating rows, starting at row index 1, excluding Row Header cells.

Perhaps not immediately obvious is the order in which these styles are reapplied. One side effect of setting more than one of these styles is the possibility of one style being rendered over another. Figure 12 illustrates the order in which styles are applied if the styles differ from their default values. Note that both ColumnHeadersDefaultCellStyle and RowHeadersDefaultCellStyle are not affected.

Figure 12. Order of style rendering

GridView Selection

Each DefaultCellStyle has a SelectionBackColor and a SelectionForeColor property that comes into play when cells are selected. The GridView gives you five ways to select cells with the SelectionMode property:

Selection Mode Enum Value Description
CellSelect 0 Selects a range of one or more cells.
FullRowSelect 1 Selects the entire row that contains the selected cell.
FullColumnSelect 2 Selects the entire column that contains the selected cell. Prevents automatic column sorting.
RowHeaderSelect 3 Selects row if row header is clicked, single cell otherwise (Default).
ColumnHeaderSelect 4 Selects column if column header is clicked, single cell otherwise. Prevents automatic column sorting.

Combining selection modes is not recommended as SelectionMode simply takes the sum of the combined enumeration values, which can generate an InvalidEnumArgumentException if outside the range of possible values for GridViewSelectionMode.

Custom Cell Formatting

SelectionMode is a great example of a declarative feature that required code in the DataGrid. If you like to write code, you've probably been disappointed so far, but have no fear because there are still situations that require you to write custom code. For instance, you may want to highlight a subset of cells based on their values, such as changing the BackColor of HireDate cells to highlight employees with a specified term of service. Unfortunately, cell styles are not geared to support design-time setting in anticipation of arbitrary values. However, the GridView offers the CellFormatting event, which gives you access to the currently rendered cell's style and offers you the opportunity to change it before being rendered. You can use this hook to evaluate the cell's data and alter its style as appropriate:

private void employeesGridView_CellFormatting(
  object sender, 
  System.Windows.Forms.GridViewCellFormattingEventArgs e) {

  // Only paint if desired, formattable column
  if( e.ColumnIndex != this.employeesGridView.Columns["HireDate"].Index ) return;
  if( e.RowIndex < 0 ) return;

  // Only paint if text provided
  if( this.txtHighlight.Text.Trim() == "" ) return;
      
  // Only paint if desired text is in cell
  DateTime   cellValue = Convert.ToDateTime(e.Value);
  if( DateTime.Now.AddYears(int.Parse(this.txtHighlight.Text) * -1) > cellValue ) return;
    // Alter cell's BackColor
    e.CellStyle.BackColor = Color.LightCoral;
  }
}

This code generates Figure 13.

Figure 13. CellFormatting in action

Custom Cell Painting

Sometimes, though, you might like to paint your cells with panache. I personally favor the linear gradient fill that is so in vogue in Milan, and would love to see that look in my GridView. Doing so requires painting directly to a Graphics object with a LinearGradiantBrush, which is not directly accessible from the CellFormatting event. However, it is available from the CellPainting event, which can be used in the same fashion as CellFormatting, albeit for hard-core UI painting junkies. The following code shows a linear gradient equivalent to the CellFormatting event:

private void employeesGridView_CellPainting(
  object sender, 
  System.Windows.Forms.GridViewCellPaintingEventArgs e) {
    
  // Only paint if desired, formattable column
  ...
  
  // Create target painting rectangle
  Rectangle rect = new Rectangle(e.Bounds.X, e.Bounds.Y, e.Bounds.Width - 1, e.Bounds.Height - 1);
      
  // Render cell background
  using( LinearGradientBrush brush = new LinearGradientBrush(rect, 
    Color.White, Color.LightCoral, 0f) ) {
    e.Graphics.FillRectangle(brush, rect);
  }
      
  // Render cell border
  using( Pen   pen = new Pen(this.employeesGridView.GridColor) ) {
    e.Graphics.DrawRectangle(pen, e.Bounds.X - 1, e.Bounds.Y - 1, 
      e.Bounds.Width, e.Bounds.Height);
  }
}

Upon execution, you'll be surprised to find that your custom painting is not actually rendered. While the code executes, the GridView renders the cell's style after this event, thus overwriting your good work. You can prevent this from happening, though, by setting GridViewCellPaintingEventArgs.Cancel to true:

private void employeesGridView_CellPainting(object sender, 
  System.Windows.Forms.GridViewCellPaintingEventArgs e) {
  ...
  // Avoid cell's style painting
  e.Cancel = true;
}

The custom painting is now retained, but in another quirk of fate, the cell's value has disappeared. This is a consequence of setting Cancel to true, which prevents the GridView from rendering the cell's value, which it does at the same time it renders the cell's style. You will have to take responsibility for rendering the value yourself:

private void employeesGridView_CellPainting(object sender, 
  System.Windows.Forms.GridViewCellPaintingEventArgs e) {
  ...
  // Render cell value
  StringFormat format = new StringFormat();
  format.LineAlignment = StringAlignment.Center;
  format.Alignment = StringAlignment.Far;
  using( Brush valueBrush = new SolidBrush(e.Style.ForeColor) ) {
    e.Graphics.DrawString(cellValue.ToString(e.Style.Format), 
      e.Style.Font, valueBrush, rect, format);
  }
  ...
}

That works fine, but what happens when we want the GridView to render some aspect of the cell's style. For example, our logic doesn't cater for painting a selected cell and why should we when the GridView can take care of it for us? In this case, we need to return from the CellPainting event handler immediately if the cell is selected. We can use State property to determine selection by checking for GridViewElementState.Selected:

private void employeesGridView_CellPainting(object sender, 
  System.Windows.Forms.GridViewCellPaintingEventArgs e) {
  ...
  // Let the cell paint itself if selected
  if( (e.State & GridViewElementState.Selected) == GridViewElementState.Selected )
    return;
  ...
}

The final result of our foray into custom cell painting is demonstrated in Figure 14.

Figure 14. Complete custom cell painting in action

GridView Extensibility

Although handling either the CellFormatting or CellPainting events may be fine for one-off customizations, they are not appropriate when such logic needs to be reusable. For example, I'd like to display an employee's location by country, and think that an icon would be more visually appealing than text. It's also an implementation I would like to use in other applications down the road. The GridView comes with a strong extensibility framework, which you can leverage through derivation to create a reusable implementation. The first step is to create a custom cell that binds to the Country data field, from which we can render the appropriate flag image. While System.Windows.Forms.GridViewCell is the base GridView cell implementation, using it requires you to completely handle cell painting because the GridViewCell's Paint event is a virtual function that you must override. However, you can derive from one of the GridViewCell implementations, such as GridViewTextBoxCell, and let it do the hard work for you. The sample derives from GridViewImageCell to leverage its ability to display images. The complete GridViewFlagCell is listed here:

public class GridViewFlagCell : System.Windows.Forms.GridViewImageCell {
  protected override void GetValue(int rowIndex, out object value, ref string errorText) {  
    // Retrieve bound value
    object   boundValue;
    base.GetValue(rowIndex, out boundValue, ref errorText);
      
    // Bail if not provided
    if( boundValue == null ) {
      // Set the out value parameter
      value = null;
      return;
    }
          
    // Return the appropriate country icon          
    string country = boundValue.ToString();
    switch( country ) {
      case "USA":
        value = new Bitmap(this.GetType(), "FLAGUSA.ICO");
        break;
      case "UK":
        value = new Bitmap(this.GetType(), "FLAGUK.ICO");
        break;
      default:
        value = new Bitmap(this.GetType(), "FLAGNONE.ICO");
        break;
    }
  }
}

Unfortunately, the GridViewFlagCell won't render itself. It needs some way to be passed to the GridView at the appropriate moment. In the GridView infrastructure, a GridView column's CellTemplate property returns a copy of a GridViewCell appropriate to that column. The parent GridView queries this property as it renders each column of each row. This process is illustrated in Figure 15.

Figure 15. How a GridView retrieves appropriate cells for rendering

We can piggyback this process with the GridViewFlagCell by creating a custom GridView column to broker it to the GridView. Simply derive from System.Windows.Forms.GridViewColumn and ensure the custom column's CellTemplate value returns a GridViewFlagCell:

public class GridViewFlagColumn : System.Windows.Forms.GridViewColumn {
  public GridViewFlagColumn() {
    base.CellTemplate = new GridViewFlagCell();
  }
}

All that remains is to add the custom implementations to the GridView at runtime:

public class Form1 : System.Windows.Forms.Form {
  ...
  private void Form1_Load(object sender, System.EventArgs e) {
    
    // Create custom column
    GridViewFlagColumn clm = new GridViewFlagColumn();

    // Bind and configure
    clm.DataPropertyName = "Country";
    clm.HeaderText = "Country";
    clm.Width = 60;
    clm.Name = "Country";
    clm.ReadOnly = true;
    clm.SortMode = GridViewColumnSortMode.Automatic;
    clm.ValueType = typeof(Bitmap);
    clm.DefaultCellStyle.Alignment = GridViewContentAlignment.MiddleCenter;

    // Attach to GridView
    int index = this.employeesGridView.Columns["Photo"].Index - 1;
    this.employeesGridView.Columns.Insert(index, clm);
    ...
  }
}

Our efforts yield Figure 16.

Figure 16. Displaying countries as icons rather than textually

A nice feature of this implementation is that sorting still works as you would expect because it's performed by the underlying data source and, subsequently, the underlying cell values, not the custom rendered cell values.

Where Are We?

This installment took us on a whirlwind tour of the GridView's visual features, both declarative and programmatic. We created a bound GridView, configured its columns and styles, used some fun features like the Frozen and AllowUsersToOrderColumns properties, implemented row selection, and extended the implementation for complex customization scenarios. I've glossed over some features where they may change as new releases are issued, while others are left until the next installment which will cover the GridView's data handling capabilities that include navigation, editing, validation, and error handling.

Acknowledgements

Thanks to Chris Sells for showing me the path, Marc Wilson and everyone involved at Microsoft for letting me walk the path, and Kym Phillpotts for keeping me on the path.

Michael Weinhardt is a senior software engineer for SERF, a retirement fund management company, where he primarily designs and builds .NET applications. He has also written for MSDN Magazine and other publications. Michael is starting work on a book project focused on Whidbey Windows Forms, his current technological love. For a minimalist Web experience, visit www.mikedub.net.