FilterableDataGridView (C#.NET)






4.80/5 (4 votes)
A DataGridView control with built-in Filtering
Update
The article was updated to the version 1.1. The links are already pointing to the new versions of the files. See details below.
FilterableDataGridView
While developing my project, I was left without Internet access for a week, and I couldn't search for an easy and already written solution for my problem, so I decided to create my own solution.
I had a DataGridView
on one of my forms with a DataSource
linked to an SQLserver through LINQ to SQL and I wanted to be able to filter the result rows, based on user input. Since the clients (the users of my program) will connect to the database through the Internet, some of them via poor connections, I didn't want to make a new query with each change of the filter. I found out that the DataGridViewRow
has a Visible
property, and it looked like an efficient way to set this according to the filters. So I created a new class inheriting from the DataGridView
, and added some extra functionality to support filtering.
My solution consists of two classes:
FilterItem
FilterableDataGridView
1. FilterItem
This class represents a filter which can be applied to a FilterableDataGridView
. It's basically two strings, one for storing the filter text, and the other to select the columns for the filter check. (This was a necessary requirement to be able to select the columns, since I didn't want it to search in my hidden ID column for example).
But the interesting part of this class is an event which gets fired every time the filter changes, and makes it possible that changing the filter text will apply the filter again without any additional method calls. See usage below.
The code:
using System;
using System.Runtime.Serialization;
namespace Rankep.FilterableDataGridView
{
/// <summary>
/// Class representing a filter
/// </summary>
[Serializable()]
public class FilterItem : ISerializable
{
/// <summary>
/// The delegate behind the filter change event
/// </summary>
public delegate void FilterChangedHandler();
/// <summary>
/// Event for notification when the filter changes
/// </summary>
public event FilterChangedHandler FilterChanged;
/// <summary>
/// The filtering text
/// </summary>
private string _filter;
/// <summary>
/// Columns to filter
/// </summary>
private string _filterColumn;
/// <summary>
/// Gets or sets the filtering text
/// </summary>
public string Filter
{
get
{
return _filter;
}
set
{
_filter = value;
//Check subscribers
if (FilterChanged != null)
FilterChanged(); //Fire event
}
}
/// <summary>
/// Gets or sets the filtered columns (Columns should be delimited with '|')
/// </summary>
public string FilterColumn
{
get
{
return _filterColumn;
}
set
{
_filterColumn = value;
//Check subscribers
if (FilterChanged != null)
FilterChanged(); //Fire event
}
}
/// <summary>
/// Constructor
/// </summary>
public FilterItem()
{
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="filter">Filtering text</param>
/// <param name="column">Filtered columns</param>
public FilterItem(string filter, string column)
{
_filter = filter;
_filterColumn = column;
}
/// <summary>
/// Constructor for deserialization
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
public FilterItem(SerializationInfo info, StreamingContext context)
{
_filter = (string)info.GetValue("Filter", typeof(string));
_filterColumn = (string)info.GetValue("FilterColumn", typeof(string));
}
/// <summary>
/// Method supporting serialization
/// </summary>
/// <param name="info"></param>
/// <param name="context"></param>
public void GetObjectData(SerializationInfo info, StreamingContext context)
{
info.AddValue("Filter", _filter);
info.AddValue("FilterColumn", _filterColumn);
}
}
}
2. FilterableDataGridView
The class inherits from DataGridView
. It contains a collection of FilterItems
as a public
property named Filters
, you can add your filters to this list. The collections type is ObservableCollection<>
, which is only available from .NET 4.0, so this sets the minimum requirements of the whole project.
When the property’s value is set, the object subscribes to the collection’s CollectionChanged
event, it’s handled the following way:
- If a new
FilterItem
is added, it subscribes to itsFilterChanged
event. - If an item is removed, it unsubscribes from its event.
- Finally the
Filter()
method is called.
The Filter
method checks every row for every filter and decides (sets) if the row should be visible or not.
Version 1.1
First of all, many thanks to Kabwla.Phone for the great suggestions, most of the change is that he wrote in his comments.
- If the
Filters
collection is changed, the event handler is released, so the old collection can be garbage collected. BeginFilterUpdate()
andEndFilterUpdate()
methods are added- The same
FilterItem
cannot be added more than once - Bug fix: the
Filter
method now skips the new uncommitted row - The sample is updated also
The code (v1.1):
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Windows.Forms;
using System.Collections.ObjectModel;
using System.Linq;
/*
* Many thanks to Kabwla.Phone from codeproject.com for suggestions to make the control better.
*/
namespace Rankep.FilterableDataGridView
{
/// <summary>
/// The class extends the functionality of a DataGridView with filtering
/// </summary>
[Serializable()]
[ToolboxBitmap(typeof(DataGridView))]
public partial class FilterableDataGridView : DataGridView
{
private object syncroot = new object();
/// <summary>
/// Indicates whether the filters are in updating mode.
/// Call BeginFilterUpdate() to enter updating mode, and EndFilterUpdate() to exit.
/// </summary>
public bool IsFilterUpdating { get; private set; }
/// <summary>
/// Indicates whether the content should be filtered after updating mode is exited.
/// </summary>
public bool IsFilterDirty { get; private set; }
/// <summary>
/// A collection to store the filters
/// </summary>
/// <remarks>
/// ObservableCollection requires .NET 4.0
/// </remarks>
private ObservableCollection<FilterItem> _filters;
/// <summary>
/// Property to get or set the collection of the filters
/// </summary>
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ObservableCollection<FilterItem> Filters
{
get
{
return _filters;
}
set
{
//Remove eventhandler so the original collection is 'free' and can be garbage collected.
if (_filters != null)
{
_filters.CollectionChanged -= _filters_CollectionChanged;
}
_filters = value;
if (_filters != null)
{
//remove again, it shold not be linked, however to be sure.
_filters.CollectionChanged -= _filters_CollectionChanged;
// Subscribe to the CollectionChanged event, so the filtering can be done automatically
_filters.CollectionChanged += _filters_CollectionChanged;
}
}
}
/// <summary>
/// The event handler of the filters CollectionChanged event,
/// so the filtering can be automatic when the collection changes
/// </summary>
/// <param name="sender">sender</param>
/// <param name="e">Event arguments</param>
void _filters_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Indicates whether the collection really changed
bool changed = true;
//If a new element is added
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (FilterItem fi in e.NewItems)
{
//If the collection already contains this FilterItem
if (_filters.Count(x => x.Equals(fi)) > 1)
{
lock (syncroot)
{
//Disable the eventhandler while removing the item
_filters.CollectionChanged -= _filters_CollectionChanged;
//Remove the newly added FilterItem from the collection
_filters.RemoveAt(e.NewItems.IndexOf(fi));
//Enable the eventhandler
_filters.CollectionChanged += _filters_CollectionChanged;
//The collection actually didn't change, there's no need to refilter
changed = false;
}
}
else
{
//subscribe to its event, so filtering will be done automatically every time when the filter changes
fi.FilterChanged += Filter;
}
}
}
//If the filter is removed
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
foreach (FilterItem fi in e.OldItems)
{
//unsubscribe from its event
fi.FilterChanged -= Filter;
}
}
//Finally filter the list
if (changed)
Filter();
}
/// <summary>
/// Constructor
/// </summary>
public FilterableDataGridView()
{
InitializeComponent();
Filters = new ObservableCollection<FilterItem>();
}
/// <summary>
/// Constructor
/// </summary>
/// <param name="container"></param>
public FilterableDataGridView(IContainer container)
{
container.Add(this);
InitializeComponent();
Filters = new ObservableCollection<FilterItem>();
}
/// <summary>
/// Sets the IsFilterUpdating property to true,
/// so you can perform multiple changes
/// and the filtering will be done only after EndFilterUpdate() is called
/// </summary>
public void BeginFilterUpdate()
{
IsFilterUpdating = true;
}
/// <summary>
/// Sets the IsFilterUpdating property to false,
/// and performs the filtering if it is necessary
/// </summary>
public void EndFilterUpdate()
{
IsFilterUpdating = false;
if (IsFilterDirty)
Filter();
}
/// <summary>
/// Method to filter the DataGridView
/// It gets called whenever the filters collection changes or a filter changes.
/// Explicit call is possible, however not necessary.
/// </summary>
public void Filter()
{
if (IsFilterUpdating)
{
IsFilterDirty = true;
}
else
{
IsFilterDirty = false;
//Set the selected cell to null, so every cell(/row) can be hidden
this.CurrentCell = null;
//Check every row in the DataGridView
foreach (DataGridViewRow row in Rows)
{
//Skip the NewRow
if (row.Index != this.NewRowIndex)
{
bool visible = true;
//Check every FilterItem
foreach (FilterItem fi in _filters)
{
//The char used to delimit the columns from each other
// it is also used to denote an OR relation ship between the filter texts
char c = '|';
//Split the string to column names
List<string> columns = new List<string>(fi.FilterColumn.Split(c));
List<string> filters = new List<string>(fi.Filter.Split(c));
bool atLeastOneContains = false;
//Check every columns
foreach (string column in columns)
{
foreach (string filter in filters)
{
if (row.Cells[column].Value != null && row.Cells[column].Value.ToString().ToUpper().Contains(filter.ToUpper()))
{
//If the column contains any of the filter texts, the filter is satisfied
atLeastOneContains = true;
break;
}
}
if (atLeastOneContains)
{
//If the column contains the filter text, the filter is satisfied
break;
}
}
//If none of the columns contain the text, the row can't be visible
if (!atLeastOneContains)
{
visible = false;
break;
}
}
//Set the Visible property the Row
row.Visible = visible;
}
}
}
}
}
}
Usage
The FilterItem Properties
Filter
: Set astring
that has to be found in the rows values. Upper or lower case doesn't matter. The ‘|’ character will translate as OR, thus any of thestring
s on the sides of ‘|’ can satisfy the filtering condition. For anAND
expression, please use multipleFilterItems
, since a row has to meet the expressions of allFilterItems
present in theFilterableDataGridView Filters
collection. Regular expressions are not supported, yet.FilterColumn
: List the columns where the search should be done, use '|' to separate column names.
E.g.:
//every row, which contains the string "John" in the column called "name"
new FilterItem("John", "name");
//every row, which contains the string "John" in the column called "name" or "address" or "phone"
new FilterItem("John", "name|address|phone");
//every row, which contains the string "John" or "Jane" in the column called "name" or "address" or "phone"
new FilterItem("John|Jane", "name|address|phone");
Using theFilterableDataGridView
Let's say you have a TextBox
(named textbox1
) and you want the FilterableDataGridView
to filter when the text changes in the textbox
.
- Add a
FilterableDataGridView
to your Form, from the toolbox. Make sure to implement the properusing
directive:
using Rankep.FilterableDataGridView;
- Create a
FilterItem
which you will use to update the filter. (Don't create a new one every time, or if you do remember to delete the old ones from the list, because all of theFilters
added to theFilterableDataGridView
.Filters
list will be checked and will have to be satisfied.)Example:
public partial class Form1 : Form { //A FilterItem for the text in the TextBox private FilterItem textboxFilter; ... }
- Assign a new
FilterItem
to it.
Example:
textboxFilter = new FilterItem("", "Name|Address|Tel");
- Subscribe to the
textbox1 TextChanged
event, and change thetextboxFilter.Filter
to theText
property of theTextbox
. (Check if theFilterableDataGridView.Filters
contains theFilterItem
, if not add it).Example:
private void textBox1_TextChanged(object sender, EventArgs e) { //Change the filter Text textboxFilter.Filter = textBox1.Text; //If the FilterableDataGridView doesn't contain the filter yet if (!filterableDataGridView1.Filters.Contains(textboxFilter)) //then add it filterableDataGridView1.Filters.Add(textboxFilter); }
Using the precompiled .dll component:
- Copy the FilterableDataGridView.dll file to your project directory.
- Open your project in Visual Studio
- Right click on the Toolbox in the category where you would like to add the component.
- Select Choose Items…
- Browse…
- Open the FilterableDataGridView.dll
- Make sure that the
checkbox
in front ofFilterableDataGridView
is checked, and click OK. - The component has now appeared in the toolbox, and you can use it as any other component. Detailed usage is described above.
Usage to update the collection with more than one FilterItem at same time:
filterableDataGridView1.BeginFilterUpdate();
try
{
filterableDataGridView1.Filters.Add(filter);
filterableDataGridView1.Filters.Add(filter2);
filterableDataGridView1.Filters.Remove(filter3);
///...
}
finally
{
filterableDataGridView1.EndFilterUpdate();
}
In this case the Filter()
method will be only called with the EndFilterUpdate()
, instead of being called every time the collection is changed.
Thank you for reading, feel free to use it, copy it, share it, improve it, ...
Any questions, advice is welcome in the comments.