最近我一直在使用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;
}
}
TreeViewEntry
是我的后备数据类型,它引用了其父节点。 Leftside_TreeView
是绑定到我的对象列表的虚拟TreeView。关闭虚拟化不是一个选择,因为关闭它会导致性能下降。
当我搜索一个对象并找到支持数据的对象时,我以对象为参数调用此FocusTreeViewNode()方法。通常,它将在第一次调用时工作,选择对象并将其显示。
第二次执行搜索时,将传入要选择的节点,但是,当清空堆栈(因此它将尝试为对象本身生成容器)时,ContainerFromItem()调用将返回null。当我调试它时,我可以在ContainerGenerator的项目列表中看到要搜索的对象,但是由于某种原因它没有被返回。我查找了所有与UpdateLayout()有关的内容以及其他内容,但我无法弄清楚。
即使在父节点进入视图后,容器中的某些对象也可能不在页面上-例如扩展器下面有250个项目,一次只能渲染60个项目。这可能是个问题吗?
更新
这里是一个示例项目,该示例项目制作了一个虚拟的树状视图来显示此问题。 https://github.com/Mgamerz/TreeViewVirtualizingErrorDemo
在VS中进行构建,然后在搜索框中输入类似4的内容。按几次搜索,即使您打开generator
对象,您也可以清楚地看到容器将为空,这将引发异常。在发电机中。
答案 0 :(得分:3)
与WPF开发的许多其他方面一样,可以使用MVVM设计模式来处理此操作。
创建一个包含IsSelected属性的ViewModel类,该类保存每个树项的数据。
然后可以通过附加属性处理将所选项目带入视图
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;
}
}
然后可以将其用作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>
我最近的blog post上的更多详细信息和用法的完整示例。
更新10月13日
博客文章已在标准(非延迟加载模式)下运行时进行了修改。关联的演示项目显示了TreeView中显示的超过40万个元素的嵌套数据结构,但是选择任何随机节点的响应都是即时的。
答案 1 :(得分:1)
在所有情况下,尤其是虚拟化情况下,很难获得给定数据项的TreeViewItem
。
幸运的是,Microsoft在How to: Find a TreeViewItem in a TreeView处为我们提供了我们已经改编的帮助程序功能,因此它不需要自定义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;
}