This is a migrated thread and some comments may be shown as answers.

Implementing filter for collection property

3 Answers 594 Views
GridView
This is a migrated thread and some comments may be shown as answers.
Peter
Top achievements
Rank 1
Peter asked on 23 Jun 2020, 04:43 AM

HI

This is longer post, sorry. First I'll explain what I'm trying to do, and then I'll tell a little about what I have tried.

The Problem

I have an object with a property that consist of a list of strings. Showing it in a RadGridView is not hard.

<RadGridView ...>
  <GridViewBoundColumnBase Header="Tags"  DataMemberBinding="{Binding CategoryTagLabels, Converter={StaticResource ListToStringConverter}}"/>
</RadGridView>

 

The ListToStringConverter basically just runs string.join(",", tags).

Now, that is mighty fine.

 

The problem is that I would like to add filtering. I would for the list of distinct values to be shown, and rows that that has any of the selected values from distinct values present in their CategoryTagLabels should be shown.

My Attempt

I tried to follow Custom Filtering Controls and the FilteringCollectionProperties_WPF from your sample SDK

<RadGridView ...>
  <GridViewBoundColumnBase Header="Tags"  DataMemberBinding="{Binding CategoryTagLabels, Converter={StaticResource ListToStringConverter}}">
    <GridViewBoundColumnBase .FilteringControl>
       <StringListFilterControl FilterMemberName="CategoryTagLabels"/>
    </GridViewBoundColumnBase .FilteringControl>
  </GridViewBoundColumnBase
</RadGridView>

 

StringListFilterControl.xaml

<UserControl x:Class="Core.Controls.ThirdParty.Telerik.StringListFilterControl"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
             xmlns:local="clr-namespace:Core.Controls.ThirdParty.Telerik"
             MinWidth="100" MinHeight="100"
             mc:Ignorable="d"
             d:DesignHeight="450" d:DesignWidth="800">
    <StackPanel>
        <ListView ItemsSource="{Binding Values}" x:Name="TagLabels">
 
        </ListView>
        <Button Content="Apply Filter" Click="ApplyFilter"></Button>
        <Button Content="Clear Filter" Click="ClearFilter"></Button>
    </StackPanel>
</UserControl>

 

StringListFilterControl.xaml.cs

public partial class StringListFilterControl : UserControl, IFilteringControl, INotifyPropertyChanged
    {
        private ICollection<string> _values;
        private GridViewBoundColumnBase _column;
        private CompositeFilterDescriptor _filterDescriptor;
 
        public StringListFilterControl()
        {
            InitializeComponent();
 
            this.DataContext = this;
        }
 
        public void Prepare(GridViewColumn columnToPrepare)
        {
            _column = columnToPrepare as GridViewBoundColumnBase;
            if (_column == null)
            {
                return;
            }
 
            var sender = _column.DataControl;
            var columnValues = ((RadGridView)sender).GetDistinctValues(_column, false);
            var values = new List<string>();
            foreach (var item in columnValues)
            {
                if (item == null) continue;
                values.AddRange((IEnumerable<string>)item);
            }
 
            var distinctValues = values.Distinct().ToList();
 
            Values = distinctValues;
        }
 
        public static readonly DependencyProperty IsActiveProperty = DependencyProperty.Register(
            "IsActive", typeof(bool), typeof(StringListFilterControl), new PropertyMetadata(default(bool)));
 
        public bool IsActive
        {
            get { return (bool) GetValue(IsActiveProperty); }
            set { SetValue(IsActiveProperty, value); }
        }
 
        public ICollection<string> Values
        {
            get => _values;
            set
            {
                _values = value;
                this.RaisePropertyChanged();
            }
        }
 
        public static readonly DependencyProperty FilterMemberNameProperty = DependencyProperty.Register(
            "FilterMemberName", typeof(string), typeof(StringListFilterControl), new PropertyMetadata(default(string)));
 
        public string FilterMemberName
        {
            get { return (string) GetValue(FilterMemberNameProperty); }
            set { SetValue(FilterMemberNameProperty, value); }
        }
 
        public event PropertyChangedEventHandler PropertyChanged;
 
        private void ApplyFilter(object sender, RoutedEventArgs e)
        {
            if (_filterDescriptor == null)
            {
                _filterDescriptor = new CompositeFilterDescriptor();
                _filterDescriptor.LogicalOperator = FilterCompositionLogicalOperator.Or;
            }
 
            if (!_column.DataControl.FilterDescriptors.Contains(_filterDescriptor))
            {
                _column.DataControl.FilterDescriptors.Add(_filterDescriptor);
            }
 
            _filterDescriptor.FilterDescriptors.Clear();
            var selectedTagLabels = TagLabels.SelectedItems;
            foreach (var selectedTagLabel in selectedTagLabels)
            {
                var tagLabel = (string) selectedTagLabel;
                var filter = new TagLabelFilterDescriptor
                {
                    FilterMemberName = FilterMemberName,
                    TagLabel = tagLabel
                };
                _filterDescriptor.FilterDescriptors.Add(filter);
            }
 
            IsActive = true;
        }
 
        private void ClearFilter(object sender, RoutedEventArgs e)
        {
            _column.DataControl.FilterDescriptors.Clear();
            IsActive = false;
        }
    }
 
    public class TagLabelFilterDescriptor : IFilterDescriptor
    {
        private static readonly MethodInfo EnumerableCastMethod = typeof(Enumerable).GetMethod("Cast");
        private static MethodInfo GenericContainsMethod = GetGenericContainsMethodInfo();
 
 
        public event PropertyChangedEventHandler PropertyChanged;
        public string TagLabel { get; set; }
        public string FilterMemberName { get; set; }
        public Expression CreateFilterExpression(Expression instance)
        {
            MemberExpression collectionPropertyAccessor = Expression.Property(instance, FilterMemberName);
 
            MethodCallExpression genericCollectionPropertyAccessor = Expression.Call(null
                , EnumerableCastMethod.MakeGenericMethod(new[] { typeof(object) })
                , collectionPropertyAccessor);
 
            ConstantExpression tagLabel = Expression.Constant(TagLabel);
 
            var expression = Expression.Call(
                GenericContainsMethod,
                genericCollectionPropertyAccessor,
                tagLabel);
 
            return expression;
        }
 
        private static MethodInfo GetGenericContainsMethodInfo()
        {
            // get the Enumerable.Contains<TSource>(IEnumerable<TSource> source, TSource value) method,
            // because it is impossible to get it through Type.GetMethod().
            var methodCall = ((MethodCallExpression)
                (
                    (Expression<Func<IEnumerable<object>, bool>>)(source => source.Contains(null))
                ).Body).Method.GetGenericMethodDefinition();
            return methodCall.MakeGenericMethod(typeof(object));
        }
    }

And this actually works and I like that it is (almost) something I can just attach to a column if the property is a list of string. This is nice as I have properties of collections of strings in other tables, so it is nice when it is easy to reuse the code.

Only problem is that I now have to reimplement the filtering UI from scratch. First of I have to make it look like the other filter controls (which is hard enough) but it also mean that any styling done for the standard filtering control, will have to maintained separately for StringListFilteringControl. 

Is there any way to reuse the standard filtering control?

3 Answers, 1 is accepted

Sort by
0
Dinko | Tech Support Engineer
Telerik team
answered on 25 Jun 2020, 09:45 AM

Hello Peter,

Thank you for the provided code snippet.

Upon investigating your scenario, I wasn't able to find a way to use the default filtering control as you have collection property with custom filtering. What still can try is to extract the default template of the FilteringControl and reused its parts so that you don't have to create it from scratch. You can take a look at the Editing Control Templates help article, which describes how you can get the default template.

As a side note, is there a particular reason to use the GridViewBoundColumnBase class? As this class is used as a based for the columns, I would suggest here to use GridViewDataColumn instead.

Regards,
Dinko
Progress Telerik

Progress is here for your business, like always. Read more about the measures we are taking to ensure business continuity and help fight the COVID-19 pandemic.
Our thoughts here at Progress are with those affected by the outbreak.
0
Peter
Top achievements
Rank 1
answered on 12 Oct 2020, 07:01 AM

Hi

I'm still trying to get this going :)

I have tried to do as you suggest and implement a new FilteringControl. As part of this I have implemented a derived of MemberColumnFilterDescriptor. This is what I got so far.

StringListColumnFilterDescriptor

 

public class StringListColumnFilterDescriptor : MemberColumnFilterDescriptor
{
    private static readonly MethodInfo EnumerableCastMethod = typeof(Enumerable).GetMethod("Cast");
    private static MethodInfo GenericContainsMethod = GetGenericContainsMethodInfo();
 
    public StringListColumnFilterDescriptor(GridViewColumn column) : base(column)
    {
    }
 
    private static MethodInfo GetGenericContainsMethodInfo()
    {
        // get the Enumerable.Contains<TSource>(IEnumerable<TSource> source, TSource value) method,
        // because it is impossible to get it through Type.GetMethod().
        var methodCall = ((MethodCallExpression)((Expression<Func<IEnumerable<object>, bool>>)(source => source.Contains(null))).Body).Method.GetGenericMethodDefinition();
        return methodCall.MakeGenericMethod(typeof(object));
    }
 
    public override Expression CreateFilterExpression(Expression instance)
    {
        string propertyName = base.Member;
 
        MemberExpression collectionPropertyAccessor = Expression.Property(instance, propertyName);
 
        MethodCallExpression genericCollectionPropertyAccessor = Expression.Call(null
            , EnumerableCastMethod.MakeGenericMethod(new[] { typeof(object) })
            , collectionPropertyAccessor);
 
 
        var distinctFilter = ((IColumnFilterDescriptor)this).DistinctFilter;
        var distinctExpressions = new List<Expression>();
        var distinctValues = distinctFilter.DistinctValues.Cast<string>();
        foreach (var distinctValue in distinctValues)
        {
            var distinctValueConstant = Expression.Constant(distinctValue);
            var containsExpression = Expression.Call(
                StringListColumnFilterDescriptor.GenericContainsMethod,
                genericCollectionPropertyAccessor,
                distinctValueConstant
            );
            distinctExpressions.Add(containsExpression);
        }
 
        Expression resultExpression = Expression.Constant(true);
        if (distinctExpressions.Any())
        {
            resultExpression = distinctExpressions[0];
            if (distinctExpressions.Count > 1)
            {
                foreach (var expression in distinctExpressions.Skip(1))
                {
                    resultExpression = Expression.Or(resultExpression, expression);
                }
            }
        }
 
        var logicalFilter = ((IColumnFilterDescriptor) this).FieldFilter.LogicalOperator;
        var field1Filter = ((IColumnFilterDescriptor) this).FieldFilter.Filter1;
        var field2Filter = ((IColumnFilterDescriptor) this).FieldFilter.Filter2;
         
        if (field1Filter.IsActive)
        {
            var searchString = ((TextBox) field1Filter.Value).Text;
 
            // property.Any(x => x.Contains(searchString ))
        }
 
        return resultExpression;
    }
}
 
Now, the problem is the line where I try to find if any of the strings in the property (which is a IEnumerable<string>) contains the searchText. This mainly a matter of LINQ Expression, but head explodes when I'm trying to create that expression.

 

field2Filter and logicalFilter  can be ignored, as I only use one free text filter. 

 

Anyone up for the challange? :)

 

Yours
/peter

 

0
Accepted
Dinko | Tech Support Engineer
Telerik team
answered on 15 Oct 2020, 07:31 AM

Hello Peter,

I will go back to your first and think of a different approach to achieve your scenario. What comes up to my mind is to use additional string property, which converts the List items into a string with come. This way, the column will display string property, and you can use the default filtering control. I have created a sample project to demonstrate this. In the attached project, you can observe that. I have subscribed to the DistinctValuesLoading where I am populating custom values for the distinct collection. Then I have subscribed to the FIltering event of the control. In the event handler, I programmatically filter the column. I know this isn't a perfect solution but you can consider it in your application.

Regards,
Dinko
Progress Telerik

Virtual Classroom, the free self-paced technical training that gets you up to speed with Telerik and Kendo UI products quickly just got a fresh new look + new and improved content including a brand new Blazor course! Check it out at https://learn.telerik.com/.

Tags
GridView
Asked by
Peter
Top achievements
Rank 1
Answers by
Dinko | Tech Support Engineer
Telerik team
Peter
Top achievements
Rank 1
Share this question
or