c# - 过滤 WPF 虚拟化 TreeView 的高效方法
问题描述
我有一个分层数据结构,我试图用 WPF TreeView 和分层数据模板对其进行可视化。项目的数量可以达到数百万,因此我决定尝试使用简单且运行良好的虚拟化功能,除了当我使用“回收”而不是“标准”作为虚拟化模式时出现的一个问题。很久以前,其他人似乎也有类似的问题:
使用 ItemsControl 和 VirtualizingStackPanel 的“BindingExpression 路径错误”
尽管如此,我在实现工作和高性能过滤方面遇到了麻烦。
根据我在互联网上可以找到的提示,我尝试了两种不同的方法,比如
- 如何在 WPF 中过滤 TreeView?
- 在 WPF 中,您可以在没有代码的情况下过滤 CollectionViewSource 吗?
- 使用 MVVM 过滤 WPF TreeView
- 如何使用 ICollectionView 过滤 wpf 树视图层次结构?
- WPF:过滤TreeView而不折叠它的节点
- 过滤树视图
第一种是使用 Converter 来基于过滤器设置 TreeViewItem.Visibility,第二种是为所有元素创建 ICollectionView ad hoc 并为每个元素分配相同的过滤谓词。
我喜欢第一种方法,因为它需要更少的代码,看起来更清晰,并且需要更少的 hack,我觉得我不得不使用那个 hack (HackToForceVisibilityUpdate) 只是因为我不知道更好,但它减慢了用户界面相当,我不知道如何解决这个问题。
第二种方法的问题在于,当过滤器更改时它会折叠所有节点(我认为可以通过跟踪过滤器更改之前的状态并在之后恢复它们来修复)并且它涉及很多额外的代码(为了在这个例子中,一个单例黑客用于不把代码炸得太多,并且添加/删除项目将不起作用)。
我觉得这两种方法都可以解决,但我不知道如何并且我真的想解决第一种方法的性能问题。
在帖子中显示很多代码,如果您不喜欢下面的代码块,这里是 pastebin 文件
并作为包含 VS 解决方案的存档和
- 第一个项目 WpfVirtualizedTreeViewPerItemVisibility 和
- 第二种方法的项目 WpfVirtualizedTreeViewPerItemVisibility3
可以在这里找到
第一种方法:
数据结构:
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(); }
}
窗口 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; }
}
窗口 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>
解决方案
推荐阅读
- nginx - 在 nginx、gunicorn、supervisor 上更新 .py 文件
- python - numpy dtype float 扩展/附加浮动值
- apache-spark - 调用 sc.textFile("...").take(1)[0] 时出现 StringIndexOutOfBoundsException
- reactjs - 如何使用 componentDidMount() 中的 json 格式从 db 获取数据
- swift - 如何在 Swift 中将字符串转换为 unicode 系列?
- xml - 如何比较两个 XML 文件并仅返回修改后的元素
- angular - Angular Reactive Forms - 让嵌套子项确定启用/禁用
- sql - 何时删除 Teradata SQL 中的额外列?
- java - 如何在一项活动 Android 中创建实例变量?
- embed - 在另一个站点上以开发模式嵌入 create-react-app