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.