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
{
[Serializable()]
public class FilterItem : ISerializable
{
public delegate void FilterChangedHandler();
public event FilterChangedHandler FilterChanged;
private string _filter;
private string _filterColumn;
public string Filter
{
get
{
return _filter;
}
set
{
_filter = value;
if (FilterChanged != null)
FilterChanged();
}
}
public string FilterColumn
{
get
{
return _filterColumn;
}
set
{
_filterColumn = value;
if (FilterChanged != null)
FilterChanged();
}
}
public FilterItem()
{
}
public FilterItem(string filter, string column)
{
_filter = filter;
_filterColumn = column;
}
public FilterItem(SerializationInfo info, StreamingContext context)
{
_filter = (string)info.GetValue("Filter", typeof(string));
_filterColumn = (string)info.GetValue("FilterColumn", typeof(string));
}
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 its FilterChanged
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()
and EndFilterUpdate()
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;
namespace Rankep.FilterableDataGridView
{
[Serializable()]
[ToolboxBitmap(typeof(DataGridView))]
public partial class FilterableDataGridView : DataGridView
{
private object syncroot = new object();
public bool IsFilterUpdating { get; private set; }
public bool IsFilterDirty { get; private set; }
private ObservableCollection<FilterItem> _filters;
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)]
public ObservableCollection<FilterItem> Filters
{
get
{
return _filters;
}
set
{
if (_filters != null)
{
_filters.CollectionChanged -= _filters_CollectionChanged;
}
_filters = value;
if (_filters != null)
{
_filters.CollectionChanged -= _filters_CollectionChanged;
_filters.CollectionChanged += _filters_CollectionChanged;
}
}
}
void _filters_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
bool changed = true;
if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
{
foreach (FilterItem fi in e.NewItems)
{
if (_filters.Count(x => x.Equals(fi)) > 1)
{
lock (syncroot)
{
_filters.CollectionChanged -= _filters_CollectionChanged;
_filters.RemoveAt(e.NewItems.IndexOf(fi));
_filters.CollectionChanged += _filters_CollectionChanged;
changed = false;
}
}
else
{
fi.FilterChanged += Filter;
}
}
}
else if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove)
{
foreach (FilterItem fi in e.OldItems)
{
fi.FilterChanged -= Filter;
}
}
if (changed)
Filter();
}
public FilterableDataGridView()
{
InitializeComponent();
Filters = new ObservableCollection<FilterItem>();
}
public FilterableDataGridView(IContainer container)
{
container.Add(this);
InitializeComponent();
Filters = new ObservableCollection<FilterItem>();
}
public void BeginFilterUpdate()
{
IsFilterUpdating = true;
}
public void EndFilterUpdate()
{
IsFilterUpdating = false;
if (IsFilterDirty)
Filter();
}
public void Filter()
{
if (IsFilterUpdating)
{
IsFilterDirty = true;
}
else
{
IsFilterDirty = false;
this.CurrentCell = null;
foreach (DataGridViewRow row in Rows)
{
if (row.Index != this.NewRowIndex)
{
bool visible = true;
foreach (FilterItem fi in _filters)
{
char c = '|';
List<string> columns = new List<string>(fi.FilterColumn.Split(c));
List<string> filters = new List<string>(fi.Filter.Split(c));
bool atLeastOneContains = false;
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()))
{
atLeastOneContains = true;
break;
}
}
if (atLeastOneContains)
{
break;
}
}
if (!atLeastOneContains)
{
visible = false;
break;
}
}
row.Visible = visible;
}
}
}
}
}
}
Usage
The FilterItem Properties
Filter
: Set a string
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 the string
s on the sides of ‘|’ can satisfy the filtering condition. For an AND
expression, please use multiple FilterItems
, since a row has to meet the expressions of all FilterItems
present in the FilterableDataGridView 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.:
new FilterItem("John", "name");
new FilterItem("John", "name|address|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 proper using
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 the Filters
added to the FilterableDataGridView
. Filters
list will be checked and will have to be satisfied.)
Example:
public partial class Form1 : Form
{
private FilterItem textboxFilter;
...
}
- Assign a new
FilterItem
to it.
Example:
textboxFilter = new FilterItem("", "Name|Address|Tel");
- Subscribe to the
textbox1 TextChanged
event, and change the textboxFilter.Filter
to the Text
property of the Textbox
. (Check if the FilterableDataGridView.Filters
contains the FilterItem
, if not add it).
Example:
private void textBox1_TextChanged(object sender, EventArgs e)
{
textboxFilter.Filter = textBox1.Text;
if (!filterableDataGridView1.Filters.Contains(textboxFilter))
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 of FilterableDataGridView
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.
CodeProject