Filtering in a HierarchicalDataTemplate via MarkupExtension?

Posted by Dan Bryant on Stack Overflow See other posts from Stack Overflow or by Dan Bryant
Published on 2010-05-05T15:40:40Z Indexed on 2010/05/05 19:48 UTC
Read the original article Hit count: 717

I'm trying to create a MarkupExtension to allow filtering of items in an ItemsSource of a HierarchicalDataTemplate. In particular, I'd like to be able to supply a method name that will be executed on the DataContext in order to perform the filtering. The usage syntax I'm after looks like this:

    <HierarchicalDataTemplate DataType="{x:Type src:DeviceBindingViewModel}" 
                              ItemsSource="{Utilities:FilterCollection {Binding Definition.Entries}, MethodName=FilterEntries}">
        <StackPanel Orientation="Horizontal">
            <Image Source="{StaticResource BindingImage}" Width="24" Height="24" Margin="3"/>
            <TextBlock Text="{Binding DisplayName}" FontSize="12" VerticalAlignment="Center"/>
        </StackPanel>
    </HierarchicalDataTemplate>

My code for the custom MarkupExtension looks like this:

public sealed class FilterCollectionExtension : MarkupExtension
{
    private readonly MultiBinding _binding;
    private Predicate<Object> _filterMethod;

    public string MethodName { get; set; }

    public FilterCollectionExtension(Binding binding)
    {
        _binding =  new MultiBinding();
        _binding.Bindings.Add(binding);

        //We package a reference to the DataContext with the binding so that the Converter has access to it
        var selfBinding = new Binding {RelativeSource = RelativeSource.Self};
        _binding.Bindings.Add(selfBinding);

        _binding.Converter = new InternalConverter(this);
    }

    public FilterCollectionExtension(Binding binding, string methodName) 
        : this(binding)
    {
        MethodName = methodName;
    }

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return _binding;
    }

    private bool FilterInternal(Object dataContext, Object value)
    {
        //Filtering is only applicable if a DataContext is defined
        if (dataContext != null)
        {
            if (_filterMethod == null)
            {
                var type = dataContext.GetType();
                var method = type.GetMethod(MethodName, new[] { typeof(Object) });
                if (method == null || method.ReturnType != typeof(bool))
                    throw new InvalidOperationException("Could not locate a filter predicate named " + MethodName + " on the DataContext");
                _filterMethod = (Predicate<Object>)Delegate.CreateDelegate(typeof(Predicate<Object>), dataContext, method);
            }
            else
            {
                if (_filterMethod.Target != dataContext)
                {
                    _filterMethod =
                        (Predicate<Object>) Delegate.CreateDelegate(typeof (Predicate<Object>), dataContext,
                                                                    _filterMethod.Method);
                }
            }

            if (_filterMethod != null)
                return _filterMethod(value);
        }

        //If no filtering resolved, just allow all elements
        return true;
    }

    private class InternalConverter : IMultiValueConverter
    {
        private readonly FilterCollectionExtension _owner;

        public InternalConverter(FilterCollectionExtension owner)
        {
            _owner = owner;
        }

        public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            var enumerable = values[0];
            var targetElement = (FrameworkElement)values[1];

            var view = CollectionViewSource.GetDefaultView(enumerable);
            view.Filter = item => _owner.FilterInternal(targetElement.DataContext, item);

            return view;
        }

        public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotSupportedException("Cannot convert back");
        }
    }
}

I can see that the extension is instantiated and I can see it return the MultiBinding that is used by the Template. I also see the call to the InternalConverter.Convert method, which sees the expected parameters (I see the collection provided by the nested {Binding}) and is successfully able to retrieve the ICollectionView for the incoming collection. The only problem is that FilterInternal never gets called.

The template is ultimately being used by a TreeView, if that's relevant. I haven't been able to figure out why the FilterInternal method is not being called and I was hoping somebody might be able to offer some insight.

© Stack Overflow or respective owner

Related posts about wpf

Related posts about hierarchicaldatatemplate