Share via


Building a Drop-Down Filter List for a DataGridView Column Header Cell

 

Karl Erickson
Microsoft Corporation

July 2006
Updated December 2007

Applies to:
   Microsoft® Visual Studio® 2005
   Microsoft® .NET Framework 2.0
   Microsoftâ Windowsâ Forms 2.0

 

Summary: The Microsoft® Windows Forms DataGridView control in Microsoft® Visual Studio® 2005 provides a grid experience similar to Microsoft® Excel, but does not provide the column filtering drop-down lists that Excel provides in its AutoFilter feature. However, the DataGridView can bind to data sources that provide filtering, such as the ADO.NET DataView. This article describes how to build a custom DataGridView column header cell that displays drop-down filter lists, by taking advantage of the filtering capabilities of the data source and the new BindingSource component.

Download code samples in C# and Visual Basic (128 KB) at theMicrosoft Download Center.

Contents

     Overview of Column Filtering
     Column Filtering and the DataGridView Control
     The DataGridViewAutoFilter Class Library
     Using the DataGridViewAutoFilter Library
     DataGridViewAutoFilterColumnHeaderCell Class Implementation Details
     DataGridViewAutoFilterTextBoxColumn Class Implementation Details
     Possible Enhancements
     Additional Resources

Overview of Column Filtering

The DataGridView control is an excellent tool for displaying several rows of tabular data. However, displaying a large amount of data in the DataGridView control at one time can make it difficult for users to find the information they need. One way to address this issue is to enable automatic sorting, so that users can sort by a particular column. Another way is to implement column filtering, so that users can display only the rows with a particular value in a particular column.

The DataGridView control already provides automatic sorting by enabling users to click the column header, assuming that the data source supports sorting. There is no built-in support for filtering, however. You can use a data source that supports filtering, but you have to provide your own user interface (UI) to enable users to affect the filtering.

Only some data sources support filtering. One typical approach is to bind a DataGridView control to a BindingSource component that is bound to a DataSet. This is the configuration created when you use the Visual Studio 2005 Windows Forms Designer to set up data binding. You can then set the BindingSource.Filter property in response to user input.

When you have space on your form, you can provide a user interface for setting column filter options using text boxes and combo boxes. But when space is limited or when column filtering is a common task in your application, you might want to provide column filtering drop-down lists directly on the column headers, like in the Excel AutoFilter feature. This is particularly useful when you display a dock-filled DataGridView in a child form.

Note

Note: For the remainder of this article, I will use the Excel term "AutoFilter" to refer to Excel-style column-filtering drop-down lists.

Column Filtering and the DataGridView Control

You may wonder why the DataGridView control doesn’t include an AutoFilter feature. The design goal of the DataGridView was to create a flexible grid control with all the most common features, while recognizing that almost everyone will have special needs that can't be satisfied by the common features. The control had to cover the basics, but also had to be easily customizable, both in terms of appearance and behavior—providing a big improvement over the DataGrid control it replaces. An AutoFilter feature is not a necessity, since you can provide UI elsewhere on a form to manage filtering. You can also implement an AutoFilter feature yourself, although this involves advanced customization.

You can customize the DataGridView control on a few different levels. For example, you can set style properties and handle paint events in your client application, or you can provide custom column and cell types, often by extending existing column and cell types in order to leverage existing functionality. Creating a new editing cell type that hosts an arbitrary Windows Forms control, for example, is relatively trivial when you extend the DataGridViewTextBoxCell class.

To implement an AutoFilter feature, you have to derive from the DataGridViewColumnHeaderCell class, which provides no editing control functionality. Extending it to display a drop-down list is more difficult than just tweaking the DataGridViewComboBoxCell class. Instead, you must do all the work of painting the drop-down button, hit-testing the mouse clicks, positioning and displaying the drop-down list, and responding appropriately to user selections.

The DataGridViewAutoFilter Class Library

The sample accompanying this article provides a demonstration implementation of the AutoFilter feature in the form of a DataGridViewAutoFilter library. This library contains a DataGridViewAutoFilterColumnHeaderCell class and a supporting column class. The sample is not a full-featured, general-purpose, bug-free, nor Microsoft-supported solution, but it does provide a groundwork, which you can modify or use to guide your own implementation.

Figure 1 shows the DataGridViewAutoFilter library in action.

Figure 1. DataGridViewAutoFilter

 

Figure 1. The AutoFilter feature implemented by using the DataGridViewAutoFilter library.

The features and dependencies of the sample are elaborated in the following sections to help you understand what you can use immediately and what you have to provide yourself. These are not exhaustive lists of the features and limitations, but they may help guide your decision to investigate further or seek another solution. Before using any of the sample code in production applications, be sure to thoroughly test all scenarios that you want to support.

Features

The sample DataGridViewAutoFilter library provides the following features:

  • Support for multi-column filtering. Each drop-down list displays all unique values in a column, taking the filters of other columns into consideration. If a column is filtered, its drop-down list displays the values that would appear in the column if it were not filtered, and the current filter value is selected.
  • Support for automatic sorting. A user can sort a column by clicking the header cell and the sorting glyph will appear next to the drop-down button.
  • Support for the following special filter options: (All), (Blanks), and (NonBlanks).
  • Support for adding the AutoFilter feature to applications at design time in Visual Studio 2005.

Features Not Implemented

The sample DataGridViewAutoFilter library does not provide some features that you may expect from using Excel, or that you may require for your particular application. The features not implemented include:

  • Sorting options in the drop-down list. Automatic sorting is supported, which makes this Excel feature unnecessary.
  • Additional special filter options, such as (Custom...) and (Top 10...). These options are outside the scope of this sample. All filtering is for a particular value or for one of the supported special filter options. If you need to provide complex filtering options, you can implement your own Custom AutoFilter dialog box, possibly similar to the one in Excel.
  • No support for filtering in unbound or virtual mode, or in bound mode with data sources that do not provide filtering. This functionality is outside the scope of this sample. If you cannot use an external data source that provides filtering, you must implement filtering yourself.
  • No support for arbitrary data formatting. The strings displayed in the drop-down list are retrieved through the DataGridViewCell.GetFormattedValue property using the column's cell style. You can use the DataGridViewCellStyle.Format property to indicate date or currency formatting. However, if you have special formatting needs, you must implement them yourself.

Dependencies

The sample DataGridViewAutoFilter library depends on four things:

  • The DataGridView.DataSource must be set to a BindingSource component.
  • The BindingSource component must be bound to an IBindingListView implementation.
  • The IBindingListView.SupportsFiltering property value must be true.
  • The IBindingListView.Filter property must support multi-column filtering.

These four requirements are fulfilled automatically when you use the Visual Studio 2005 Windows Forms Designer to set up data binding through ADO.NET. (For more information, see How to: Bind Data to the Windows Forms DataGridView Control Using the Designer in the MSDN2 library.) Alternatively, you can bind to any IBindingListView implementation that meets the requirements.

If you need to bind to an existing data source, you must bind the data source to a BindingSource component and bind the BindingSource to your DataGridView control. The dependency on the BindingSource component is necessary because of the BindingSource.RaiseListChangedEvents property. This property can be set to false to enable temporary modification of the filter without updating the data displayed in the DataGridView control. This is necessary to calculate the values that should appear in a drop-down list, even when the column itself is filtered. If you can provide similar functionality in an alternative data source, you can work around this dependency.

To support filtering, the BindingSource must be bound to an IBindingListView with a SupportsFiltering property value of true. This is the case when you bind to an ADO.NET object such as a DataSet (for example, when you set up data binding through the designer). Although the DataSet class does not implement IBindingListView itself, the BindingSource can retrieve a DataView from the appropriate DataTable within the DataSet. In this case, the BindingSource.Filter property maps to the DataView.RowFilter property, which makes use of the DataColumn.Expression property.

The IBindingListView interface does not dictate how its Filter property must be implemented, but any application that uses the Filter property must make certain assumptions about the format of the filter string. The sample DataGridViewAutoFilter library expects the IBindingListView.Filter property to accept filter strings for Boolean expressions using the syntax documented in the DataColumn.Expression topic in the .NET Framework managed reference documentation.

Using the DataGridViewAutoFilter Library

The DataGridViewAutoFilter library is a single assembly that you can reference in your projects to gain access to the DataGridViewAutoFilterColumnHeaderCell and DataGridViewAutoFilterTextBoxColumn classes. You can use these classes either programmatically or through the Windows Forms Designer in Visual Studio 2005.

To display the AutoFilter drop-down button in a header cell, a column's HeaderCell property must be set to an instance of the DataGridViewAutoFilterColumnHeaderCell class. You can set the HeaderCell property of specific columns programmatically. The Windows Forms Designer, however, does not let you set the HeaderCell property in a Properties window.

To support the designer experience, the DataGridViewAutoFilterTextBoxColumn is provided. In the designer, you can select this column type in the Edit Columns and Add Columns dialog boxes. The DataGridViewAutoFilterTextBoxColumn extends the DataGridViewTextBoxColumn in order to set the HeaderCell property to a new instance of the DataGridViewAutoFilterColumnHeaderCell class.

After you have added one or more AutoFilter header cells to your application, you may want to provide user feedback about the current filter state, and provide a way for users to show all rows. You can use the BindingSource component to retrieve the filtered row count and to remove the filter, but the DataGridViewAutoFilter cell and column classes provide this functionality in convenience methods.

You may also want to provide keyboard access to the drop-down list from your form code. In Excel, users can navigate to a cell containing a drop-down button, and then press ALT+UP ARROW or ALT+DOWN ARROW. In the DataGridView control, however, users cannot navigate to a header cell. Instead, you may want to enable ALT+UP/DOWN ARROW to display the drop-down list for whichever column contains the current cell.

The following procedures describe four usage scenarios for the DataGridViewAutoFilter classes. The first procedure describes how to add the AutoFilter feature to your Windows Forms application using the designer. The second procedure describes how to add the AutoFilter feature programmatically. The third procedure describes how to enhance your client application by displaying a filter status string and a Show All option. Finally, the fourth procedure describes how to enable users to display the drop-down list using the keyboard.

To add the AutoFilter feature to your application using the designer:

  1. In your Windows Application project, add a reference to the DataGridViewAutoFilter.dll assembly.
  2. Add a DataGridView control to a form and bind data to it using the Choose Data Source task on the control's smart tag. For more information, see How to: Bind Data to the Windows Forms DataGridView Control Using the Designer in the MSDN2 library.
  3. After the columns have been generated, on the control's smart tag, click Edit Columns.
  4. In the Edit Columns dialog box, select a column.
  5. In the Properties window in the dialog box, select the ColumnType property and choose DataGridViewAutoFilterTextBoxColumn from the drop-down list.
  6. Repeat steps 4 and 5 for all columns that you want to use the AutoFilter feature with.
  7. After you have completed column configuration, run your application and confirm that drop-down buttons appear in the headers of the columns you selected.

Figure 2 shows a sample of the Edit Columns dialog box being used to set the column type to DataGridViewAutoFilterTextBoxColumn.

Fig 2. The Edit Columns dialog box with the ColumnType property set to DataGridViewAutoFilterTextBoxColumn.

Figure 2. The Edit Columns dialog box with the ColumnType property set to DataGridViewAutoFilterTextBoxColumn.

To add the AutoFilter feature to your application programmatically:

  1. In your Windows Application project, add a reference to the DataGridViewAutoFilter.dll assembly.

  2. Add the following using statement to the top of your code file so that you don't have to qualify the DataGridViewAutoFilter class names.

  3. Add a DataGridView control to a form.

  4. Handle the DataGridView.BindingContextChanged event using either the designer or the event-hookup code shown. The event handler must be associated with the event before the DataSource property is set so that the event will occur as a result of data binding.

  5. Set the DataGridView.DataSource property. The following code assumes you have already created and initialized a BindingSource component named customersBindingSource, either through the designer, or in code. For more information on setting up data binding programmatically, see How to: Bind Data to the Windows Forms DataGridView Control in the MSDN2 library.

  6. In the BindingContextChanged event handler, set the DataGridViewColumn.HeaderCell property for the columns you want to affect. The following example code iterates through the columns in a DataGridView control and sets each header to a new DataGridViewAutoFilterColumnHeaderCell object.

  7. Run your application and confirm that drop-down buttons appear in the headers of the columns you selected.

To add a StatusStrip control that displays the filter status and a Show All option:

  1. Follow one of the previous procedures to add the AutoFilter feature to a DataGridView control.

  2. If you haven't already done so, add the following using statement to the top of your code file so that you don't have to qualify the DataGrideViewAutoFilter  class names.

        using DataGridViewAutoFilter;
    
  3. Add a StatusStrip control to your form named statusStrip1 and containing two ToolStripStatusLabel components named filterStatusLabel and showAllLabel.

  4. Configure the labels by using the following code or equivalent settings in the Properties window of the designer.

      filterStatusLabel.Text = "";
      filterStatusLabel.Visible = false;
      showAllLabel.Text = "Show &All";
      showAllLabel.Visible = false;
      showAllLabel.IsLink = true;
      showAllLabel.LinkBehavior = LinkBehavior.HoverUnderline;
    
  5. Handle the Click event for the showAllLabel component.

      // Add this code to the constructor or associate 
      // the handler with the event using the designer. 
      showAllLabel.Click += new EventHandler(showAllLabel_Click);
    
      // ...
    
      private void showAllLabel_Click(object sender, EventArgs e)
      {
          DataGridViewAutoFilterTextBoxColumn.RemoveFilter(dataGridView1);
      }
    
  6. Handle the DataGridView.DataBindingComplete event.

          // Add this code to the constructor or associate 
        // the handler with the event using the designer. 
        dataGridView1.DataBindingComplete += 
            new DataGridViewBindingCompleteEventHandler(
            dataGridView1_DataBindingComplete);
    
        // ...
    
        void dataGridView1_DataBindingComplete(object sender,
            DataGridViewBindingCompleteEventArgs e)
        {
            String filterStatus = DataGridViewAutoFilterColumnHeaderCell
                .GetFilterStatus(dataGridView1);
            if (String.IsNullOrEmpty(filterStatus))
            {
                showAllLabel.Visible = false;
                filterStatusLabel.Visible = false;
            }
            else
            {
                showAllLabel.Visible = true;
                filterStatusLabel.Visible = true;
                filterStatusLabel.Text = filterStatus;
    
        }
    
  7. Run your application and apply a filter.

Figure 3 shows a sample of the resulting StatusStrip after a filter has been applied.

Fig 3. A StatusStrip control displaying the filter status and a Show All option.

Figure 3. A StatusStrip control displaying the filter status and a Show All option.

To enable users to display the drop-down list using the keyboard:

  1. If you haven't already done so, add the following using statement to the top of your code file so that you don't have to qualify the DataGridViewAutoFilter class names.

      using DataGridViewAutoFilter;
    
  2. Handle the DataGridView.KeyDown event.

            // Add this code to the constructor.
        this.dataGridView1.KeyDown += new 
            KeyEventHandler(dataGridView1_KeyDown);
    
    
        // ...
    
        void dataGridView1_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Alt && (e.KeyCode == Keys.Down || e.KeyCode == Keys.Up))
            {
                DataGridViewAutoFilterColumnHeaderCell filterCell =
                    this.dataGridView1.CurrentCell.OwningColumn.HeaderCell as 
                    DataGridViewAutoFilterColumnHeaderCell;
                if (filterCell != null)
                {
                    filterCell.ShowDropDownList();
                    e.Handled = true;
                }
            }
        }
    
  3. Run your application and press ALT+UP ARROW or ALT+DOWN ARROW to open the drop-down list for the current column. Press ESC to close the drop-down list.

DataGridViewAutoFilterColumnHeaderCell Class Implementation Details

Now that you know how to use the DataGridViewAutoFilter library and know some of its capabilities, the rest of this article is devoted to describing the details of how the DataGridViewAutoFilter library works.

The DataGridViewAutoFilter library might meet your requirements as it is, making it unnecessary to know any of the implementation details. However, you might want to customize or extend its capabilities to better serve your needs, or you might discover a bug that you want to fix. Additionally, understanding the implementation will help if you want to create other custom header cell types for the DataGridView control.

The primary class in the DataGridViewAutoFilter library is DataGridViewAutoFilterColumnHeaderCell class. This class derives from the DataGridViewColumnHeaderCell class.

The DataGridViewAutoFilterColumnHeaderCell implementation details can be divided into four categories:

  • Initialization.
  • Displaying the drop-down button.
  • Displaying, hiding, and handling user interaction with the drop-down list.
  • Filtering the bound data when the user selects a filter from the list.

The following sections describe these categories in more detail and provide example code to illustrate various points. The example code is excerpted from the accompanying DataGridViewAutoFilter library, but some details are left out for brevity. For the full details, see the complete source code in the sample.

Initialization

The primary initialization task is to create a ListBox control for use as a drop-down list. The DataGridViewAutoFilter library uses a single ListBox-derived control for all AutoFilter header cells in an application, storing it in a static variable called dropDownListBox. Using a single instance is possible because only one drop-down list is displayed at a time. It is also possible because the contents, size, and location of the drop-down list are easiest to determine at the time the list is displayed, since they are affected by frequent changes to a DataGridView control such as filtering, resizing, and scrolling.

The Windows Forms ListBox control is used because it provides the appearance and behavior of an Excel AutoFilter list, including a vertical scroll bar to navigate long lists. In contrast, the ToolStripDropDown control is not used because it lacks a vertical scroll bar, instead using up and down buttons in line with its typical usage as a menu. The ComboBox control is not used because its drop-down list cannot easily be displayed without displaying the text box part of the control.

The FilterListBox Class

The FilterListBox control extends the ListBox control to ensure that it receives keyboard messages that would normally be intercepted by the parent DataGridView control. Additionally, the FilterListBox class changes a few ListBox property settings to configure the control for use as a drop-down list.

The FilterListBox class overrides the protected IsInputKey and ProcessKeyMessage methods in order to intercept all keystrokes except those handled by the operating system, such as ALT+F4. The Control.ProcessKeyMessage method normally dispatches messages to the ProcessKeyPreview method of the control's parent. If the control's parent is not interested in the message, it goes to the control's own ProcessKeyEventArgs method in order to generate keyboard events.

Unfortunately, the DataGridView.ProcessKeyPreview method intercepts all keyboard messages unless they are for an editing control hosted by an ordinary (non-header) cell in edit mode. (For more information about hosting editing controls, see How to: Host Controls in Windows Forms DataGridView Cells in the MSDN2 library.) For keyboard handling, the DataGridView control ignores all other child controls. As a result, keystrokes that you expect the hosted control to handle are handled by the DataGridView control instead.

To prevent this behavior, the ProcessKeyMessage override in the FilterListBox class skips the parent-control processing and instead sends the keyboard message directly to the ProcessKeyEventArgs method. The ProcessKeyEventArgs method then raises FilterListBox keyboard events that the DataGridViewAutoFilterColumnHeaderCell class can handle.

The source code for the FilterListBox class follows:

  private class FilterListBox : ListBox
  {
      public FilterListBox()
      {
          Visible = false;
          IntegralHeight = true;
          BorderStyle = BorderStyle.FixedSingle;
          TabStop = false;
      }

      protected override bool IsInputKey(Keys keyData)
      {
          return true;
      }

      protected override bool ProcessKeyMessage(ref Message m)
      {
          return ProcessKeyEventArgs(ref m);
      }
  }

The Constructors and the Clone Method

The DataGridViewAutoFilterColumnHeaderCell class provides an empty default constructor and a constructor overload that takes an existing DataGridViewColumnHeaderCell instance. The constructor overload copies the property values of the specified cell to the new instance. This is useful when you want to replace an existing header cell with an AutoFilter cell, but keep all other header details the same.

The Clone method also uses the constructor overload to create a new instance with the same property values as an existing instance. For this reason, the constructor overload determines whether the specified column header cell is also an AutoFilter cell so that the AutoFilter-specific property values can be copied.

The DataGridView control clones columns and their header cells when the DataSource changes and a new set of columns must be automatically generated. The clones are used for any columns in the new schema that match columns in the old schema. That way you do not have to reconfigure columns that are the same in multiple data sources.

For this reason, it is important to make sure that you override the Clone method when you extend a DataGridView cell or column type and copy any public property values that you add.

The source code for the non-default class constructor and the Clone method follows:

  public DataGridViewAutoFilterColumnHeaderCell(DataGridViewColumnHeaderCell oldHeaderCell)
  {
      this.ContextMenuStrip = oldHeaderCell.ContextMenuStrip;
      this.ErrorText = oldHeaderCell.ErrorText;
      this.Tag = oldHeaderCell.Tag;
      this.ToolTipText = oldHeaderCell.ToolTipText;
      this.Value = oldHeaderCell.Value;
      this.ValueType = oldHeaderCell.ValueType;
      if (oldHeaderCell.HasStyle)
      {
          this.Style = oldHeaderCell.Style;
      }

      DataGridViewAutoFilterColumnHeaderCell filterCell =
          oldHeaderCell as DataGridViewAutoFilterColumnHeaderCell;
      if (filterCell != null)
      {
          this.FilteringEnabled = filterCell.FilteringEnabled;
          this.AutomaticSortingEnabled = filterCell.AutomaticSortingEnabled;
          this.DropDownListBoxMaxLines = filterCell.DropDownListBoxMaxLines;
          this.currentDropDownButtonPaddingOffset = 
              filterCell.currentDropDownButtonPaddingOffset;
      }
  }

  public override object Clone()
  {
      return new DataGridViewAutoFilterColumnHeaderCell(this);
  }

The OnDataGridViewChanged Method

An AutoFilter header cell is not part of a DataGridView control at the time it is created, but only after it has been added to a column contained in a DataGridView control. When this occurs, the OnDataGridViewChanged method is called. The DataGridViewAutoFilterColumnHeaderCell class overrides this method to provide initialization that cannot occur before the DataGridView control is available.

When an AutoFilter header cell gains access to the DataGridView control that contains it, it can access the control's data source and confirm that the column type is appropriate for filtering and sorting. For example, if the column is an Image column, the cell's FilteringEnabled property is set to false and the drop-down button is not displayed.

The OnDataGridViewChanged method also ensures that the DataGridView control is properly configured. First, it makes sure that the control's SortMode property is not set to Automatic. Normally, automatic sorting occurs when the user clicks anywhere on a column header cell. For AutoFilter cells, however, automatic sorting must not occur when the user clicks the portion of the cell occupied by the drop-down button. For this reason, sorting must be implemented manually and the SortMode property must never be set to Automatic.

The OnDataGridViewChanged method also calls the VerifyDataSource method to check whether the DataGridView control is bound to a valid data source. The control is not required to have a valid DataSource property value at this time, but if it does, the VerifyDataSource method confirms that it is a BindingSource, and throws an exception if it isn't.

In addition to verifying the data source, the OnDataGridViewChanged method associates event handlers with various DataGridView events that affect the display of the drop-down button and list or the state of the data source. This method also initializes the drop-down button bounds so that any initial column autosizing will accommodate the button width.

The source code for the OnDataGridViewChanged method follows:

  protected override void OnDataGridViewChanged()
  {
      if (this.DataGridView == null) return;

      if (OwningColumn != null)
      {
          if (OwningColumn is DataGridViewImageColumn ||
          (OwningColumn is DataGridViewButtonColumn && 
          ((DataGridViewButtonColumn)OwningColumn)
              .UseColumnTextForButtonValue) ||
          (OwningColumn is DataGridViewLinkColumn && 
          ((DataGridViewLinkColumn)OwningColumn).UseColumnTextForLinkValue))
          {
              AutomaticSortingEnabled = false;
              FilteringEnabled = false;
          }

          if (OwningColumn.SortMode == DataGridViewColumnSortMode.Automatic)
          {
              OwningColumn.SortMode = 
                  DataGridViewColumnSortMode.Programmatic;
          }
      }

      VerifyDataSource();
      HandleDataGridViewEvents();
      SetDropDownButtonBounds();
      base.OnDataGridViewChanged();
 }

Displaying the Drop-Down Button

The drop-down button is not an actual Button control. A Button control is not necessary since the DataGridViewCell type provides an OnMouseDown method that can be overridden to handle mouse clicks. The AutoFilter cell just has to paint a button and check for clicks in the appropriate part of the header.

The Paint Method

The Paint method is responsible for painting the drop-down button in the header cell. Several factors influence the appearance of the drop-down button:

  • The size of the drop-down button is based on the height of the header text.
  • The location of the drop-down button is based on the location of the header cell and the value of the DataGridView.RightToLeft property.
  • If the application is currently themed, the drop-down button is also themed.
  • If dropDownListBox is currently visible, the drop-down button is shown in its pressed state.
  • If the column is currently filtered, the drop-down button is highlighted.
  • If the cell's FilteringEnabled property is false, the drop-down button is not displayed.

These factors can change while an application is running, so most of them must be determined at the time the Paint method is called. The size and location may remain relatively constant, however, so these values are stored in a DropDownButtonBounds property and reused as long as they do not change. The DropDownButtonBounds property also indicates the region in which a user can click the header cell to display the drop-down list.

The button's size and location must change whenever its header cell changes size or location. This can result from user actions such as horizontally scrolling the DataGridView control, resizing its columns or column headers, and resizing the control itself (for example, resizing a form that contains a dock-filled DataGridView control). As described earlier, the OnDataGridViewChanged method attaches handlers to various DataGridView events. The handlers for events related to scrolling and resizing call the InvalidateDropDownButtonBounds method, which just sets the button bounds to Rectangle.Empty.

When the Paint method is called, if the DropDownButtonBounds value is Empty, the SetDropDownButtonBounds method is called to initialize the DropDownButtonBounds value.

The source code for the Paint method follows:

  protected override void Paint(
      Graphics graphics, Rectangle clipBounds, Rectangle cellBounds, 
      int rowIndex, DataGridViewElementStates cellState, 
      object value, object formattedValue, string errorText, 
      DataGridViewCellStyle cellStyle, 
      DataGridViewAdvancedBorderStyle advancedBorderStyle, 
      DataGridViewPaintParts paintParts)
  {
      // Use the base method to paint the default appearance. 
      base.Paint(graphics, clipBounds, cellBounds, rowIndex, 
          cellState, value, formattedValue, 
          errorText, cellStyle, advancedBorderStyle, paintParts);

      // Continue only if filtering is enabled and ContentBackground is 
      // part of the paint request. 
      if (!FilteringEnabled || 
          (paintParts & DataGridViewPaintParts.ContentBackground) == 0)
      {
          return;
      }

      // Retrieve the current button bounds. 
      Rectangle buttonBounds = DropDownButtonBounds;

      // Continue only if the buttonBounds is big enough to draw.
      if (buttonBounds.Width < 1 || buttonBounds.Height < 1) return;

      // Paint the button manually or using visual styles if visual styles 
      // are enabled, using the correct state depending on whether the 
      // filter list is showing and whether there is a filter in effect 
      // for the current column. 
      if (Application.RenderWithVisualStyles)
      {
          ComboBoxState state = ComboBoxState.Normal;

          if (dropDownListBoxShowing)
          {
              state = ComboBoxState.Pressed;
          }
          else if (filtered)
          {
              state = ComboBoxState.Hot;
          }
          ComboBoxRenderer.DrawDropDownButton(
              graphics, buttonBounds, state);
      }
      else
      {
          // Determine the pressed state in order to paint the button 
          // correctly and to offset the down arrow. 
          Int32 pressedOffset = 0;
          PushButtonState state = PushButtonState.Normal;
          if (dropDownListBoxShowing)
          {
              state = PushButtonState.Pressed;
              pressedOffset = 1;
          }
          ButtonRenderer.DrawButton(graphics, buttonBounds, state);

          // If there is a filter in effect for the column, paint the 
          // down arrow as an unfilled triangle. If there is no filter 
          // in effect, paint the down arrow as a filled triangle.
          if (filtered)
          {
              graphics.DrawPolygon(SystemPens.ControlText, new Point[] {
                  new Point(
                      buttonBounds.Width / 2 + 
                          buttonBounds.Left - 1 + pressedOffset, 
                      buttonBounds.Height * 3 / 4 + 
                          buttonBounds.Top - 1 + pressedOffset),
                  new Point(
                      buttonBounds.Width / 4 + 
                          buttonBounds.Left + pressedOffset,
                      buttonBounds.Height / 2 + 
                          buttonBounds.Top - 1 + pressedOffset),
                  new Point(
                      buttonBounds.Width * 3 / 4 + 
                          buttonBounds.Left - 1 + pressedOffset,
                      buttonBounds.Height / 2 + 
                          buttonBounds.Top - 1 + pressedOffset)
              });
          }
          else
          {
              graphics.FillPolygon(SystemBrushes.ControlText, new Point[] {
                  new Point(
                      buttonBounds.Width / 2 + 
                          buttonBounds.Left - 1 + pressedOffset, 
                      buttonBounds.Height * 3 / 4 + 
                          buttonBounds.Top - 1 + pressedOffset),
                  new Point(
                      buttonBounds.Width / 4 + 
                          buttonBounds.Left + pressedOffset,
                      buttonBounds.Height / 2 + 
                          buttonBounds.Top - 1 + pressedOffset),
                  new Point(
                      buttonBounds.Width * 3 / 4 + 
                          buttonBounds.Left - 1 + pressedOffset,
                      buttonBounds.Height / 2 + 
                          buttonBounds.Top - 1 + pressedOffset)
              });
          }
      }

  }

The SetDropDownButtonBounds and AdjustPadding Methods

The SetDropDownButtonBounds method initializes the DropDownButtonBounds value. The drop-down button size is based on the preferred header cell height for a single line of header text. The button location is aligned with the bottom right corner of the cell, or the bottom left in right-to-left environments. Both the size and location are adjusted to provide a visual offset that varies depending on whether visual styles are enabled.

The SetDropDownButtonBounds method also calls the AdjustPadding method to modify the DataGridViewCellStyle.Padding property in effect for the cell, based on the drop-down button width. The padding adjustment enables the DataGridView control to account for the drop-down button width when resizing columns automatically and when displaying the sorting glyph. Because of the Padding adjustment, the cell's GetPreferredSize method does not have to be overridden to customize the automatic sizing.

The source code for the SetDropDownButtonBounds and AdjustPadding methods follows:

  private void SetDropDownButtonBounds()
  {
      // Retrieve the cell display rectangle, which is used to 
      // set the position of the drop-down button. 
      Rectangle cellBounds = 
          this.DataGridView.GetCellDisplayRectangle(
          this.ColumnIndex, -1, false);

      // Initialize a variable to store the button edge length,
      // setting its initial value based on the font height. 
      Int32 buttonEdgeLength = this.InheritedStyle.Font.Height + 5;

      // Calculate the height of the cell borders and padding.
      Rectangle borderRect = BorderWidths(
          this.DataGridView.AdjustColumnHeaderBorderStyle(
          this.DataGridView.AdvancedColumnHeadersBorderStyle,
          new DataGridViewAdvancedBorderStyle(), false, false));
      Int32 borderAndPaddingHeight = 2 +
          borderRect.Top + borderRect.Height +
          this.InheritedStyle.Padding.Vertical;
      Boolean visualStylesEnabled =
          Application.RenderWithVisualStyles &&
          this.DataGridView.EnableHeadersVisualStyles;
      if (visualStylesEnabled) 
      {
          borderAndPaddingHeight += 3;
      }

      // Constrain the button edge length to the height of the 
      // column headers minus the border and padding height. 
      if (buttonEdgeLength >
          this.DataGridView.ColumnHeadersHeight -
          borderAndPaddingHeight)
      {
          buttonEdgeLength =
              this.DataGridView.ColumnHeadersHeight -
              borderAndPaddingHeight;
      }

      // Constrain the button edge length to the
      // width of the cell minus three.
      if (buttonEdgeLength > cellBounds.Width - 3)
      {
          buttonEdgeLength = cellBounds.Width - 3;
      }

      // Calculate the location of the drop-down button, with adjustments
      // based on whether visual styles are enabled. 
      Int32 topOffset = visualStylesEnabled ? 4 : 1;
      Int32 top = cellBounds.Bottom - buttonEdgeLength - topOffset;
      Int32 leftOffset = visualStylesEnabled ? 3 : 1;
      Int32 left = 0;
      if (this.DataGridView.RightToLeft == RightToLeft.No)
      {
          left = cellBounds.Right - buttonEdgeLength - leftOffset;
      }
      else
      {
          left = cellBounds.Left + leftOffset;
      }

      // Set the dropDownButtonBoundsValue value using the calculated 
      // values, and adjust the cell padding accordingly.  
      dropDownButtonBoundsValue = new Rectangle(left, top, 
          buttonEdgeLength, buttonEdgeLength);
      AdjustPadding(buttonEdgeLength + leftOffset);
  }

  private void AdjustPadding(Int32 newDropDownButtonPaddingOffset)
  {
      // Determine the difference between the new and current 
      // padding adjustment.
      Int32 widthChange = newDropDownButtonPaddingOffset - 
          currentDropDownButtonPaddingOffset;

      // If the padding needs to change, store the new value and 
      // make the change.
      if (widthChange != 0)
      {
          // Store the offset for the drop-down button separately from 
          // the padding in case the client needs additional padding.
          currentDropDownButtonPaddingOffset = 
              newDropDownButtonPaddingOffset;
        
          // Create a new Padding using the adjustment amount, then add it
          // to the cell's existing Style.Padding property value. 

          Padding dropDownPadding = new Padding(0, 0, widthChange, 0);
          this.Style.Padding = Padding.Add(
              this.InheritedStyle.Padding, dropDownPadding);
      }
  }

Displaying, Hiding, and Handling User Interaction With the Drop-Down Filter List

The drop-down list is normally displayed when the user clicks the drop-down button. The OnMouseDown method is responsible for handling user mouse clicks on the column header. If the user clicks the column header but the mouse click is not within the bounds specified by the DropDownButtonBounds property, and automatic sorting is enabled, the column is sorted and the sorting glyph is displayed next to the drop-down button. If the mouse click is within the DropDownButtonBounds and the drop-down list is not already showing, it is displayed using the ShowDropDownList method.

As described earlier, you can also handle the DataGridView.KeyDown event to display the drop-down list when the user presses a particular key combination, such as ALT+DOWN ARROW. When the correct keystrokes are detected, the event handler calls the ShowDropDownList method to display the drop-down list.

After the drop-down list appears, the user can navigate the list using the keyboard, scroll the list if a scroll bar is showing, select a value using the keyboard or mouse, or click elsewhere.

Clicking a filter option, or selecting an option and pressing ENTER, calls the UpdateFilter method (described in a following section) followed by the HideDropDownList method. Clicking somewhere other than the drop-down list so that the drop-down list loses input focus calls HideDropDownList, which removes the event handlers from their dropDownListBox events in addition to hiding the list control and removing it from the DataGridView control. The HideDropDownList method is also called when the user presses ESC or when a DataGridView event occurs that would change the location or contents of the drop-down list.

Hiding the drop-down list in certain DataGridView event handlers is necessary because some UI elements, such as ToolStrip-related controls, do not capture input focus. If user interactions with such controls result in changes to the DataGridView control or its contents, it might be inappropriate to continue displaying the drop-down list. For example, if the user clicks a "Show All" link on a StatusStrip control, like the one described earlier, the data source will become unfiltered. In this case, if the drop-down list is still showing, it may contain incorrect filter options based on the previous filter setting, and it may also be in the wrong location if the filter change caused several columns to resize.

The ShowDropDownList Method

The details of displaying the drop-down list are in the ShowDropDownList method. This method performs the following actions:

  1. Populates the dropDownListBox.Items collection with filter options. The primary task is to retrieve values from the data source. This task is handled by the PopulateFilters method, described in the following section. After the filter values have been retrieved, the ShowDropDownList method adds them to the Items collection and highlights the current filter value if there is one in effect for the column.
  2. Sets the dropDownListBox.Bounds property based on thedropDownListBox  contents and several other factors. This action is handled by the SetDropDownListBoxBounds method, which is described in a following section.
  3. Associates event handlers with dropDownListBox events to manage user interactions with the drop-down list.
  4. Displays the newly configured dropDownListBox on the DataGridView control.

As soon as the drop-down list appears, the user can interact with it as described earlier.

The source code for the ShowDropDownList method follows:

  public void ShowDropDownList()
  {
      PopulateFilters();

      String[] filterArray = new String[filters.Count];
      filters.Keys.CopyTo(filterArray, 0);
      dropDownListBox.Items.Clear();
      dropDownListBox.Items.AddRange(filterArray);
      dropDownListBox.SelectedItem = selectedFilterValue;

      HandleDropDownListBoxEvents();

      SetDropDownListBoxBounds();
      dropDownListBox.Visible = true;
      dropDownListBoxShowing = true;
      this.DataGridView.Controls.Add(dropDownListBox);
      dropDownListBox.Focus();

      // Invalidate the cell so that the drop-down button will repaint
      // in the pressed state. 
      this.DataGridView.InvalidateCell(this);
  }

The PopulateFilters Method

The drop-down filter list for a particular column must contain one copy of each value that appears in the column. This assumes that the column is not currently applying a filter. If the column is filtered, the filter must be ignored so that the list values are the same as if it weren't filtered. The filter list contains only values that can appear in the column regardless of the column's own filter, so the filter values for all other columns remain in effect even though the column's own filter value is not in effect. 

The values displayed in dropDownFilterList must be formatted just as they are for display in the DataGridView control. When a filter value is selected, however, its formatted value is not always compatible with the BindingSource.Filter property. For this reason, string representations of both the formatted and unformatted values are stored in an OrderedDictionary instance called filters. An OrderedDictionary is used so that the ShowDropDownList method can populate dropDownListBox by accessing the formatted values in their correct order through the dictionary's Keys collection. When a filter value is selected, the UpdateFilter method can use the formatted display value as a dictionary key to retrieve the unformatted value, which is compatible with the BindingSource.Filters property.

To retrieve the necessary filter values, the PopulateFilters method performs the following actions:

  1. Sets the BindingSource.RaiseListChangedEvents property to false. This lets the PopulateFilters method modify the current BindingSource.Filter property value without causing the DataGridView to refresh its display.
  2. Caches the current Filter value.
  3. Calls the FilterWithoutCurrentColumn method, which parses the current filter string by removing the part related to the current column.
  4. Sets the Filter property to the parsed value.
  5. Clears the filters dictionary.
  6. Retrieves the current column's value for each row in the BindingSource and adds it to an ArrayList. Null and DBNull.Value values are excluded, but their presence is noted for later.
  7. Sorts the ArrayList. The ArrayList.Sort method uses the IComparable implementation of the value type, so strings will be sorted alphabetically, numeric values numerically, and DateTime values according to calendar order.
  8. For each value in the sorted ArrayList, determines its formatted string representation by calling the DataGridViewCell.GetFormattedValue method and passing in the column's InheritedStyle property value. This ensures that the values appear in the drop-down list with the same formatting they have in the DataGridView cells. 
  9. Adds each formatted value along with the unformatted string representation of each value to the filters dictionary if it has not already been added, excluding empty strings, but noting their presence.
  10. Adds special filter options to the filters dictionary as follows:
    • Always adds (All) as the first item in the list.
    • Adds (Blanks) and (NonBlanks) to the end of the list if the column contained both empty strings (or nulls) and non-empty strings.
    • Restores the BindingSource.Filter property to the cached value.
    • Sets the BindingSource.RaiseListChangedEvents property to true to resume normal operations.

The source code for the PopulateFilters method follows:

  private void PopulateFilters()
  {
      if (this.DataGridView == null) return;
  
      // Cast the data source to a BindingSource. 
      BindingSource data = this.DataGridView.DataSource as BindingSource;

      // Prevent the data source from notifying the DataGridView of changes. 
      data.RaiseListChangedEvents = false;

      // Cache the current BindingSource.Filter value and then change 
      // the Filter property to temporarily remove any filter for the 
      // current column. 
      String oldFilter = data.Filter;
      data.Filter = FilterWithoutCurrentColumn(oldFilter);

      // Reset the filters dictionary and initialize some flags
      // to track whether special filter options are needed. 
      filters.Clear();
      Boolean containsBlanks = false;
      Boolean containsNonBlanks = false;

      // Initialize an ArrayList to store the values in their original
      // types. This enables the values to be sorted appropriately.  
      ArrayList list = new ArrayList(data.Count);

      // Retrieve each value and add it to the ArrayList if it isn't
      // already present. 
      foreach (Object item in data)
      {
          Object value = null;

          // Use the ICustomTypeDescriptor interface to retrieve properties
          // if it is available; otherwise, use reflection. The 
          // ICustomTypeDescriptor interface is useful to customize

          // which values are exposed as properties. For example, the 
          // DataRowView class implements ICustomTypeDescriptor to expose 
          // cell values as property values.        
          // 
          // Iterate through the property names to find a case-insensitive
          // match with the DataGridViewColumn.DataPropertyName value.
          // This is necessary because DataPropertyName is case-
          // insensitive, but the GetProperties and GetProperty methods
          // used below are case-sensitive.
          ICustomTypeDescriptor ictd = item as ICustomTypeDescriptor;
          if (ictd != null)
          {
              PropertyDescriptorCollection properties = 
                  ictd.GetProperties();
              foreach (PropertyDescriptor property in properties)
              {
                  if (String.Compare(this.OwningColumn.DataPropertyName,
                      property.Name, true /*case insensitive*/,
                      System.Globalization.CultureInfo.InvariantCulture)

                      == 0)
                  {
                      value = property.GetValue(item);
                      break;
                  }
              }
          }
          else
          {
              PropertyInfo[] properties = item.GetType().GetProperties(
                  BindingFlags.Public | BindingFlags.Instance);
              foreach (PropertyInfo property in properties)
              {
                  if (String.Compare(this.OwningColumn.DataPropertyName,
                      property.Name, true /*case insensitive*/,
                     System.Globalization.CultureInfo.InvariantCulture) 
                      == 0)
                  {
                      value = property.GetValue(item, 
                          null /*property index*/);
                      break;
                  }
              }
          }

          // Skip empty values, but note that they are present. 
          if (value == null || value == DBNull.Value)
          {
              containsBlanks = true;
              continue;
          }

          // Add values to the ArrayList if they are not already there.
          if (!list.Contains(value))
          {
              list.Add(value);
          }
      }

      // Sort the ArrayList. The default Sort method uses the IComparable 
      // implementation of the stored values so that string, numeric, and 
      // date values will all be sorted correctly. 
      list.Sort();

      // Convert each value in the ArrayList to its formatted representation
      // and store both the formatted and unformatted string representations
      // in the filters dictionary. 
      foreach (Object value in list)
      {
          // Use the cell's GetFormattedValue method with the column's
          // InheritedStyle property so that the dropDownListBox format
          // will match the display format used for the column's cells. 
          String formattedValue = null;
          DataGridViewCellStyle style = OwningColumn.InheritedStyle;
          formattedValue = (String)GetFormattedValue(value, -1, ref style, 
              null, null, DataGridViewDataErrorContexts.Formatting);

          if (String.IsNullOrEmpty(formattedValue))
          {
              // Skip empty values, but note that they are present.
              containsBlanks = true;
          }
          else if (!filters.Contains(formattedValue))
          {
              // Note whether non-empty values are present. 
              containsNonBlanks = true;

              // For all non-empty values, add the formatted and 
              // unformatted string representations to the filters 
              // dictionary.
              filters.Add(formattedValue, value.ToString());
          }
      }

      // Restore the filter to the cached filter string and 
      // re-enable data source change notifications. 
      if (oldFilter != null) data.Filter = oldFilter;
      data.RaiseListChangedEvents = true;

      // Add special filter options to the filters dictionary
      // along with null values, since unformatted representations
      // are not needed. 
      filters.Insert(0, "(All)", null);
      if (containsBlanks && containsNonBlanks)
      {
          filters.Add("(Blanks)", null);
          filters.Add("(NonBlanks)", null);
      }
  }

The SetDropDownListBoxBounds Method

The SetDropDownListBoxBounds method initializes the size and location of the drop-down list. The preferred size depends primarily on the dropDownListBox contents, which are the formatted values stored in the Keys collection of the filters dictionary. The SetDropDownListBoxBounds method first calls the Graphics.MeasureString method for each filter value. For each value, the width is stored if it is wider than all previous values, and the height is added to an accumulating total height for all values. The results are then used to determine the preferred size.

The preferred height is the smallest of the following values:

  • The accumulated height of all filter values.
  • The user-specified maximum height as calculated from the DropDownListBoxMaxLines property value.
  • The available height of the DataGridView control client area.

The preferred width is the width of the widest filter value, plus the width of the scrollbar if the preferred height does not display all dropDownListBox items, plus a small amount for padding.

The preferred dropDownListBox location is based on the location of the drop-down button and the edge of the DataGridView control. The right edge of the list should ideally be aligned with the right edge of the drop-down button. If RightToLeft is enabled, the left edge of the list is aligned to the left edge of the button instead. If aligning the list to the button would make it overlap the edge of the DataGridView control, however, then the overlapping edge of the list should be at the edge of the control instead.

After the dropDownListBox size and location have been specified, the drop-down list is ready for display.

The source code for the SetDropDownListBoxBounds method follows:

  private void SetDropDownListBoxBounds()
  {
      // Declare variables that will be used in the calculation, 
      // initializing dropDownListBoxHeight to account for the 
      // ListBox borders.
      Int32 dropDownListBoxHeight = 2;
      Int32 currentWidth = 0;
      Int32 dropDownListBoxWidth = 0;
      Int32 dropDownListBoxLeft = 0;

      // For each formatted value in the filters dictionary Keys collection,
      // add its height to dropDownListBoxHeight and, if it is wider than 
      // all previous values, set dropDownListBoxWidth to its width.
      using (Graphics graphics = dropDownListBox.CreateGraphics())
      {
          foreach (String filter in filters.Keys)
          {
              SizeF stringSizeF = graphics.MeasureString(
                  filter, dropDownListBox.Font);
              dropDownListBoxHeight += (Int32)stringSizeF.Height;
              currentWidth = (Int32)stringSizeF.Width;
              if (dropDownListBoxWidth < currentWidth)
              {
                  dropDownListBoxWidth = currentWidth;
              }
          }
      }

      // Increase the width to allow for horizontal margins and borders. 
      dropDownListBoxWidth += 6;

      // Constrain the dropDownListBox height to the 
      // DropDownListBoxMaxHeightInternal value, which is based on 

      // the DropDownListBoxMaxLines property value but constrained by
      // the maximum height available in the DataGridView control.
      if (dropDownListBoxHeight > DropDownListBoxMaxHeightInternal)
      {
          dropDownListBoxHeight = DropDownListBoxMaxHeightInternal;

          // If the preferred height is greater than the available height,
          // adjust the width to accommodate the vertical scroll bar. 
          dropDownListBoxWidth += SystemInformation.VerticalScrollBarWidth;
      }

      // Calculate the ideal location of the left edge of dropDownListBox 
      // based on the location of the drop-down button and taking the 
      // RightToLeft property value into consideration. 
      if (this.DataGridView.RightToLeft == RightToLeft.No)
      {
          dropDownListBoxLeft = DropDownButtonBounds.Right - 
              dropDownListBoxWidth + 1;
      }
      else
      {
          dropDownListBoxLeft = DropDownButtonBounds.Left - 1;
      }

      // Determine the left and right edges of the available horizontal
      // width of the DataGridView control. 
      Int32 clientLeft = 1;
      Int32 clientRight = this.DataGridView.ClientRectangle.Right;
      if (this.DataGridView.DisplayedRowCount(false) < 
          this.DataGridView.RowCount)
      {
          if (this.DataGridView.RightToLeft == RightToLeft.Yes)
          {
              clientLeft += SystemInformation.VerticalScrollBarWidth;
          }
          else
          {
              clientRight -= SystemInformation.VerticalScrollBarWidth;
          }
      }

      // Adjust the dropDownListBox location and/or width if it would
      // otherwise overlap the left or right edge of the DataGridView.
      if (dropDownListBoxLeft < clientLeft)
      {
          dropDownListBoxLeft = clientLeft;
      }
      Int32 dropDownListBoxRight =  
          dropDownListBoxLeft + dropDownListBoxWidth + 1;
      if (dropDownListBoxRight > clientRight)
      {
          if (dropDownListBoxLeft == clientLeft)
          {
              dropDownListBoxWidth -=
                  dropDownListBoxRight - clientRight;
          }
          else
          {
              dropDownListBoxLeft -=
                  dropDownListBoxRight - clientRight;
              if (dropDownListBoxLeft < clientLeft)
              {
                  dropDownListBoxWidth -= clientLeft - dropDownListBoxLeft;
                  dropDownListBoxLeft = clientLeft;
              }
          }
      }

      // Set the ListBox.Bounds property using the calculated values. 
      dropDownListBox.Bounds = new Rectangle(dropDownListBoxLeft,
          DropDownButtonBounds.Bottom, // top of drop-down list box
          dropDownListBoxWidth, dropDownListBoxHeight);
  }

Handling ListBox Events

The dropDownListBox events that need handling are the LostFocus, MouseClick, and KeyDown events. If the user clicks somewhere other than the drop-down list so that the drop-down list loses input focus, the LostFocus event handler calls the HideDropDownList method. If the user clicks a filter option or presses ENTER, the MouseClick or KeyDown event handler calls the UpdateFilter method then the HideDropDownList method. If the user presses ESC, the KeyDown event handler calls just the HideDropDownList method.

Filtering the Bound Data

When the user clicks a filter option from a drop-down list for a particular column, the data source is filtered so that only rows with the selected value in that column are displayed. Because there is only one BindingSource.Filter property for the entire data source, the selected filter option must be combined with the filter options for all columns into a single filter string.

There are four methods that work with filter strings: UpdateFilter, FilterWithoutCurrentColumn, RemoveFilter, and GetFilterStatus.

The UpdateFilter Method

The UpdateFilter method is responsible for modifying the BindingSource.Filter value in response to a user selection in the drop-down filter list. To do this, it performs the following actions:

  1. Calls the FilterWithoutCurrentColumn method to retrieve a parsed filter string that does not include the current column's filter value.
  2. If the user selected the (All) option, sets the Filter property to the parsed value.
  3. If the user selected an option other than (All), creates a filter string for the column and adds it to the current Filter value.

The filter string for ordinary filter values is in the following form:

  [columnName]='filterValue'

For the (Blanks) and (NonBlanks) options, the filter string converts the value to a string, using the empty string for null values, and then tests whether the converted value is of zero length. These filter strings are in the following forms:

  LEN(ISNULL(CONVERT([columnName],'System.String'),''))=0
  LEN(ISNULL(CONVERT([columnName],'System.String'),''))>0

If the BindingSource.Filter property value is null or empty, the UpdateFilter method sets it to the column filter string. If the Filter property is not null or empty, the column filter is appended to the end of the Filter value and delimited from existing values with the string " AND ".

The source code for the UpdateFilter method follows:

  private void UpdateFilter()
  {
      // Continue only if the selection has changed.
      if (dropDownListBox.SelectedItem.ToString()
          .Equals(selectedFilterValue))
      {
          return;
      }

      // Store the new selection value. 
      selectedFilterValue = dropDownListBox.SelectedItem.ToString();

      // Cast the data source to an IBindingListView.
      IBindingListView data = 
          this.DataGridView.DataSource as IBindingListView;

      // If the user selection is (All), remove any filter currently 
      // in effect for the column. 
      if (selectedFilterValue.Equals("(All)"))
      {
          data.Filter = FilterWithoutCurrentColumn(data.Filter);
          filtered = false;
          currentColumnFilter = String.Empty;
          return;
      }

      // Declare a variable to store the filter string for this column.
      String newColumnFilter = null;

      // Store the column name in a form acceptable to the Filter property, 
      // using a backslash to escape any closing square brackets. 

      String columnProperty = 
          OwningColumn.DataPropertyName.Replace("]", @"\]");

      // Determine the column filter string based on the user selection.
      // For (Blanks) and (NonBlanks), the filter string determines whether
      // the column value is null or an empty string. Otherwise, the filter
      // string determines whether the column value is the selected value. 
      switch (selectedFilterValue)
      {
          case "(Blanks)":
              newColumnFilter = String.Format(
                  "LEN(ISNULL(CONVERT([{0}],'System.String'),''))=0",
                  columnProperty);
              break;
          case "(NonBlanks)":
              newColumnFilter = String.Format(
              "LEN(ISNULL(CONVERT([{0}],'System.String'),''))>0",
                  columnProperty);
              break;
          default:
              newColumnFilter = String.Format("[{0}]='{1}'",
                  columnProperty,
                  ((String)filters[selectedFilterValue])
                  .Replace("'", "''"));  
              break;
      }

      // Determine the new filter string by removing the previous column 
      // filter string from the BindingSource.Filter value, then appending 
      // the new column filter string, using " AND " as appropriate. 
      String newFilter = FilterWithoutCurrentColumn(data.Filter);
      if (String.IsNullOrEmpty(newFilter))
      {
          newFilter += newColumnFilter;
      }
      else
      {
          newFilter += " AND " + newColumnFilter;
      }


      // Set the filter to the new value.
      try
      {
          data.Filter = newFilter;
      }
      catch (InvalidExpressionException ex)
      {
          throw new NotSupportedException(
              "Invalid expression: " + newFilter, ex);
      }

      // Indicate that the column is currently filtered
      // and store the new column filter for use by subsequent
      // calls to the FilterWithoutCurrentColumn method. 
      filtered = true;
      currentColumnFilter = newColumnFilter;
  }

The FilterWithoutCurrentColumn Method

The FilterWithoutCurrentColumn method parses a given filter string in order to remove the filter for the current column. Although it does not modify the BindingSource.Filter property directly, this method is used by the UpdateFilter and PopulateFilters methods to modify the Filter property.

To parse the filter string, the FilterWithoutCurrentColumn method uses the currentColumnFilter field, which stores only the current column's portion of the BindingSource.Filter property value. The FilterWithoutCurrentColumn method searches the specified filter string for the currentColumnFilter value. If the column value is found, it returns a copy of the specified string without the column value, and without any extraneous " AND " delimiters that prevent the return value from being a valid filter string.

The source code for the FilterWithoutCurrentColumn method follows:

  private String FilterWithoutCurrentColumn(String filter)
  {
      // If there is no filter in effect, return String.Empty. 
      if (String.IsNullOrEmpty(filter))
      {
          return String.Empty;
      }

        // If the column is not filtered, return the filter string unchanged. 
      if (!filtered)
      {
          return filter;
      }

      if (filter.IndexOf(currentColumnFilter) > 0)
      {
          // If the current column filter is not the first filter, return
          // the specified filter value without the current column filter 
          // and without the preceding " AND ". 
          return filter.Replace(
              " AND " + currentColumnFilter, String.Empty);
      }
      else
      {
          if (filter.Length > currentColumnFilter.Length)
          {
              // If the current column filter is the first of multiple 
              // filters, return the specified filter value without the 
              // current column filter and without the subsequent " AND ". 
              return filter.Replace(
                  currentColumnFilter + " AND ", String.Empty);
          }
          else
          {
              // If the current column filter is the only filter, 
              // return the empty string.
              return String.Empty;
          }
      }
  }

The RemoveFilter and GetFilterStatus Methods

The static RemoveFilter and GetFilterStatus methods are provided to make it easier for a client application to display a filter status string and expose a Show All option. They are static because they relate to the data source rather than to an individual cell. The data source can be different for different DataGridView controls, however, so these methods require the client application to pass in a reference to a DataGridView control. Although these methods are implemented on the cell class, they are also exposed through the DataGridViewAutoFilterTextBoxColumn class for convenience.

The RemoveFilter method sets the BindingSource.Filter property value to null to remove all filters. This method enables client applications to implement a Show All option without having to access the Filter property directly. A client application can set the Filter property to null or String.Empty without causing any problems, but setting it to any other value may interfere with the filtering provided by the DataGridViewAutoFilter library. For this reason, it is a good idea to keep modifications to the Filter property hidden from client code.

The GetFilterStatus method returns a status string containing the filtered and unfiltered BindingSource.Count property values. This method does more work than the RemoveFilter method, since it must temporarily modify the Filter property value to retrieve the unfiltered count. It does this by using the same process as described earlier for the PopulateFilters method. If the data source is not currently filtered, the GetFilterStatus method returns an empty string. Otherwise, it returns a string in the following format:

unfilteredCount of filteredCount records found

The source code for the GetFilterStatus method follows:

  public static String GetFilterStatus(DataGridView dataGridView)
  {
      // Continue only if the specified value is valid. 
      if (dataGridView == null)
      {
          throw new ArgumentNullException("dataGridView");
      }

      // Cast the data source to a BindingSource.
      BindingSource data = dataGridView.DataSource as BindingSource;

      // Return String.Empty if there is no appropriate data source or
      // there is no filter in effect. 
      if (String.IsNullOrEmpty(data.Filter) ||
          data == null || 
          data.DataSource == null || 
          !data.SupportsFiltering)
      {
          return String.Empty;
      }

      // Retrieve the filtered row count. 
      Int32 currentRowCount = data.Count;

      // Retrieve the unfiltered row count by 
      // temporarily unfiltering the data.
      data.RaiseListChangedEvents = false;
      String oldFilter = data.Filter;
      data.Filter = null;
      Int32 unfilteredRowCount = data.Count;
      data.Filter = oldFilter;
      data.RaiseListChangedEvents = true;

      Debug.Assert(currentRowCount <= unfilteredRowCount, 
          "current count is greater than unfiltered count");

      // Return String.Empty if the filtered and unfiltered counts
      // are the same, otherwise, return the status string. 
      if (currentRowCount == unfilteredRowCount)
      {
          return String.Empty;
      }
      return String.Format("{0} of {1} records found", 
          currentRowCount, unfilteredRowCount);
  }

For more information about using the RemoveFilter and GetFilterStatus methods, see "Using the DataGridViewAutoFilter library" earlier in this article.

Additional Properties

TheDataGridViewAutoFilterColumnHeaderCell class adds three properties to those it inherits from the DataGridViewColumnHeaderCell base class: AutomaticSortingEnabled, FilteringEnabled, and DropDownListBoxMaxLines.

The AutomaticSortingEnabled property lets you disable automatic sorting while keeping a column SortMode property value of Programmatic. Automatic sorting with the AutoFilter feature requires the SortMode property value to be Programmatic to prevent clicks on the drop-down button from sorting the column. If you want to handle sorting yourself, however, you must leave the SortMode property set to Programmatic in order to reserve space in the column header for the sorting glyph.

The FilteringEnabled property lets you disable filtering. When FilteringEnabled is false, the drop-down button is not displayed and the header appears and behaves like an ordinary column header cell.

The DropDownListBoxMaxLines property lets you customize the preferred maximum number of lines that appear in the drop-down list. The actual height is constrained by the available height in the DataGridView control, but this property lets you limit the height even further.

These cell properties are also exposed through the DataGridViewAutoFilterTextBoxColumn class for convenience.

DataGridViewAutoFilterTextBoxColumn Class Implementation Details

The DataGridViewAutoFilterTextBoxColumn class is provided as an example of how to add AutoFilter support to an existing column type. Having an AutoFilter column type is particularly useful to enable users to add the AutoFilter feature to their applications by using the Windows Forms Designer, as described earlier in this article.

The important code in the DataGridViewAutoFilterTextBoxColumn class is the class constructor, which does nothing except set the DefaultHeaderCellType property to the DataGridViewAutoFilterColumnHeaderCell type and set the SortMode property to Programmatic so that clicks on the drop-down button will not trigger automatic sorting.

  public DataGridViewAutoFilterTextBoxColumn() : base()
  {
      base.DefaultHeaderCellType = 
          typeof(DataGridViewAutoFilterColumnHeaderCell);
      base.SortMode = DataGridViewColumnSortMode.Programmatic;
  }

The remaining code in the DataGridViewAutoFilterTextBoxColumn class is provided for convenience. The DefaultHeaderCellType and SortMode properties are reimplemented to hide them from the designer and to throw an exception if the SortMode property is set to Automatic. Additionally, the column class exposes the new public properties and the static methods of the cell class. These members just wrap the cell members, so setting a property value on a column instance will update the same property in the column's header cell instance.

Possible Enhancements

The DataGridViewAutoFilter library provides a basic user interface for column filtering and shows you how to customize a DataGridView column header cell. It also demonstrates how to host a Windows Forms control in a cell when you are unable to derive from an existing cell type.

As an AutoFilter feature, the DataGridViewAutoFilter library leaves several areas for potential improvement. The following list describes enhancements that you may want to consider making to the DataGridViewAutoFilterColumnHeaderCell  class:

  • Custom filtering: Displaying a custom-filter dialog box like the one in Excel that allows you to specify filter values such as "contains", "does not contain", "begins with", and so on.
  • Filtering with any data source, in unbound mode, or in virtual mode.
  • Support for special cell values, such as images.
  • Integration with additional header-cell features, such as a multi-column sort feature with header labels that indicate the sorted columns and their sort precedence.

Additional Resources

To provide feedback on this article and on the DataGridViewAutoFilter library, and to check for updates, see the Windows Forms Documentation Updates Blog.

The following resources provide additional information about customizing DataGridView cells:

For more information on Windows Forms in general, see: