2

I've got a hierarchical data structure which i'm trying to visualize with a WPF TreeView and hierarchical data templates. The number of items can reach millions, so i decided to try the virtualization features which work easy and well, besides that one problem which arises when i use "Recycling" instead of "Standard" for the VirtualizationMode. A long time ago someone else seemed to have a similar problem:

"BindingExpression path error" using ItemsControl and VirtualizingStackPanel

Nevertheless, i have troubles to implement a working and performant filtering.

I tried two different approaches, based on hints i could find on the internet, like

The first is to use a Converter to set TreeViewItem.Visibility based on a filter, the second is to create ICollectionView ad hoc for all elements and assign the same filter predicate to each one.

I like the first approach because it requires less code, seems to be more clear, and requires less hacks, and i feel like i had to use that one hack (HackToForceVisibilityUpdate) only because i don't know better, but it slows down the UI considerably and i don't know how to fix that.

The problem with the second approach is that it collapses all nodes when the filter is changed (which i think could be fixed by tracking the states before the filter change and restoring them afterwards) and that it involves a lot of additional code (for the sake of the example, a singleton hack is used to not blow the code up too much and adding/removing items won't work).

I feel like both approaches could be fixed but i cannot figure out how and i really would like to fix the performance issues of the first approach.

It's a lot of code to show in a post and if you don't like the code blocks below, here are the files as pastebin

and as an archive containing a VS solution and

  • a project WpfVirtualizedTreeViewPerItemVisibility for the 1st and
  • a project WpfVirtualizedTreeViewPerItemVisibility3 for the 2nd approach

which can be found here

The first approach:

The data structures:

public class TvItemBase {}
public class TvItemType1 : TvItemBase
{
    public string Name1 { get; set; }
    public List<TvItemBase> Entries { get; } = new List<TvItemBase>();
}
public class TvItemType2 : TvItemBase
{
    public string Name2 { get; set; }
    public int i { get; set; }
}

The converter looks like this (it corresponds to the "filterFunc" in the 2nd approach)

public class TvItemType2VisibleConverter : IMultiValueConverter
{
    public TvItemType2VisibleConverter() { }
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        if (values.Length < 2)
            return Visibility.Visible;
        var tvItem = values[0] as TreeViewItem;
        if (tvItem == null)
            return Visibility.Visible;
        var entry = tvItem.DataContext as TvItemType2;
        if (entry == null)
            return Visibility.Visible;
        var model = values[1] as IFilterProvider;
        if (model == null)
            return Visibility.Visible;

        if (!model.ShowA)
            return Visibility.Collapsed;
        else if (entry.i % 2 == 0)
            return Visibility.Collapsed;
        else
            return Visibility.Visible;
    }
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture) { throw new System.NotImplementedException(); }
}

and the window impl, which is also the view model, looks like this

public interface IFilterProvider
{
    bool ShowA { get; }
}
public partial class MainWindow : Window, INotifyPropertyChanged, IFilterProvider
{
    public event PropertyChangedEventHandler PropertyChanged;
    void NotifyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }

    bool ShowA_ = true;
    public bool ShowA
    {
        get { return ShowA_; }
        set { ShowA_ = value; NotifyChanged(nameof(ShowA)); NotifyChanged(nameof(HackToForceVisibilityUpdate)); }
    }
    public bool HackToForceVisibilityUpdate { get { return true; } }

    void generateTestItems(TvItemType1 parent, int nof1, int nof2, int levels)
    {
        for (int i = 0; i < nof1; i++)
        {
            var i1 = new TvItemType1 { Name1 = string.Format("F_{0}.{1}.{2}.{3}", levels, i, nof1, nof2) };
            parent.Entries.Add(i1);
            if (levels > 0)
                generateTestItems(i1, nof1, nof2, levels - 1);
        }
        for (int i = 0; i < nof2; i++)
            parent.Entries.Add(new TvItemType2 { Name2 = string.Format("{0}.{1}.{2}.{3}", levels, nof1 + i, nof1, nof2), i = nof1 + i });
    }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;

        var i1 = new TvItemType1 { Name1 = "root" };
        generateTestItems(i1, 10, 1000, 3);
        tv.ItemsSource = new List<TvItemBase> { i1 };
    }
}

Finally, here's the xaml:

<Window.Resources>
    <local:TvItemType2VisibleConverter x:Key="TvItemType2VisibleConverter"/>
    <HierarchicalDataTemplate DataType="{x:Type local:TvItemType1}" ItemsSource="{Binding Entries}">
        <TextBlock Text="{Binding Name1}" />
        <HierarchicalDataTemplate.ItemContainerStyle>
            <Style TargetType="TreeViewItem">
                <Setter Property="Visibility">
                    <Setter.Value>
                        <MultiBinding Converter="{StaticResource TvItemType2VisibleConverter}">
                            <Binding RelativeSource="{RelativeSource Self}" />
                            <!-- todo: how to specify the filter provider through a view model property (using Path and Source?) -->
                            <Binding RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:IFilterProvider}" />
                            <!-- todo: how to enforce filter reevaluation without this hack -->
                            <Binding Path="HackToForceVisibilityUpdate" RelativeSource="{RelativeSource Mode=FindAncestor, AncestorType=local:IFilterProvider}" />
                        </MultiBinding>
                    </Setter.Value>
                </Setter>
                <Setter Property="IsExpanded" Value="True" />
            </Style>
        </HierarchicalDataTemplate.ItemContainerStyle>
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate DataType="{x:Type local:TvItemType2}">
        <TextBlock Text="{Binding Name2}" />
    </HierarchicalDataTemplate>
</Window.Resources>
<Grid>
    <ToggleButton Content="A" IsChecked="{Binding ShowA, Mode=TwoWay}" Width="20" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" />
    <TreeView x:Name="tv"
              ScrollViewer.VerticalScrollBarVisibility="Visible" 
              ScrollViewer.CanContentScroll="True"
              VirtualizingStackPanel.IsVirtualizing="True"
              VirtualizingStackPanel.VirtualizationMode="Standard" Margin="0,24,0,0" />
    <!-- todo: when using VirtualizingStackPanel.VirtualizationMode="Recycling", a lot of 
    System.Windows.Data Error: 40 : BindingExpression path error: 'Entries' property not found on 'object' ''TvItemType2' (HashCode=...)'. BindingExpression:Path=Entries; DataItem='TvItemType2' (HashCode=...); target element is 'TreeViewItem' (Name=''); target property is 'ItemsSource' (type 'IEnumerable')
    are flooding the output window.
    See f.e. https://stackoverflow.com/questions/4950208/bindingexpression-path-error-using-itemscontrol-and-virtualizingstackpanel -->
</Grid>

Code for the second approach:

The data structures:

public class TvItemBase {}
public class TvItemType1 : TvItemBase
{
    public string Name1 { get; set; }
    public List<TvItemBase> Entries { get; } = new List<TvItemBase>();
    public ICollectionView FilteredEntries
    {
        get
        {
            var dv = CollectionViewSource.GetDefaultView(Entries);
            dv.Filter = MainWindow.singleton.filterFunc; // todo:hack
            return dv;
        }
    }
}
public class TvItemType2 : TvItemBase
{
    public string Name2 { get; set; }
    public int i { get; set; }
}

and the window impl, which is also the view model, looks like this

public interface IFilterProvider
{
    bool ShowA { get; }
}

public partial class MainWindow : Window, INotifyPropertyChanged, IFilterProvider
{
    public event PropertyChangedEventHandler PropertyChanged;
    void NotifyChanged(string name) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name)); }

    bool ShowA_ = true;
    public bool ShowA
    {
        get { return ShowA_; }
        set
        {
            ShowA_ = value;
            // todo:hack
            // todo:why does this update the whole tree
            (tv.ItemsSource as ICollectionView).Refresh();
            NotifyChanged(nameof(ShowA));
        }
    }

    void generateTestItems(TvItemType1 parent, int nof1, int nof2, int levels)
    {
        for (int i = 0; i < nof1; i++)
        {
            var i1 = new TvItemType1 { Name1 = string.Format("F_{0}.{1}.{2}.{3}", levels, i, nof1, nof2) };
            parent.Entries.Add(i1);
            if (levels > 0)
                generateTestItems(i1, nof1, nof2, levels - 1);
        }
        for (int i = 0; i < nof2; i++)
            parent.Entries.Add(new TvItemType2 { Name2 = string.Format("{0}.{1}.{2}.{3}", levels, nof1 + i, nof1, nof2), i = nof1 + i });
    }

    public bool filterFunc(object obj)
    {
        var entry = obj as TvItemType2;
        if (entry == null)
            return true;
        var model = this;

        if (!model.ShowA)
            return false;
        else if (entry.i % 2 == 0)
            return false;
        else
            return true;
    }

    public MainWindow()
    {
        InitializeComponent();
        DataContext = this;
        singleton = this; // todo:hack

        var i1 = new TvItemType1 { Name1 = "root" };
        generateTestItems(i1, 10, 1000, 3);
        //generateTestItems(i1, 3, 10, 3);
        var l = new List<TvItemBase> { i1 };
        var dv = CollectionViewSource.GetDefaultView(l);
        dv.Filter = filterFunc;
        tv.ItemsSource = dv;
    }
    public static MainWindow singleton = null; // todo:[really big]hack
}

Finally, here's the xaml:

<Window.Resources>
    <HierarchicalDataTemplate DataType="{x:Type local:TvItemType1}" ItemsSource="{Binding FilteredEntries}">
        <TextBlock Text="{Binding Name1}" />
        <HierarchicalDataTemplate.ItemContainerStyle>
            <Style TargetType="TreeViewItem">
                <Setter Property="IsExpanded" Value="True" />
            </Style>
        </HierarchicalDataTemplate.ItemContainerStyle>
    </HierarchicalDataTemplate>
    <HierarchicalDataTemplate DataType="{x:Type local:TvItemType2}">
        <TextBlock Text="{Binding Name2}" />
    </HierarchicalDataTemplate>
</Window.Resources>
<Grid>
    <ToggleButton Content="A" IsChecked="{Binding ShowA, Mode=TwoWay}" Width="20" Height="20" HorizontalAlignment="Left" VerticalAlignment="Top" />
    <TreeView x:Name="tv"
              ScrollViewer.VerticalScrollBarVisibility="Visible" 
              ScrollViewer.CanContentScroll="True"
              VirtualizingStackPanel.IsVirtualizing="True"
              VirtualizingStackPanel.VirtualizationMode="Standard" Margin="0,24,0,0" />
</Grid>
wonko realtime
  • 545
  • 9
  • 26
  • Have you given any though to having a UI design that doesn't require millions of items in a treeview simultaneously? – Robert Harvey Mar 07 '19 at 20:07
  • @RobertHarvey The use case is a regression test with hundreds of our projects, generating thausands of files each, which will be automatically compared by parsing and listing the comparison results in a tree if there are major differences. The desired result are no or only minor differences and then there's no need to display all these files or even a tree at all, but when strange patterns arise, it's beneficial to look at the difference sets simultaneously. But also if it's just for academic reasons, i'd like to visualize huge containers really performant. – wonko realtime Mar 07 '19 at 20:56

0 Answers0