Introduction

This code sample focuses on filtering a BindingSource component in a Windows Form project where its data source is a DataTable rather than filtering from the Filter property of a BindingSource. The reason is that many developers writing window form applications use a BindingSource in tangent with a TableAdapter or simply using a DataSet or DataTable and need case or case insensitive capabilities for filter where the BindingSource component filter does not have the ability to filter with case insensitive casing.

In this article a language extension library which has extensions for a BindingSource with a DataSource set to a DataTable will show how to make filter using LIKE conditions easy. There are methods focus on LIKE conditions for starts-with, ends-with and contains with one for a general equal, all provide an option for casing.

Anatomy

Filter type

An enum is provided to indicate which type of filter is desired. The first member is for use in a ComboBox to prompt the user to select which type of filter to apply for a filter operation. These enum members are passed to a language extension method to apply a filter on a BindingSource component with the DataSource set to a DataTable.

public enum FilterCondition
{
    Select,
    StartsWith,
    Contains,
    EndsWith
}

To populate a ComboBox with the above Enum.
ProductNameFilterComboBox.DataSource = Enum.GetValues(typeof(FilterCondition));

When read to apply a filter, assert that the first member is not select.
var filterType = (FilterCondition)ProductNameFilterComboBox.SelectedItem;
if (filterType == FilterCondition.Select) return;

Once a valid enum member has be validated a TextBox is needed to capture user text for a filter. If the TextBox Text is empty clear the filter, otherwise apply the filter.
string.IsNullOrWhiteSpace(ProductNameTextBox.Text

Helpers

If there are unescaped apostrophes in the text to filter they need to be escaped which is done using the following language extension method within filter language extensions.

public static string EscapeApostrophe(this string pSender)
{
    return pSender.Replace("'", "''");
}

Within the extension methods below there are helper extension methods to the filtering extension method which means be sure to copy those to your project with the extension methods or better, copy the entire extension class to a project, trim out any extensions not indented to use.

BindingSource filter extensions

Each filter is against a DataTable which has a property CaseSensitive which indicates whether string comparisons within the table are case-sensitive. In each of the following examples the placement of the percent symbol (wildcard character) indicates the type of LIKE Transact-SQL.

Note: When you do string comparisons by using LIKE, all characters in the pattern string are significant. Significant characters include any leading or trailing spaces. If a comparison in a query is to return all rows with a string LIKE 'abc ' (abc followed by a single space), a row in which the value of that column is abc (abc without a space) isn't returned. However, trailing blanks, in the expression to which the pattern is matched, are ignored. If a comparison in a query is to return all rows with the string LIKE 'abc' (abc without a space), all rows that start with abc and have zero or more trailing blanks are returned.

Starts-with

public static void RowFilterStartsWith(
    this BindingSource sender,
    string field, string value,
    bool caseSensitive = false)
{
    sender.DataTable().CaseSensitive = caseSensitive;
    sender.DataView().RowFilter = $"{field} LIKE '{value.EscapeApostrophe()}%'";
}

Usage where the second argument would typically come from text entered into a TextBox by a user. The last argument dictates case sensitivity.
bindingSource.RowFilterStartsWith("ContactName", "An", true);

Ends-with

public static void RowFilterEndsWith(
    this BindingSource sender,
    string field, string value,
    bool caseSensitive = false)
{
    sender.DataTable().CaseSensitive = caseSensitive;
    sender.DataView().RowFilter = $"{field} LIKE '%{value.EscapeApostrophe()}'";
}

Contains

public static void RowFilterContains(
    this BindingSource sender,
    string field, string pValue,
    bool caseSensitive = false)
{
    sender.DataTable().CaseSensitive = caseSensitive;
    sender.DataView().RowFilter = $"{field} LIKE '%{pValue.EscapeApostrophe()}%'";
}

Each of the above can be placed into a single extension method where in code its not clear which type of filter/like condition is being performed which is personal taste plus may be harder to debug with one extension method for all three types of like conditions.

To provide the option to use clearly named extensions with less clear names the following trigger is the FilterCondition using a switch statement to use the named extensions.

public static void RowFilter(
    this BindingSource sender,
    string field,
    string value,
    FilterCondition condition,
    bool pCaseSensitive = false)
{
    switch (condition)
    {
        case FilterCondition.StartsWith:
            sender.RowFilterStartsWith(field, value.EscapeApostrophe(), pCaseSensitive);
            break;
        case FilterCondition.Contains:
            sender.RowFilterContains(field, value.EscapeApostrophe(), pCaseSensitive);
            break;
        case FilterCondition.EndsWith:
            sender.RowFilterEndsWith(field, value.EscapeApostrophe(), pCaseSensitive);
            break;
    }
}
 

Multiple field conditions

If there is a need to perform filters on multiple fields simple add a AND condition and alter the extension below which has been kept simple so it's easy to extend.
public static void RowFilterTwoConditions(
    this BindingSource sender,
    string field1,
    string value1,
    string pField2,
    string value2,
    bool caseSensitive = false)
{
    sender.DataTable().CaseSensitive = caseSensitive;
    sender.DataView().RowFilter =
        $"{field1} = '{value1.EscapeApostrophe()}' AND {pField2} = '{value2.EscapeApostrophe()}'";
}

Freeform LIKE

All of the filter/LIKE conditions have been done in each of the language extensions, another option is to create the condition in the form and pass to the following extension method. This requires a bit more knowledge of creating conditions from multiple TextBoxes, CheckBox and or Radio buttons.
public static void RowFilterFreeForm(
    this BindingSource sender,
    string filter,
    bool caseSensitive = false)
{
    sender.DataTable().CaseSensitive = caseSensitive;
    sender.DataView().RowFilter = filter;
}

Mock up example

bindingSource.RowFilterFreeForm("Country IN ('Argentina', 'Canada', 'UK')", true);

Clearing filters

To clear a filter, use the following extension method.
public static void RowFilterClear(this BindingSource sender)
{
    sender.DataView().RowFilter = "";
}

Helper extensions to LIKE extensions

The following are used with the extensions above and can be used to access a underlying DataTable and Default view of the DataTable.

Get the DataTable for a BindingSource
public static DataTable DataTable(this BindingSource sender)
{
    return (DataTable)sender.DataSource;
}

Get the DefaultView for a BindingSource.
public static DataView DataView(this BindingSource pSender)
{
    return ((DataTable)pSender.DataSource).DefaultView;
}

Unit test

To ensure the extension methods work as expected, unit test are provided. Since each unit test method uses the same database table, the table is read in for each test and set to a BindingSource in TestInitialize method which runs before each test method ensuring a fresh copy of data.

There are currently 21 test, this could be 42 or more test were the additional test would test more scenarios for filtering. 

Sample project

In the following project, select filter type, enter text into the TextBox and the filter is applied as text entered. The button Filter Data is to demonstrate performing a LIKE without filtering as typing. 



using System;
using System.Windows.Forms;
using Operations;
using FormHelpers;
using static ExtensionLibrary.BindingSourceExtensions;
 
namespace SampleWinForm
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
            Shown += Form1_Shown;
            ProductNameTextBox.KeyDown += ProductNameTextBox_KeyDown;
            ProductNameTextBox.TextChanged += ProductNameTextBox_TextChanged;
        }
 
        private readonly BindingSource _bindingSource = new BindingSource();
 
        private void Form1_Shown(object sender, EventArgs e)
        {
            ProductNameFilterComboBox.DataSource = Enum.GetValues(typeof(FilterCondition));
 
            var dataOperations = new DataOperations();
 
            _bindingSource.DataSource = dataOperations.GetProducts();
            dataGridView1.DataSource = _bindingSource;
 
            dataGridView1.Columns[0].AutoSizeMode = DataGridViewAutoSizeColumnMode.Fill;
 
            ProductNameTextBox.SetCueText("Filter or empty to clear");
 
            ActiveControl = dataGridView1;
        }
 
        private void FilterDataButton_Click(object sender, EventArgs e)
        {
            FilterOperation();
        }
        private void ProductNameTextBox_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.KeyCode == Keys.Enter)
            {
                FilterOperation();
            }
        }
        /// <summary>
        /// Search while typing
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void ProductNameTextBox_TextChanged(object sender, EventArgs e)
        {
            var filterType = (FilterCondition)ProductNameFilterComboBox.SelectedItem;
            if (filterType == FilterCondition.Select) return;
 
            if (ProductNameTextBox.Text.Length > 0)
            {
                _bindingSource.RowFilter("ProductName", ProductNameTextBox.Text, filterType);
            }
            else
            {
                _bindingSource.RowFilterClear();
            }
        }
        private void FilterOperation()
        {
            var filterType = (FilterCondition)ProductNameFilterComboBox.SelectedItem;
            if (filterType == FilterCondition.Select) return;
 
            if (string.IsNullOrWhiteSpace(ProductNameTextBox.Text))
            {
                _bindingSource.RowFilterClear();
            }
            else
            {
                _bindingSource.RowFilter("ProductName", ProductNameTextBox.Text, filterType);
            }
        }
    }
}

Prepare to run code samples

  1. From Solution Explorer, right click, select restore NuGet packages for BaseConnectionLibrary.
  2. Using a new query window in Visual Studio or using a new query window in SSMS execute the database script in GenerateDatabaseAndData.sql.
  3. In the project Operations, in DataOperations class constructor change DatabaseServer to the name of your server, otherwise an exception will be thrown.

Using the extensions in a project

  1. Copy ExtensionsLibrary to the same folder as your Visual Studio, add the project.
  2. Add a reference to the Windows Form project will will utilize the extensions.
  3. Add controls on a form similar to the code sample provided.

Using list

Besides using a DataTable, a list<T> is an option. For example, using the same fields as used above for a DataTable a class is used.
public class Product
{
    public int Identifier { get; set; }
    public string Name { get; set; }
}

To review, the following was used to populate a DataTable.
public DataTable GetProducts()
{
    var dt = new DataTable();
 
    using (var cn = new SqlConnection { ConnectionString = ConnectionString })
    {
        using (var cmd = new SqlCommand { Connection = cn })
        {
 
            cmd.CommandText =
                "SELECT ProductID, ProductName FROM  dbo.Products ORDER BY ProductName";
 
            cn.Open();
 
            dt.Load(cmd.ExecuteReader());
 
            dt.Columns["ProductID"].ColumnMapping = MappingType.Hidden;
           
        }
    }
 
    return dt;
 
}

With minor refactor, the following will return a list rather than a DataTable.

public List<Product> Products()
{
    var productList = new List<Product>();
 
    using (var cn = new SqlConnection { ConnectionString = ConnectionString })
    {
        using (var cmd = new SqlCommand { Connection = cn })
        {
 
            cmd.CommandText =
                "SELECT ProductID, ProductName FROM  dbo.Products ORDER BY ProductName";
 
            cn.Open();
 
            var reader = cmd.ExecuteReader();
 
            while (reader.Read())
            {
                productList.Add(new Product() {Identifier = reader.GetInt32(0), Name = reader.GetString(1)});
            }
 
 
        }
    }
 
    return productList;
}

Since a List<T> does not support sorting unlike a DataTable which supports sorting a open source BindingListView which supports sorting will be used found here and installed using NuGet.

Setting up

First installed the custom BindingListView followed by creating a variable for the BindingListView and a BindingSource component.

BindingListView<Product> view = new BindingListView<Product>(new List<Product>());
BindingSource _bindingSource = new BindingSource();

In either form load or form shown event read data from SQL-Server into a list passing the list to the BindingListView.
var dataOperations = new DataOperations();
view = new BindingListView<Product>(dataOperations.Products());

Next add the following class with extension methods to provide similar functionality used with a BindingSource/DataTable to perform LIKE where clauses.
public static class BindingListViewExtensions
{
    public static void NameStartsWith(this BindingListView<Product> sender, string fieldValue, bool caseSensitive = true)
    {
        if (caseSensitive)
        {
            sender.ApplyFilter(product => product.Name.StartsWith(fieldValue));
        }
        else
        {
 
            sender.ApplyFilter(product => product.Name
                .StartsWith(fieldValue, StringComparison.CurrentCultureIgnoreCase));
 
        }
         
    }
    public static void NameContains(this BindingListView<Product> sender, string fieldValue, bool caseSensitive = true)
    {
        if (caseSensitive)
        {
            sender.ApplyFilter(product => product.Name.Contains(fieldValue));
        }
        else
        {
 
            sender.ApplyFilter(product => product.Name
                                              .IndexOf(fieldValue, StringComparison.OrdinalIgnoreCase) >= 0);
 
        }
 
    }
    public static void NameEndsWith(this BindingListView<Product> sender, string fieldValue, bool caseSensitive = true)
    {
        if (caseSensitive)
        {
            sender.ApplyFilter(product => product.Name.EndsWith(fieldValue));
        }
        else
        {
 
            sender.ApplyFilter(product => product.Name
                .EndsWith(fieldValue, StringComparison.CurrentCultureIgnoreCase));
 
        }
 
    }
 
    public static List<Product> Products(this BindingListView<Product> sender)
    {
        return sender.Cast<Product>().ToList();
    }
    public static void RemoveFilter(this BindingListView<Product> sender)
    {
        sender.Filter = null;
    }
}

Back to the form, add a DataGridView (named dataGridView1) and TextBox (named ProductNameTextBox) and a CheckBox (named CaseSensitiveCheckBox with checked = true) . Subscribe to TextChanged event for the TextBox.
private void ProductNameTextBox_TextChanged(object sender, EventArgs e)
{
 
    view.NameContains(ProductNameTextBox.Text, CaseSensitiveCheckBox.Checked);
 
}

In form load or shown event.
ProductNameTextBox.TextChanged += ProductNameTextBox_TextChanged;

Typing into the TextBox an incremental filter is performed. Delete text in the TextBox to remove the filter.

BindingListView caveat

To drill down to a record the following code will do this. Although drilling down to a record is done in PositionChanged event of the BindingSource this can be done in other events e.g. a button click event.
private void _bindingSource_PositionChanged(object sender, EventArgs e)
{
    if (_bindingSource.Current == null) return;
 
    var customer = ((ObjectView<Product>)_bindingSource.Current).Object;
 
}

See also Important details on the following page.

Summary

In this article a solid base has been provided to perform filters using like wildcards without the mechanics revealed in form code, instead all the work is performed within language extensions methods. BindingSource components provide rich features which should be explored for working with data in Windows Forms rather than simply setting a DataTable to the DataSource of a DataGridView or databinding to controls like TextBoxes.

See also

.NET: DataBinding
C# Complete ComboBox
Extension methods (C#)

Source code

https://github.com/karenpayneoregon/BindingSourceFiltering_C