WPF TreeView - 键盘导航性能缓慢
WPF TreeView - Slow Performance in Keyboard Navigation
我有一个运行时填充的 TreeView。它是这样定义的:
<TreeView ItemsSource="{Binding Path=RootItem.ChildItems}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
...
</TreeView.ItemTemplate>
</TreeView>
我正在使用带有 Style / Setter 的部分,使我可以将两个属性 IsSelected
和 IsExpanded
绑定到同名属性(类型bool
) 的基本项,因为我正在使用它们来确定何时以及如何异步加载子项。
树视图上显示的项目都继承自相同的基础class。我绑定的 属性 看起来像这样:
public bool IsExpanded
{
get { return _IsExpanded; }
set
{
if (HasAsynchChildItems)
if (value)
LoadChildItems();
else if (!value)
Collapse();
_IsExpanded = value;
}
}
HasAsynchChildItems
是一个属性,用于确定是否应异步加载子项。 LoadChildItems()
是加载它们的那个。它是异步的。
只要只有几个项目,一切都很好。
在树下的某个点,有大约 2500 多个项目,应用程序开始变得无响应,加载大约需要 20-30 秒。不仅如此,在加载它们之后,当我更改选择时,应用程序会再次挂起 10 秒以上。
所有导致应用变慢的项目都已 HasAsycnchChildItems
设置为 false。所以LoadChildItems()
没有启动。
是我通过样式setter绑定的问题吗?
PS:我也用 Performance Explorer 监控过,Visual Studio 告诉我 90% 的处理时间在 PresentationFramework.ni.dll
.
中减少了
有什么想法吗?
编辑
经过更多调查,我需要更新问题:
我发现加载真的很快。这些项目在从数据库中检索后几乎立即显示。
选择和扩展也几乎是即时的。
真正停滞的是应用程序挂起,是通过键盘(向上 + 向下箭头)在节点中导航。每个大约需要 15 秒。如果我通过鼠标单击更改选择,它会立即生效。
我怀疑所有项目都被调用了
尝试
public bool IsExpanded
{
get { return _IsExpanded; }
set
{
if (_IsExpanded == value) return;
_IsExpanded = value;
if (HasAsynchChildItems)
{
if (_IsExpanded)
LoadChildItems();
else
Collapse();
}
}
}
一旦您使用 LoadChildItems() 保存结果,您就不必再次加载它们
我找到了某种解决方法。
正如我在上面的评论中所述(因此我再次编辑了我的原始问题)我发现问题既不在 DataBinding 中,也不在任何其他 data-related 问题中。这只是 TreeView 控件本身及其处理键盘导航的方式的问题。
如果您通过 UpArrow 或 DownArrow 在 TreeView 中导航,仅 会出现此问题。
PageDown、PageUp 或 Mouse-Click 不会发生这种情况。
这就是我所做的。首先,我在我的 TreeView 上启用了虚拟化,仅此一项就已经改进了很多东西,但还不令人满意:
<TreeView ItemsSource="{Binding Path=RootItem.Items}" Grid.Row="1"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
我做的第二件事是编写自己的自定义事件处理程序来处理 ArrowUp/ArrowDown 在 TreeView 中的导航。我必须补充一点,我完全不喜欢那样写代码(这也是快速测试/实验的结果,所以也许真的有龙!)。此外:我的 TreeView 中的所有项目都继承自基础 class ManagementItem
,它提供 IsSelected
、IsExpanded
、Index
(指示项目在parent 的 child 项)。
private void TreeView_PreviewKeyDown(object sender, KeyEventArgs e)
{
var tv = sender as TreeView;
var item = tv.SelectedItem as ManagementItem;
e.Handled = false;
if (e.Key == Key.Down)
try
{
if (item.IsExpanded)
item.Items.First().IsSelected = true;
else
if (item.Index == item.Owner.Items.Count - 1)
item.Owner.Owner.Items[item.Owner.Index + 1].IsSelected = true;
else
item.Owner.Items[item.Index + 1].IsSelected = true;
e.Handled = true;
} catch {}
if (e.Key == Key.Up)
try
{
if (item.Index == 0)
item.Owner.IsSelected = true;
else
item.Owner.Items[item.Index - 1].IsSelected = true;
e.Handled = true;
} catch {}
}
这大大提高了导航性能。
我确认了 Hemisphera 的解决方法。它实际上使树视图导航按预期工作。
我有一个具有 2 个初始级别的 TreeView:第一个 5K 节点,第二个级别 20K 个节点(每个父节点约 4 个子节点)。默认 WPF 导航从一个节点移动到另一个节点需要 3-4 秒。此实现在 ~100ms
内完成
实施时需要考虑的几件事:
为您的 TreeViewItems 模型创建基础 class(在 Hemisphera 的代码中,ManagementItem class)。包括所需的属性(如 Hemisphera 的代码所示,IsSelected、IsExpanded、Index 和 Parent 或 Owner)。
不要忘记在您的基础中实施 INotifyPropertyChanged class。此外,您需要在设置 IsExpanded 和 IsSelected 时触发事件。这是 TwoWayBinding 所必需的:
private bool _isExpanded;
public bool IsExpanded
{
get { return _isExpanded; }
set
{
_isExpanded = value;
if (PropertyChanged != null)
PropertyChanged(this, IsExpandedEventsArgs);
}
}
private bool _isSelected;
public bool IsSelected
{
get {return _isSelected;}
set
{
_isSelected = value;
if (PropertyChanged != null)
PropertyChanged(this, IsSelectedEventArgs);
}
}
TreeView_PreviewKeyDown 方法中有一些特殊情况需要考虑。即:更高级别的节点没有父节点(或所有者)。 key up 逻辑,从一个高层节点变为一个展开的低层节点时,还需要处理IsExpanded属性
我有一个运行时填充的 TreeView。它是这样定义的:
<TreeView ItemsSource="{Binding Path=RootItem.ChildItems}">
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
...
</TreeView.ItemTemplate>
</TreeView>
我正在使用带有 Style / Setter 的部分,使我可以将两个属性 IsSelected
和 IsExpanded
绑定到同名属性(类型bool
) 的基本项,因为我正在使用它们来确定何时以及如何异步加载子项。
树视图上显示的项目都继承自相同的基础class。我绑定的 属性 看起来像这样:
public bool IsExpanded
{
get { return _IsExpanded; }
set
{
if (HasAsynchChildItems)
if (value)
LoadChildItems();
else if (!value)
Collapse();
_IsExpanded = value;
}
}
HasAsynchChildItems
是一个属性,用于确定是否应异步加载子项。 LoadChildItems()
是加载它们的那个。它是异步的。
只要只有几个项目,一切都很好。
在树下的某个点,有大约 2500 多个项目,应用程序开始变得无响应,加载大约需要 20-30 秒。不仅如此,在加载它们之后,当我更改选择时,应用程序会再次挂起 10 秒以上。
所有导致应用变慢的项目都已 HasAsycnchChildItems
设置为 false。所以LoadChildItems()
没有启动。
是我通过样式setter绑定的问题吗?
PS:我也用 Performance Explorer 监控过,Visual Studio 告诉我 90% 的处理时间在 PresentationFramework.ni.dll
.
有什么想法吗?
编辑
经过更多调查,我需要更新问题:
我发现加载真的很快。这些项目在从数据库中检索后几乎立即显示。
选择和扩展也几乎是即时的。
真正停滞的是应用程序挂起,是通过键盘(向上 + 向下箭头)在节点中导航。每个大约需要 15 秒。如果我通过鼠标单击更改选择,它会立即生效。
我怀疑所有项目都被调用了
尝试
public bool IsExpanded
{
get { return _IsExpanded; }
set
{
if (_IsExpanded == value) return;
_IsExpanded = value;
if (HasAsynchChildItems)
{
if (_IsExpanded)
LoadChildItems();
else
Collapse();
}
}
}
一旦您使用 LoadChildItems() 保存结果,您就不必再次加载它们
我找到了某种解决方法。
正如我在上面的评论中所述(因此我再次编辑了我的原始问题)我发现问题既不在 DataBinding 中,也不在任何其他 data-related 问题中。这只是 TreeView 控件本身及其处理键盘导航的方式的问题。
如果您通过 UpArrow 或 DownArrow 在 TreeView 中导航,仅 会出现此问题。
PageDown、PageUp 或 Mouse-Click 不会发生这种情况。
这就是我所做的。首先,我在我的 TreeView 上启用了虚拟化,仅此一项就已经改进了很多东西,但还不令人满意:
<TreeView ItemsSource="{Binding Path=RootItem.Items}" Grid.Row="1"
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Recycling"
我做的第二件事是编写自己的自定义事件处理程序来处理 ArrowUp/ArrowDown 在 TreeView 中的导航。我必须补充一点,我完全不喜欢那样写代码(这也是快速测试/实验的结果,所以也许真的有龙!)。此外:我的 TreeView 中的所有项目都继承自基础 class ManagementItem
,它提供 IsSelected
、IsExpanded
、Index
(指示项目在parent 的 child 项)。
private void TreeView_PreviewKeyDown(object sender, KeyEventArgs e)
{
var tv = sender as TreeView;
var item = tv.SelectedItem as ManagementItem;
e.Handled = false;
if (e.Key == Key.Down)
try
{
if (item.IsExpanded)
item.Items.First().IsSelected = true;
else
if (item.Index == item.Owner.Items.Count - 1)
item.Owner.Owner.Items[item.Owner.Index + 1].IsSelected = true;
else
item.Owner.Items[item.Index + 1].IsSelected = true;
e.Handled = true;
} catch {}
if (e.Key == Key.Up)
try
{
if (item.Index == 0)
item.Owner.IsSelected = true;
else
item.Owner.Items[item.Index - 1].IsSelected = true;
e.Handled = true;
} catch {}
}
这大大提高了导航性能。
我确认了 Hemisphera 的解决方法。它实际上使树视图导航按预期工作。
我有一个具有 2 个初始级别的 TreeView:第一个 5K 节点,第二个级别 20K 个节点(每个父节点约 4 个子节点)。默认 WPF 导航从一个节点移动到另一个节点需要 3-4 秒。此实现在 ~100ms
内完成实施时需要考虑的几件事:
为您的 TreeViewItems 模型创建基础 class(在 Hemisphera 的代码中,ManagementItem class)。包括所需的属性(如 Hemisphera 的代码所示,IsSelected、IsExpanded、Index 和 Parent 或 Owner)。
不要忘记在您的基础中实施 INotifyPropertyChanged class。此外,您需要在设置 IsExpanded 和 IsSelected 时触发事件。这是 TwoWayBinding 所必需的:
private bool _isExpanded; public bool IsExpanded { get { return _isExpanded; } set { _isExpanded = value; if (PropertyChanged != null) PropertyChanged(this, IsExpandedEventsArgs); } } private bool _isSelected; public bool IsSelected { get {return _isSelected;} set { _isSelected = value; if (PropertyChanged != null) PropertyChanged(this, IsSelectedEventArgs); } }
TreeView_PreviewKeyDown 方法中有一些特殊情况需要考虑。即:更高级别的节点没有父节点(或所有者)。 key up 逻辑,从一个高层节点变为一个展开的低层节点时,还需要处理IsExpanded属性