我最近一直在使用WPF树视图,当用户使用在后备对象上设置IsSelected属性的搜索功能时,我正试图让所选项目显示在屏幕上时非常糟糕.
目前我的方法是使用这个答案中的方法:https://stackoverflow.com/a/34620549/800318
private void FocusTreeViewNode(TreeViewEntry node)
{
if (node == null) return;
var nodes = (IEnumerable<TreeViewEntry>)LeftSide_TreeView.ItemsSource;
if (nodes == null) return;
var stack = new Stack<TreeViewEntry>();
stack.Push(node);
var parent = node.Parent;
while (parent != null)
{
stack.Push(parent);
parent = parent.Parent;
}
var generator = LeftSide_TreeView.ItemContainerGenerator;
while (stack.Count > 0)
{
var dequeue = stack.Pop();
LeftSide_TreeView.UpdateLayout();
var treeViewItem = (TreeViewItem)generator.ContainerFromItem(dequeue);
if (stack.Count > 0)
{
treeViewItem.IsExpanded = true;
}
else
{
if (treeViewItem == null)
{
//This is being triggered when it shouldn't be
Debugger.Break();
}
treeViewItem.IsSelected = true;
}
treeViewItem.BringIntoView();
generator = treeViewItem.ItemContainerGenerator;
}
}
Run Code Online (Sandbox Code Playgroud)
TreeViewEntry是我的后备数据类型,它具有对其父节点的引用.Leftside_TreeView是绑定到我的对象列表的虚拟化TreeView.关闭虚拟化不是一种选择,因为性能非常糟糕.
当我搜索一个对象并找到后备数据对象时,我将该对象作为其参数调用此FocusTreeViewNode()方法.它通常在第一次调用时工作,选择对象并将其带入视图.
在第二次执行搜索时,将传入要选择的节点,但是当清空堆栈时,ContainerFromItem()调用(因此它尝试为对象本身生成容器)返回null.当我调试这个时,我可以在ContainerGenerator的项目列表中看到我正在搜索的对象,但由于某种原因它没有被返回.我查看了所有与UpdateLayout()和其他事情有关的事情,但我无法弄清楚这一点.
即使在父节点进入视图之后,容器中的一些对象也可能在页面外 - 例如,扩展器在其下面有250个项目,并且在时间上仅渲染60个.这可能是个问题吗?
更新
这是一个示例项目,它使虚拟化树视图显示此问题.https://github.com/Mgamerz/TreeViewVirtualizingErrorDemo
在VS中构建它,然后在搜索框中输入类似的东西4.按几次搜索,它会抛出一个异常,说容器为空,即使你打开generator对象,你也可以清楚地看到它在生成器中.
与 WPF 开发的许多其他方面一样,可以使用 MVVM 设计模式来处理此操作。
创建一个 ViewModel 类,包括一个 IsSelected 属性,该属性保存每个树项的数据。
然后可以通过附加属性处理将所选项目置于视图中
public static class perTreeViewItemHelper
{
public static bool GetBringSelectedItemIntoView(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(BringSelectedItemIntoViewProperty);
}
public static void SetBringSelectedItemIntoView(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(BringSelectedItemIntoViewProperty, value);
}
public static readonly DependencyProperty BringSelectedItemIntoViewProperty =
DependencyProperty.RegisterAttached(
"BringSelectedItemIntoView",
typeof(bool),
typeof(perTreeViewItemHelper),
new UIPropertyMetadata(false, BringSelectedItemIntoViewChanged));
private static void BringSelectedItemIntoViewChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (!(args.NewValue is bool))
return;
var item = obj as TreeViewItem;
if (item == null)
return;
if ((bool)args.NewValue)
item.Selected += OnTreeViewItemSelected;
else
item.Selected -= OnTreeViewItemSelected;
}
private static void OnTreeViewItemSelected(object sender, RoutedEventArgs e)
{
var item = e.OriginalSource as TreeViewItem;
item?.BringIntoView();
// prevent this event bubbling up to any parent nodes
e.Handled = true;
}
}
Run Code Online (Sandbox Code Playgroud)
然后可以将其用作 TreeViewItems 样式的一部分
<Style x:Key="perTreeViewItemContainerStyle"
TargetType="{x:Type TreeViewItem}">
<!-- Link the properties of perTreeViewItemViewModelBase to the corresponding ones on the TreeViewItem -->
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="IsEnabled" Value="{Binding IsEnabled}" />
<!-- Include the two "Scroll into View" behaviors -->
<Setter Property="vhelp:perTreeViewItemHelper.BringSelectedItemIntoView" Value="True" />
<Setter Property="vhelp:perTreeViewItemHelper.BringExpandedChildrenIntoView" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TreeViewItem}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="14" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<ToggleButton x:Name="Expander"
Grid.Row="0"
Grid.Column="0"
ClickMode="Press"
IsChecked="{Binding Path=IsExpanded, RelativeSource={RelativeSource TemplatedParent}}"
Style="{StaticResource perExpandCollapseToggleStyle}" />
<Border x:Name="PART_Border"
Grid.Row="0"
Grid.Column="1"
Padding="{TemplateBinding Padding}"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<ContentPresenter x:Name="PART_Header"
Margin="0,2"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
ContentSource="Header" />
</Border>
<ItemsPresenter x:Name="ItemsHost"
Grid.Row="1"
Grid.Column="1" />
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="IsExpanded" Value="false">
<Setter TargetName="ItemsHost" Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="HasItems" Value="false">
<Setter TargetName="Expander" Property="Visibility" Value="Hidden" />
</Trigger>
<!-- Use the same colors for a selected item, whether the TreeView is focussed or not -->
<Trigger Property="IsSelected" Value="true">
<Setter TargetName="PART_Border" Property="Background" Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.HighlightTextBrushKey}}" />
</Trigger>
<Trigger Property="IsEnabled" Value="false">
<Setter Property="Foreground" Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type TreeView}">
<Setter Property="ItemContainerStyle" Value="{StaticResource perTreeViewItemContainerStyle}" />
</Style>
Run Code Online (Sandbox Code Playgroud)
在我最近的博客文章中提供了更多详细信息和完整的使用示例。
10 月 13 日更新
博客文章已针对在标准(非延迟加载模式)下运行时进行了修改。相关的演示项目显示了在 TreeView 中显示的超过 400,000 个元素的嵌套数据结构,但对选择任何随机节点的响应是即时的。
TreeViewItem在所有情况下,尤其是虚拟化的情况下,获取给定数据项的是相当困难的。
幸运的是,微软在这里为我们提供了一个辅助函数如何:在我已经改编的TreeView中查找TreeViewItem,因此它不需要自定义VirtualizingStackPanel类(需要.NET Framework 4.5或更高版本,对于旧版本,请参阅上面的链接)。
以下是替换FocusTreeViewNode方法的方法:
private void FocusTreeViewNode(MenuItem node)
{
if (node == null)
return;
var treeViewItem = GetTreeViewItem(tView, node);
treeViewItem?.BringIntoView();
}
public static TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
if (container == null)
throw new ArgumentNullException(nameof(container));
if (item == null)
throw new ArgumentNullException(nameof(item));
if (container.DataContext == item)
return container as TreeViewItem;
if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
{
container.SetValue(TreeViewItem.IsExpandedProperty, true);
}
container.ApplyTemplate();
if (container.Template.FindName("ItemsHost", container) is ItemsPresenter itemsPresenter)
{
itemsPresenter.ApplyTemplate();
}
else
{
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
if (itemsPresenter == null)
{
container.UpdateLayout();
itemsPresenter = FindVisualChild<ItemsPresenter>(container);
}
}
var itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);
var children = itemsHostPanel.Children;
var virtualizingPanel = itemsHostPanel as VirtualizingPanel;
for (int i = 0, count = container.Items.Count; i < count; i++)
{
TreeViewItem subContainer;
if (virtualizingPanel != null)
{
// this is the part that requires .NET 4.5+
virtualizingPanel.BringIndexIntoViewPublic(i);
subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
}
else
{
subContainer = (TreeViewItem)container.ItemContainerGenerator.ContainerFromIndex(i);
subContainer.BringIntoView();
}
if (subContainer != null)
{
TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);
if (resultContainer != null)
return resultContainer;
subContainer.IsExpanded = false;
}
}
return null;
}
private static T FindVisualChild<T>(Visual visual) where T : Visual
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(visual); i++)
{
if (VisualTreeHelper.GetChild(visual, i) is Visual child)
{
if (child is T item)
return item;
item = FindVisualChild<T>(child);
if (item != null)
return item;
}
}
return null;
}
Run Code Online (Sandbox Code Playgroud)