过滤WPF虚拟TreeView的高效方法

时间:2019-03-07 17:54:01

标签: c# wpf treeview

我有一个分层的数据结构,我正在尝试使用WPF TreeView和分层的数据模板进行可视化。项目数量可以达到数百万,因此我决定尝试易于使用且性能良好的虚拟化功能,此外,当我在VirtualizationMode中使用“回收”而不是“标准”时会出现一个问题。很久以前,其他人似乎也有类似的问题:

"BindingExpression path error" using ItemsControl and VirtualizingStackPanel

尽管如此,我仍然难以实施有效且高效的过滤功能。

我尝试了两种不同的方法,基于我可以在互联网上找到的提示,例如

第一个方法是使用Converter来基于过滤器设置TreeViewItem.Visibility,第二个方法是为所有元素创建ICollectionView ad hoc并为每个元素分配相同的过滤谓词。

我喜欢第一种方法,因为它需要更少的代码,看起来更清晰,并且需要更少的hack,而且我觉得我不得不使用一个hack(HackToForceVisibilityUpdate)只是因为我不了解,但是大大降低了用户界面的速度,我不知道该如何解决。

第二种方法的问题在于,当更改过滤器时,它会折叠所有节点(我认为可以通过更改过滤器之前跟踪状态并在之后恢复状态来解决此问题),并且涉及很多其他代码(出于示例的目的,使用了单例hack来避免过多地破坏代码,并且添加/删除项将无法正常工作。

我觉得这两种方法都可以解决,但是我不知道如何解决,我真的很想解决第一种方法的性能问题。

帖子中显示了很多代码,如果您不喜欢下面的代码块,则这些文件为pastebin

并作为包含VS解决方案和

的存档
  • 第一个项目的WpfVirtualizedTreeViewPerItemVisibility和
  • 用于第二种方法的项目WpfVirtualizedTreeViewPerItemVisibility3

可以找到here

第一种方法:

数据结构:

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; }
}

转换器看起来像这样(它对应于第二种方法中的“ filterFunc”)

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(); }
}

和也是视图模型的window impl看起来像这样

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 };
    }
}

最后,这是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>

第二种方法的代码:

数据结构:

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; }
}

和也是视图模型的window impl看起来像这样

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
}

最后,这是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>

0 个答案:

没有答案