虚拟化的 WPF TreeView 滚动非常不稳定。

34

如果在拥有不同大小项的TreeView中启用了虚拟化,则会出现多个问题:

  • 垂直滚动条会随机更改其大小,并且在查看整个树后不会记住元素的大小,使用鼠标滚动很困难。

  • 上下滚动一段时间后,框架代码会抛出ArgumentNullException异常。

复现很简单:创建一个新的WPF应用程序,然后将此代码放入 MainWindow.xaml 中。

<Window x:Class="VirtualTreeView.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="800" Width="400" Left="0" Top="0"
        DataContext="{Binding RelativeSource={RelativeSource Self}}">
    <Grid>
        <TreeView x:Name="tvwItems" ItemsSource="{Binding Items}"
                VirtualizingPanel.IsVirtualizing="True" VirtualizingPanel.VirtualizationMode="Recycling">
            <TreeView.ItemTemplate>
                <DataTemplate>
                    <Border Height="{Binding Height}" Width="{Binding Height}"
                            BorderThickness="1" Background="DarkGray" BorderBrush="DarkBlue"/>
                </DataTemplate>
            </TreeView.ItemTemplate>
        </TreeView>
    </Grid>
</Window>

将此代码复制粘贴到MainWindow.xaml.cs中。
using System.Collections.ObjectModel;
using System.Linq;

namespace VirtualTreeView
{
    public partial class MainWindow
    {
        public ObservableCollection<Item> Items { get; set; }

        public MainWindow ()
        {
            Items = new ObservableCollection<Item>(Enumerable.Range(0, 20).Select(i => new Item {
                Height = i*20,
            }));
            InitializeComponent();
        }
    }

    public class Item
    {
        public double Height { get; set; }
    }
}

当应用程序运行时,将鼠标指针移动到树形视图中,使用鼠标滚轮向下滚动到底部,然后向上滚动,再开始向下滚动。在中间的某个位置,会抛出以下异常:

System.ArgumentNullException was unhandled
  HResult=-2147467261
  Message=Value cannot be null.
Parameter name: element
  Source=PresentationCore
  ParamName=element
  StackTrace:
       at MS.Internal.Media.VisualTreeUtils.AsNonNullVisual(DependencyObject element, Visual& visual, Visual3D& visual3D)
       at System.Windows.Media.VisualTreeHelper.GetParent(DependencyObject reference)
       at System.Windows.Controls.VirtualizingStackPanel.FindScrollOffset(Visual v)
       at System.Windows.Controls.VirtualizingStackPanel.OnAnchorOperation(Boolean isAnchorOperationPending)
       at System.Windows.Controls.VirtualizingStackPanel.OnAnchorOperation()
       at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
       at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
       at System.Windows.Threading.DispatcherOperation.InvokeImpl()
       at System.Windows.Threading.DispatcherOperation.InvokeInSecurityContext(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Windows.Threading.DispatcherOperation.Invoke()
       at System.Windows.Threading.Dispatcher.ProcessQueue()
       at System.Windows.Threading.Dispatcher.WndProcHook(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
       at MS.Win32.HwndWrapper.WndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam, Boolean& handled)
       at MS.Win32.HwndSubclass.DispatcherCallbackOperation(Object o)
       at System.Windows.Threading.ExceptionWrapper.InternalRealCall(Delegate callback, Object args, Int32 numArgs)
       at MS.Internal.Threading.ExceptionFilterHelper.TryCatchWhen(Object source, Delegate method, Object args, Int32 numArgs, Delegate catchHandler)
       at System.Windows.Threading.Dispatcher.LegacyInvokeImpl(DispatcherPriority priority, TimeSpan timeout, Delegate method, Object args, Int32 numArgs)
       at MS.Win32.HwndSubclass.SubclassWndProc(IntPtr hwnd, Int32 msg, IntPtr wParam, IntPtr lParam)
       at MS.Win32.UnsafeNativeMethods.DispatchMessage(MSG& msg)
       at System.Windows.Threading.Dispatcher.PushFrameImpl(DispatcherFrame frame)
       at System.Windows.Threading.Dispatcher.PushFrame(DispatcherFrame frame)
       at System.Windows.Threading.Dispatcher.Run()
       at System.Windows.Application.RunDispatcher(Object ignore)
       at System.Windows.Application.RunInternal(Window window)
       at System.Windows.Application.Run(Window window)
       at System.Windows.Application.Run()
       at VirtualTreeView.App.Main() in d:\Docs\Projects\_Try\VirtualTreeView\obj\Debug\App.g.cs:line 0
       at System.AppDomain._nExecuteAssembly(RuntimeAssembly assembly, String[] args)
       at System.AppDomain.ExecuteAssembly(String assemblyFile, Evidence assemblySecurity, String[] args)
       at Microsoft.VisualStudio.HostingProcess.HostProc.RunUsersAssembly()
       at System.Threading.ThreadHelper.ThreadStart_Context(Object state)
       at System.Threading.ExecutionContext.RunInternal(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state, Boolean preserveSyncCtx)
       at System.Threading.ExecutionContext.Run(ExecutionContext executionContext, ContextCallback callback, Object state)
       at System.Threading.ThreadHelper.ThreadStart()

您还可以看到,异常不是唯一的问题。在向上和向下滚动时,滚动条的大小不断改变。(在ListBox中不会出现相同的问题,因为它无法预测大小,但在查看整个列表后会记住总高度。)

问题:如何使滚动条正常工作并消除异常?(我不介意链接到替代 TreeView 控件或支持此场景的虚拟面板。)


4
相关链接:http://connect.microsoft.com/VisualStudio/feedback/details/763639/wpf-application-built-on-net-4-0-freezes-under-system-with-net-4-5-while-scrolling-the-treeview-under-specific-conditions。该问题反映了一个使用 .NET 4.0 构建的 WPF 应用程序在特定条件下,在安装了 .NET 4.5 的系统上滚动 TreeView 时会出现冻结的情况。 - user7116
这可能与您的GPU或CPU的性能有关。另外,如果在高度绑定上放置回退值,作为测试是否更好?如果使用ScrollViewer.VerticalScrollBarVisibility将滚动条始终设置为可见,会发生什么? - Brannon
1
我能够在.NET 4.0,Win7 64位上重现此问题。为了重现,必须严格按照以下说明操作:使用鼠标滚轮向下滚动到底部,然后使用鼠标滚轮向上滚动到顶部。在某个时刻,它会抛出异常。以任何其他方式滚动都不会触发此问题。 - Murven
我之前发布了一个答案,但在进行一些测试后问题又出现了,似乎这不是一个一致的复现。我认为这可能与Connect中的错误有关。 - Murven
你尝试过为TreeView设置固定高度吗?我记得我曾经遇到过类似的问题,并通过为垂直滚动设置固定高度来解决它。 - almog.ori
显示剩余9条评论
5个回答

10
为了使链接更加突出,我也将其发布在回答中。看起来框架代码中存在一个错误,并且目前还没有找到解决方法。我已经在Microsoft Connect上报告了这个错误: Microsoft Connect: WPF虚拟化TreeView中的滚动非常不稳定 还有一个可能相关的错误是由@sixlettervariables在评论中发布的: Microsoft Connect: 在特定条件下滚动树视图时,基于.NET 4.0构建的WPF应用程序会在使用.NET 4.5的系统下冻结 如果您能够重现这些错误,请投票支持它们。

微软已经关闭了这个问题吗?.Net4.5中是否已经解决了这个问题? - Rohit Vats
@RohitVats 根据微软的评论,“更新的确切性质和日期尚未确定”,似乎他们还没有包含修复程序。 - Athari
3
这个问题已经在.NET 4.5.2中得到解决。 - user704772
未来读者注意,从4.6.1版本开始,如果您绑定(MVVM风格)的项目是结构体,则会崩溃。我不确定它是否与此问题有关(尽管我有类似的堆栈跟踪)。对我来说,切换到类解决了这个问题。 - Julien Lebot
这真的是这个问题的答案吗? - Yvonnila

3

这个bug已经存在了10年,.NET 6仍然存在。

我遇到的触发条件是一个TreeView,其中每个TreeViewItem都有一些自定义格式的文本(通过TextBlock inlines实现),宽度足够需要水平滚动条。如果我将水平滚动条拖到最右边,然后再拖动垂直滚动条向上或向下,就会出现此异常。

由于其他方法都不起作用,我想将我的解决方法添加到解决方案中。异常来自某个非公开方法AsNonNullVisual。我只是捕获特定的异常并忽略它:

Dispatcher.UnhandledException += ( s, e ) => {
    if( e.Exception.TargetSite?.Name == "AsNonNullVisual" )
        e.Handled = true;
};

当异常被抛出时,垂直滚动条拇指会有些抖动,但除此之外没有其他视觉效果,滚动将像往常一样继续。

将此放置在App.xaml.cs、您的主窗口构造函数或任何其他仅在早期运行一次的位置。


2
截至.NET 5,这个问题在WPF中仍然存在,并且Microsoft已经退出了Microsoft Connect,因此现在甚至不清楚他们是否还关注这个问题。我也遇到了同样的问题,并偶然发现了一个解决方法。本质上,它只是做了TreeView应该做的相同的事情,使用HierarchicalDataTemplate渲染每个节点,但内置的TreeView在滚动时崩溃,而这个版本则没有(在我的情况下是项目树)。
<DockPanel>
   <DockPanel.Resources>
      <HierarchicalDataTemplate DataType="{x:Type src:Item}" ItemsSource="{Binding Path=Children}">
         <TextBlock Text"{Binding}"/>
      </HierarchicalDataTemplate>
   </DockPanel.Resources>

   <TreeView x:Name="tvwItems" VirtualizingStackPanel.IsVirtualizing="True" VirtualizingStackPanel.VirtualizationMode="Recycling" ItemsSource="{Binding Items}">
   </TreeView>
</DockPanel>

1

默认情况下,虚拟化堆栈面板使用像素渲染来渲染子元素,回收模式将丢弃不再需要在UI中的树形视图容器内的每个元素。这会导致滚动条大小自动更改。虚拟化面板像素渲染技术会导致滚动选项变慢。通过更改为VirtualizingPanel.ScrollUnit="Item",可以解决您的问题。以下XAML对我有效。

<Window x:Class="VirtualTreeView.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    Title="MainWindow" Height="800" Width="400" Left="0" Top="0"
    DataContext="{Binding RelativeSource={RelativeSource Self}}">
<Grid>
    <TreeView x:Name="tvwItems"
              ItemsSource="{Binding Items}"
              VirtualizingPanel.IsVirtualizing="True"
              VirtualizingPanel.VirtualizationMode="Recycling"
              VirtualizingPanel.ScrollUnit="Item"
              >
        <TreeView.ItemTemplate>
            <DataTemplate>
                <Border Height="{Binding Height}"
                        Width="{Binding Height}"
                        BorderThickness="1"
                        Background="DarkGray"
                        BorderBrush="DarkBlue" />
            </DataTemplate>
        </TreeView.ItemTemplate>
    </TreeView>
</Grid>
</Window>

2
ScrollUnit=Item模式下,TreeView的虚拟化是没有意义的,因为每个分支都被视为一个项目。它之所以在这个示例中可以工作,仅仅是因为没有子项。在引入ScrollUnit=Pixel之前,TreeView中的虚拟化是完全不可能的。 - Athari

0

在加载窗口时,我在WPF应用程序中遇到了同样的错误。经过一些研究,我发现了类似于这篇文章的东西,我注意到它是有趣的WindowStyle元素。

在我的情况下,在WPF中的XAML设计窗口中出现了错误,而Windows属性值为

WindowStyle ="none"

我已将其值更改为 WindowStyle ="SingleBorderWindow" ,这些错误已消失


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接