Implementing the IBindingListView for filtering

Due to customer feedback and requests, I've been working on an article that demonstrates a simple implementation of the filtering portion of the IBindingListView. This implementation works with the DataGridViewAutoFilter code that Karl first published in the summer of 2006 and allows you to search, sort and filter a list of business objects. In the course of writing the paper, I realized the filtering implementation could be even simpler than what I had for my article and still correctly interact with Karl's code.

For the simple filtering, I start with my generic implementation of searching and sorting demonstrated in my article published in summer 2006. I then use the FindCore method to find matches for the filter value. The code listing is lengthy so I've posted the complete listing here.

That said; I want to walk you through some of the code. The filtering expression my code handles follows a subset of the guidelines established by the DataColumn.Expression property. I'll look for a property name followed by an equals followed by the filter value, typically in quotes. For example, LastName="Smith". In this simple example, I am only concerned with values equal to the filter value. My upcoming article will cover more complex filtering.

The IBindingListView interface contains three members related to filter; the SupportsFiltering and Filter properties as well as the RemoveFilter method. The first step is to implement the SupportsFiltering property to always return true.

public bool SupportsFiltering

{

get { return true; }

}

Implementing the Filter property is more difficult and requires some preliminary steps. When the filter value is set, the filter is applied to the list and the list returns the items that meets the filter criteria. In addition, the original list must be cached because the filter can be removed, in which case, the list should revert to its original contents. Therefore, the unfiltered list is stored in a read-only property named UnfilteredList. I chose a read-only property to allow for the code to be unit-tested. The following code shows the declaration for the unfiltered list.

List<T> unfilteredListValue = new List<T>();

public List<T> UnfilteredList

{

get { return unfilteredListValue; }

}

Next is the Filter property itself.

private string filterValue = null;

public string Filter

 {

    ...

 }

 The getter for the filter property is straightforward.

get

{

     return filterValue;

}

However, the setter is more complicated and requires some explanantion. The first step in setting the Filter property is to check whether the filter value has changed. If the filter value is unchanged, the set operation should not do anything. If the Filter value has changed, I temporarily disable change notifications for the list. It is important that I do this step because I will be manipulating the contents of the list. In the context of an application, bound controls should not be notified of these internal changes. To disable change notifications, I set the RaiseListChangedEvents property to false; This property is set back to true when the set call is finished. Next, I'll check for a filter value of null. Null indicates the filter has been removed and the original list contents should be reset. I must also check the filter value to see whether it matches the expected format, which I do with a regular expression. If the filter value does not match the expected format, or the filter expression indicates multi-column filtering, an ArgumentException is thrown. If the filter complies with the expected format, I parse the filter string and apply it to the data source. Finally, I set the RaiseListChangedEvents property back to true and call raise a ListChanged event to signal bound controls to refresh. In the following code, the filter parsing and application is accomplished by two separate methods; GetFilterParts and ApplyFilter, which I'll discuss next.

set

{

    if (filterValue != value)

    {

        RaiseListChangedEvents = false;

        // If filter value is null, reset list.

        if (value == null)

        {

            this.ClearItems();

            foreach (T t in unfilteredListValue)

                this.Items.Add(t);

            filterValue = value;

        }

 

         // If the value is empty string, do nothing.

         // This behavior is compatible with DataGridView

         // AutoFilter code.

        else if (value == "") { }

         // If the value is not null or string, than process

         // normal.

        else if (Regex.Matches(value,

         "[?[\\w ]+]? ?[=] ?'?[\\w|/: ]+'?",

         RegexOptions.Singleline).Count == 1)

        {

            // If the filter is not set.

            unfilteredListValue.Clear();

            unfilteredListValue.AddRange(this.Items);

            filterValue = value;

            GetFilterParts();

            ApplyFilter();

        }

        else if (Regex.Matches(value,

            "[?[\\w ]+]? ?[=] ?'?[\\w|/: ]+'?",

            RegexOptions.Singleline).Count > 1)

            throw new ArgumentException("Multi-column" +

              "filtering is not implemented.");

 

        else throw new ArgumentException("Filter is not" +

               "in the format: propName = 'value'.");

 

        RaiseListChangedEvents = true;

        OnListChanged(new ListChangedEventArgs(ListChangedType.Reset, -1));

    }

}

In this example, parsing the filter is straightforward. I look for the "=" and grab the property name and filter value. The filter value is converted to the same type as the specified property. If the property is not found, or the conversion cannot be performed, an exception is thrown.

First, I expose some properties and private fields to store the filter parts.

private string filterPropertyNameValue;

private Object filterCompareValue;

public string FilterPropertyName

{

get { return filterPropertyNameValue; }

}

public Object FilterCompare

{

get { return filterCompareValue; }

}

Then I get the parts.

public void GetFilterParts()

{

string[] filterParts = Filter.Split(new char[] { '=' },

StringSplitOptions.RemoveEmptyEntries);

filterPropertyNameValue =

filterParts[0].Replace("[", "").Replace("]", "").Trim();

PropertyDescriptor propDesc =

TypeDescriptor.GetProperties(typeof(T))[filterPropertyNameValue.ToString()];

if (propDesc != null)

{

try

{

TypeConverter converter = TypeDescriptor.GetConverter(propDesc.PropertyType);

filterCompareValue = converter.ConvertFromString(filterParts[1].Replace("'", "").Trim());

}

catch (NotSupportedException)

{

throw new ArgumentException("Specified filter value " +

FilterCompare + " can not be converted from string." +

"..Implement a type converter for " + propDesc.PropertyType.ToString());

}

}

else throw new ArgumentException("Specified property '" +

FilterPropertyName + "' is not found on type " +

typeof(T).Name + ".");

}

Following is the ApplyFilter method.  This method is pretty simple; I loop through the items using the FindCore method to get all of the matches to the filter value.

private void ApplyFilter()

{

unfilteredListValue.Clear();

unfilteredListValue.AddRange(this.Items);

List<T> results = new List<T>();

PropertyDescriptor propDesc =

TypeDescriptor.GetProperties(typeof(T))[FilterPropertyName];

if (propDesc != null)

{

int tempResults = -1;

do

{

tempResults = FindCore(tempResults + 1, propDesc, FilterCompare);

if (tempResults != -1)

results.Add(this[tempResults]);

} while (tempResults != -1);

}

this.ClearItems();

if (results != null && results.Count > 0)

{

foreach (T itemFound in results)

this.Add(itemFound);

}

}

The final step is to implement RemoveFilter. This is simple; set Filter to null.

public void RemoveFilter()

{

if (Filter != null) Filter = null;

}

Hopefully this helps you get started with filtering and please look for my upcoming article that shows a more complex filtering implementation.