C# 过滤WPF虚拟化树状视图的性能方法

C# 过滤WPF虚拟化树状视图的性能方法,c#,wpf,treeview,C#,Wpf,Treeview,我有一个层次化的数据结构,我正试图用WPF树视图和层次化的数据模板来可视化它。项目的数量可以达到数百万,因此我决定尝试工作简单且良好的虚拟化功能,除了在虚拟化模式中使用“回收”而不是“标准”时出现的一个问题。很久以前,其他人似乎也有类似的问题: 然而,我很难实现工作和性能过滤 我尝试了两种不同的方法,基于互联网上的提示,比如 第一种方法是使用转换器来设置基于过滤器的TreeView.Visibility,第二种方法是为所有元素创建ICollectionView即席视图,并为每个

我有一个层次化的数据结构,我正试图用WPF树视图和层次化的数据模板来可视化它。项目的数量可以达到数百万,因此我决定尝试工作简单且良好的虚拟化功能,除了在虚拟化模式中使用“回收”而不是“标准”时出现的一个问题。很久以前,其他人似乎也有类似的问题:

然而,我很难实现工作和性能过滤

我尝试了两种不同的方法,基于互联网上的提示,比如

第一种方法是使用转换器来设置基于过滤器的TreeView.Visibility,第二种方法是为所有元素创建ICollectionView即席视图,并为每个元素指定相同的过滤器谓词

我喜欢第一种方法,因为它需要更少的代码,看起来更清晰,需要更少的黑客,我觉得我不得不使用这一种黑客(HackToForceVisibilityUpdate),只是因为我不太清楚,但它大大降低了UI的速度,我不知道如何修复它

第二种方法的问题是,当过滤器改变时,它会折叠所有节点(我认为可以通过跟踪过滤器改变前的状态并在改变后恢复状态来修复),并且它涉及大量额外的代码(在本例中,我们使用单例黑客来避免过多地破坏代码,从而无法添加/删除项)

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

在文章中要显示的代码很多,如果您不喜欢下面的代码块,以下是作为pastebin的文件

并作为包含VS解决方案和

  • 项目WpfVirtualizedTreeViewPerItemVisibility用于第一个和第二个
  • 第二种方法的项目WPFvirtualizedTreeViewPerItemVisibility 3
可以找到

第一种方法:

数据结构:

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; }
}
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; }
}
public类TvItemBase{}
公共类TVTItemType1:TVTItemBase
{
公共字符串名称1{get;set;}
公共列表项{get;}=new List();
}
公共类TVTItemType2:TVTItemBase
{
公共字符串名称2{get;set;}
公共整数i{get;set;}
}
转换器如下所示(它对应于第二种方法中的“filterFunc”)

公共类TVTItemType2VisibleConverter:IMultiValueConverter
{
公共TVTItemType2VisibleConverter(){}
公共对象转换(对象[]值,类型targetType,对象参数,System.Globalization.CultureInfo区域性)
{
如果(值。长度<2)
返回可见性。可见;
var TviItem=值[0]作为树项;
if(tviitem==null)
返回可见性。可见;
var entry=tviitem.DataContext作为tviitemtype2;
if(条目==null)
返回可见性。可见;
var模型=作为IFilterProvider的值[1];
if(model==null)
返回可见性。可见;
如果(!model.ShowA)
返回可见性。折叠;
else if(条目.i%2==0)
返回可见性。折叠;
其他的
返回可见性。可见;
}
公共对象[]ConvertBack(对象值,类型[]targetTypes,对象参数,System.Globalization.CultureInfo区域性){抛出新系统。NotImplementedException();}
}
窗口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 };
    }
}
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
}
公共接口IFilterProvider
{
bool ShowA{get;}
}
公共部分类主窗口:窗口,INotifyPropertyChanged,IFilterProvider
{
公共事件属性更改事件处理程序属性更改;
void NotifyChanged(字符串名称){PropertyChanged?.Invoke(这个,新的PropertyChangedEventArgs(名称));}
bool ShowA=真;
昭和公馆
{
获取{return ShowA;}
设置{ShowA_uz=value;NotifyChanged(nameof(ShowA));NotifyChanged(nameof(hacktoforcevibilityupdate));}
}
公共bool hacktoforcevibilityupdate{get{return true;}}
void generateTestItems(TvItemType1父项、int-nof1、int-nof2、int-levels)
{
对于(int i=0;i0)
发电机系统(i1、nof1、nof2,级别-1);
}
对于(int i=0;i
最后,这里是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>
<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>

第二种方法的代码:

数据结构:

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; }
}
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; }
}
public类TvItemBase{}
公共类TVTItemType1:TVTItemBase
{
公共字符串名称1{get;set;}
公共列表项{get;}=new List();
公共ICollectionView过滤器设备
{
得到
{
var dv=CollectionViewSource.GetDefaultView(条目);
dv.Filter=MainWindow.sing