Filtering in a HierarchicalDataTemplate via MarkupExtension?
- by Dan Bryant
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.