Telerik blogs

As you may already know, RadGridView received a "filtering" boost with our latest release. As a developer, you are no longer stuck with the default filtering user interface. With the introduction of the Custom Filtering Controls feature, you can easily craft the filtering control of your dreams. In this blog post I will try to explain what are the steps needed to achieve this by creating a fully-functional custom control. Although the control is tailored to a specific scenario, it can be used as a reference implementation for developing any kind of custom control. Finally, I would strongly suggest having the source code in front you while following this tutorial.

And here is the full source code. Note that you will need to upgrade to our Latest Internal Build.

The Background

Let us pretend that we have a column showing numbers formatted as percentages:

FitnessColumn

 Now, probably most of you are familiar with Excel’s filtering capabilities and particularly this one:

 ExcelCustomAutoFilter

Wouldn’t it be great if we were able to show something similar when the fitness column is filtered. It certainly would, so let’s do it.

The IFilteringControl interface

As you probably already figured it out, this is the interface that your control must implement in order to play nicely with RadGridView. Quite naturally, RadGridView’s native filtering control implements this interface. The interface itself is pretty simple:

   1: /// <summary>
   2: /// Provides functionality required by all filtering components.
   3: /// </summary>
   4: public interface IFilteringControl
   5: {
   6: /// <summary>
   7: /// Prepares the component for the column it will service.
   8: /// </summary>
   9: /// <param name="column">The column to prepare for.</param>
  10: void Prepare(GridViewBoundColumnBase column);
  11:  
  12: /// <summary>
  13: /// Gets a value indicating whether the filtering is active.
  14: /// </summary>
  15: bool IsActive { get; set; } 
  16: }

The Prepare method is called each time the control is about to be shown (when the user clicks the little funnel in the header cell). If you do not need to change anything between two consecutive times when the filtering UI is shown, there wouldn’t be much inside this method’s body. This is rarely the case though, as you will see later when we discuss distinct values. The IsActive property provides means of “filling up” the funnel when the column is filtered.

What is filtering, anyway? 

How do we filter RadGridView? That is an easy one: To filter the grid simply add an instance of IFilterDescriptor to the grid’s FilterDescriptors collection. Remove it to clear the filter. In order to implement Excel-like filtering like the one shown above we would need something saying “Give me all players that have fitness above 15% and less than 85%”. Let us break this down to “Fitness > 15%ANDFitness < 85%”. Luckily, Telerik provides the classes that suit this purpose perfectly. One is the CompositeFilterDescriptor, which naturally implements the IFilterDescriptor interface. This will be the one that we will add to the grid’s FilterDescriptors collection. This class provides the means to combine a number of different filters logically. In our case these filters will be only two and will be represented by instances of the much simpler FilterDescriptor class. One of them will indicate “Fitness > 0.15”, the other will indicate “Fitness < 0.85” and they will be combined by the LogicalOperator of the CompositeFilterDescriptor. Let’s take a look at the class hierarchy:

 FilterDescriptorsClassDiagram

And here is the design that will fulfill our requirements:

Design

MVVM, of course…

Once we have established our requirements let us create the UI and the view model. Here is my proposal for the UI part:

UI

Naturally, all color rectangles from the Excel screenshot above are the candidates for the public view model properties bound to the UI elements. The CompositeFilterDescriptor and the two FilterDescriptor’s I talked about earlier will be private fields of our view model and they will be updated each time one of the properties is changed.

Let us establish several rules. A FilterDescriptor is said to be active when its value part (the right combo) is different than null / empty string. The CompositeFilterDescriptor filter is said to be active if one or both of its child filters are active. We will make our view model “smart” and let it add and remove its composite filter from the grid’s FilterDescriptors collection. If the composite filter becomes active, it will be automatically added to the grid’s collection thus filtering the grid. When the composite filter becomes inactive, it will be automatically removed from the grid’s collection thus un-filtering the grid. Finally, these changes will be communicated to the filtering control’s IsActive property so that the funnel is correctly filled / emptied. But how do we reach the grid?

The IFilteringControl.Prepare method

As I have already said before, this method will be called each time the filter is about to show up. Fortunately, its only parameter is priceless. This is the column we are performing filtering on. From the column we can obtain a reference to the grid, and once we have it we can access all kinds of cool stuff to do the job for us. Here is what my Prepare method looks like:

   1: public void Prepare(GridViewBoundColumnBase column)
   2: {
   3: if (this.DataContext == null)
   4:     {
   5: // Create the view mode the very first time Prepare is called.
   6:         var viewModel = new PercentageFilteringViewModel(column
   7:             , column.DataControl.FilterDescriptors
   8:             , column.DataControl.GetDistinctValues);
   9:  
  10: // Let my view model tell me whether the filtering is active.
  11: this.SetBinding(IsActiveProperty
  12:             , new Binding("IsActive")
  13:             {
  14:                 Source = viewModel
  15:                 , Mode = BindingMode.OneWay
  16:             });
  17:  
  18: this.DataContext = viewModel;
  19:     }
  20: else
  21:     {
  22:         ((PercentageFilteringViewModel)this.DataContext).Prepare();
  23:     }

 

Feeding the view model

The first parameter we pass in the view model’s constructor is the column itself. The view model needs the column in order to obtain the data member name and its type. Also, if you have noticed our column is formatted to show percentages by using DataFormatString="{}{0:P2}". It is quite natural that you will want to display the distinct values (explained in just a second) formatted in exactly the same manner that normal data is, which in our case is percentage with two digits after the decimal point. Well, the column can help you with this formatting, since it is already doing it for the normal data cells anyway. That is why there is an extension method that will create a function for you when you pass in a column. This function will apply the IValueConverter of the column’s DataMemberBinding (if any) and then apply the DataFormatString (if any). Here is the signature of this extension method:

   1: public static Func<object, object> CreateConvertedAndFormattedValueFunc(this IDataFieldDescriptor fieldDescriptor)

If you do not use this function your distinct values will be “raw”, for example 0.015 instead of 1.50%.

The second parameter is the already famous FilterDescriptors collection. The view model will add and remove its CompositeFilterDescriptor when it thinks it is necessary.

The final parameter is very interesting. In case you have not noticed, the combo boxes in the right column are editable, that is you can either type in a value or you can choose one from the drop down. But where do these values come from? What is the items source of these combo boxes? Luckily, RadGridView has a method called GetDistinctValues which accepts a column and a boolean parameter called filter. Let me explain the filter parameter. Imagine you have a grid with sports players. You have filtered it to show only players from country X. Now you go to the player name column and want to filter. Do you want to see all possible player names? Set the filter parameter to false. Do you want to see the names of players from country X only? Set the filter parameter to true. This is also commonly known as “drill-down distinct values”. If you know that data can possibly change (added, deleted, modified records) between two consecutive filtering operations, then you should populate the distinct values each time the Prepare method is called. This is to ensure that these distinct values are up to date at the time of filtering. Here is how I populate the distinct values:

   1: private void PopulateDistinctValues()
   2: {
   3: // Backup the values since we will repopulate the collection
   4:     var f1 = this.Filter1Value;
   5:     var f2 = this.Filter2Value;
   6: 
   7: this.distinctValues.Clear();
   8: 
   9: foreach (object rawDistinctValue in this.getDistinctValuesCallback(this.dataFieldDescriptor, true))
  10:     {
  11: if (this.convertedAndFormattedValueFunc != null)
  12:         {
  13: // Convert the distinct value to take into account the
  14: // IValueConverter on the column's DataMemberBinding (if any)
  15: // and the column's DataFormatString (if any). If none of them
  16: // exists, then this would be the identity function, i.e. it will
  17: // return the same thing that was passed in.
  18: this.distinctValues.Add(new DistinctValue(rawDistinctValue
  19:                 , this.convertedAndFormattedValueFunc(rawDistinctValue)));
  20:         }
  21: else
  22:         {
  23: // Return the "raw" unformatted distinct value.
  24: this.distinctValues.Add(new DistinctValue(rawDistinctValue
  25:                 , rawDistinctValue));
  26:         }
  27:     }
  28:  
  29: // Restore the original values.
  30: this.Filter1Value = f1;
  31: this.Filter2Value = f2;
  32: }

After we have fed the view model with all information it needs to operate normally let’s see how it will create its filters:

   1: private void CreateFilterDescriptors()
   2: {
   3: this.filter1 = new FilterDescriptor(this.dataMemberName
   4:         , FilterOperator.IsGreaterThan
   5:         , null)
   6:     {
   7:         MemberType = this.dataFieldDescriptor.DataType
   8:     };
   9:  
  10: this.filter2 = new FilterDescriptor(this.dataMemberName
  11:         , FilterOperator.IsLessThan
  12:         , null)
  13:     {
  14:         MemberType = this.dataFieldDescriptor.DataType
  15:     };
  16: 
  17: this.compositeFilter = new CompositeFilterDescriptor
  18:     {
  19:         LogicalOperator = FilterCompositionLogicalOperator.And
  20:     };
  21: this.compositeFilter.FilterDescriptors.CollectionChanged += this.OnCompositeFilterDescriptorsCollectionChanged;
  22: }

The activation / deactivation logic is fairly simple. When a child filter realizes it has become active / inactive it attaches / detaches itself to the composite filter descriptor:

   1: public object Filter1Value
   2: {
   3:     get { return this.filter1.Value; }
   4:     set
   5:     {
   6: if (this.filter1.Value == value) return;
   7:  
   8: if (value != null)
   9:         {
  10: this.filter1.Value = value;
  11: if (!this.compositeFilter.FilterDescriptors.Contains(this.filter1))
  12:             {
  13: this.compositeFilter.FilterDescriptors.Add(this.filter1);
  14:             }
  15:         }
  16: else
  17:         {
  18: this.compositeFilter.FilterDescriptors.Remove(this.filter1);
  19: this.filter1.Value = null;
  20:         }
  21:  
  22: this.OnPropertyChanged("Filter1Value");
  23:     }
  24: }

Following exactly the same pattern, the composite filter descriptor listens for its children changing and does the necessary:

   1: void OnCompositeFilterDescriptorsCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
   2: {
   3: if (this.compositeFilter.FilterDescriptors.Count > 0)
   4:     {
   5: if (!this.targetFilters.Contains(this.compositeFilter))
   6:         {
   7: this.targetFilters.Add(this.compositeFilter);
   8:         }
   9:     }
  10: else
  11:     {
  12: this.targetFilters.Remove(this.compositeFilter);
  13:     }
  14: 
  15: this.OnPropertyChanged("IsActive");
  16: }

Finally, it lets the world know something’s going on by calling the OnPropertyChanged method. The control’s IsActive property is bound to the view model’s IsActive property so the funnel either fills up (we have filters) or empties (we don’t):

   1: public bool IsActive
   2: {
   3:     get { return this.targetFilters.Contains(this.compositeFilter); }
   4: }

Entering text manually

Since the two combo boxes on the right are editable, I have created a simple method that will parse what the user enters and translate it to a double, since the the two filters are working with double values. Percentages are only for presentation. You can modify and extend this method to allow any kind of input. In my case, I will let the user enter values like “12,34%”:

   1: public static object TryParsePercentageText(string text)
   2: {
   3: if (text.Contains("%"))
   4:     {
   5:         text = text.Replace("%", "");
   6:         text = text.Trim();
   7: double result;
   8: if (Double.TryParse(text, out result))
   9:         {
  10: return result / 100;
  11:         }
  12:     }
  13:  
  14: return null;
  15: }

Putting it all together

We have developed our control, we have developed our “smart” view model, but what is next. We have to tell the percentage column to use our brand new shiny control instead of the native one. It could not be any simpler than this, can it?

   1: <telerik:GridViewDataColumn Header="Fitness %"
   2: DataMemberBinding="{Binding Fitness}"
   3: DataFormatString="{}{0:P2}">
   4: <telerik:GridViewDataColumn.FilteringControl>
   5: <PercentageCustomFiltering:PercentageFilteringControl/>
   6: </telerik:GridViewDataColumn.FilteringControl>
   7: </telerik:GridViewDataColumn>

 

 And here you can see the little gem in action:

 

If you have any questions or suggestions for improving this tutorial I would be more than glad to hear them. Any ideas for future custom filtering controls would be greatly appreciated as well. With your feedback, we will be able to develop and publish the most demanded custom filtering controls for your reference.

On the other hand, those of you who are feeling really creative could use this project as a reference implementation and build an entirely new and different filtering control, demonstrating their particular requirements. One of the best places to share your achievement with the community would be our forums. The online examples host another two fascinating filtering controls -- an attendance slider and a date range filter.

For those of you who got excited, here is the full source code again.

 

 


Related Posts

Comments

Comments are disabled in preview mode.