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 the Microsoft 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
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.
.gif) Note: For the remainder of this
article, I will use the Excel term "AutoFilter" to refer to
Excel-style column-filtering drop-down lists. |
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 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.
.gif)
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.
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:
- In your Windows Application project, add a reference to the
DataGridViewAutoFilter.dll assembly.
- 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.
- After the columns have been generated, on the control's smart tag, click
Edit Columns.
- In the Edit Columns dialog box, select a column.
- In the Properties window in the dialog box, select the ColumnType
property and choose DataGridViewAutoFilterTextBoxColumn from the
drop-down list.
- Repeat steps 4 and 5 for all columns that you want to use the AutoFilter
feature with.
- 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.
.gif)
Figure 2. The Edit Columns dialog box with the ColumnType
property set to DataGridViewAutoFilterTextBoxColumn.
To add the AutoFilter feature to your application programmatically:
- In your Windows Application project, add a reference to the
DataGridViewAutoFilter.dll assembly.
- 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;
- Add a DataGridView control to a form.
- 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.
// Add this code to the constructor.
dataGridView1.BindingContextChanged += new
EventHandler(dataGridView1_BindingContextChanged);
- 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.
// Add this code to the form Load event handler.
dataGridView1.DataSource = customersBindingSource;
- 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.
private void dataGridView1_BindingContextChanged(object sender, EventArgs e)
{
if (dataGridView1.DataSource == null) return;
foreach (DataGridViewColumn col in dataGridView1.Columns)
{
col.HeaderCell = new
DataGridViewAutoFilterColumnHeaderCell(col.HeaderCell);
}
dataGridView1.AutoResizeColumns();
}
- 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:
- Follow one of the previous procedures to add the AutoFilter feature to a
DataGridView control.
- 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;
- Add a StatusStrip control to your form named statusStrip1 and
containing two ToolStripStatusLabel components named filterStatusLabel and
showAllLabel.
- 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;
- 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);
}
- 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;
}
- Run your application and apply a filter.
Figure 3 shows a sample of the resulting StatusStrip after
a filter has been applied.
.gif)
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:
- 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;
- 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;
}
}
}
- 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.
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);
}
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:
- 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.
- 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.
- Associates event handlers with dropDownListBox events to
manage user interactions with the drop-down list.
- 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:
- 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.
- Caches the current Filter value.
- Calls the FilterWithoutCurrentColumn method, which parses
the current filter string by removing the part related to the current column.
- Sets the Filter property to the parsed value.
- Clears the filters dictionary.
- 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.
- 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.
- 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.
- 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.
- 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.
These special options do not require unformatted
equivalents, so they are added to the dictionary along with null values.
- 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:
- Calls the FilterWithoutCurrentColumn method to retrieve a
parsed filter string that does not include the current column's filter value.
- If the user selected the (All) option, sets the Filter property to the
parsed value.
- 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
The DataGridViewAutoFilterColumnHeaderCell 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.
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.
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.
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: