当ScrollViewer嵌套时,外层ScrollViewer的IsDeferredScrollingEnabled设为true时引发内层ScrollViewer不能滚动所引发的思考。
事情起因

测试报告说存在滚动条不能拖动的情况,我们几个开发人员多次测试都未重现该问题。后面发现是操作系统的问题,在XP和部分Win7上会存在该问题。而在我们开发人员的机器上,包括Win7 SP1,Windows Server2008上都未出现该问题。

该问题的具体表现是拖动ScrollViewer时的滚动条不能滚动里面的内容,但是点击滚动条上下方的RepeatButton(即通常情况下的三角形按钮)却能滚动里面的内容。

本以为找到了问题,解决起来会很快。但是我们几个同事试了好久,都没找到问题。我也简单看了下,开始以为会是ScrollChanged事件响应将滚动条滚回去了,结果不是。后面就忙其他的去了。再后来,另外一个同事发现是外层ScrollViewer的IsDeferredScrollingEnabled设为了True。

下面是这个情况的一个简单示例,感兴趣的朋友可以试试。

<Window x:Class="ScrollViewerTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow"
        Width="525"
        Height="350">
    <Grid>
        <ScrollViewer IsDeferredScrollingEnabled="True">
            <Canvas Width="1000"
                    Height="1000"
                    Background="Red">
                <ScrollViewer Canvas.Left="200"
                              Canvas.Top="200"
                              Width="100"
                              Height="100">
                    <Canvas Width="500" Height="500">
                        <Canvas.Background>
                            <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                                <GradientStop Offset="0" Color="Black" />
                                <GradientStop Offset="1" Color="White" />
                            </LinearGradientBrush>
                        </Canvas.Background>
                    </Canvas>
                </ScrollViewer>
            </Canvas>
        </ScrollViewer>
    </Grid>
</Window>
原因探索

MSDN

先MSDN查看IsDeferredScrollingEnabled的含义。

Gets or sets a value that indicates whether the content is stationary when the user drags the Thumb of a ScrollBar.

在下面有备注:

Displaying a large number of items may cause performance issues. In this case, it might be useful to use deferred scrolling. For more information, see Optimizing Performance: Controls.

This property can be used as an instance property and an attached property.

再查看Optimizing Performance: Controls中Deferred Scrolling一节

By default, when the user drags the thumb on a scrollbar, the content view continuously updates. If scrolling is slow in your control, consider using deferred scrolling. In deferred scrolling, the content is updated only when the user releases the thumb.

To implement deferred scrolling, set the IsDeferredScrollingEnabled property to true. IsDeferredScrollingEnabled is an attached property and can be set on ScrollViewer and any control that has a ScrollViewer in its control template.

这些都是我一早就知道的,该属性是用于优化性能的。默认情况下,滚动条滚动时里面的内容会更新,在数据量较大且希望快速滚动时效率会比较低,而将该属性设为true,将会在松开滚动条时才更新内容。

源码

利用ILSpy查看ScrollViewer的源码。

查找IsDeferredScrollingEnabled的引用,发现其只在如下的函数中使用

private static void OnQueryScrollCommand(object target, CanExecuteRoutedEventArgs args)
        {
            args.CanExecute = true;
            if (args.Command == ComponentCommands.ScrollPageUp || args.Command == ComponentCommands.ScrollPageDown)
            {
                ScrollViewer scrollViewer = target as ScrollViewer;
                Control control = (scrollViewer != null) ? (scrollViewer.TemplatedParent as Control) : null;
                if (control != null && control.HandlesScrolling)
                {
                    args.CanExecute = false;
                    args.ContinueRouting = true;
                    args.Handled = true;
                    return;
                }
            }
            else
            {
                if (args.Command == ScrollBar.DeferScrollToHorizontalOffsetCommand || args.Command == ScrollBar.DeferScrollToVerticalOffsetCommand)
                {
                    ScrollViewer scrollViewer2 = target as ScrollViewer;
                    if (scrollViewer2 != null && !scrollViewer2.IsDeferredScrollingEnabled)
                    {
                        args.CanExecute = false;
                        args.Handled = true;
                    }
                }
            }
        }

再查找OnQueryScrollCommand的引用,发现其只在如下的函数中使用

private static void InitializeCommands()
        {
            ExecutedRoutedEventHandler executedRoutedEventHandler = new ExecutedRoutedEventHandler(ScrollViewer.OnScrollCommand);
            CanExecuteRoutedEventHandler canExecuteRoutedEventHandler = new CanExecuteRoutedEventHandler(ScrollViewer.OnQueryScrollCommand);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.LineLeftCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.LineRightCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.PageLeftCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.PageRightCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.LineUpCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.LineDownCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.PageUpCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.PageDownCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToLeftEndCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToRightEndCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToEndCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToHomeCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToTopCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToBottomCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToHorizontalOffsetCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.ScrollToVerticalOffsetCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.DeferScrollToHorizontalOffsetCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ScrollBar.DeferScrollToVerticalOffsetCommand, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ComponentCommands.ScrollPageUp, executedRoutedEventHandler, canExecuteRoutedEventHandler);
            CommandHelpers.RegisterCommandHandler(typeof(ScrollViewer), ComponentCommands.ScrollPageDown, executedRoutedEventHandler, canExecuteRoutedEventHandler);
        }

而InitializeCommands仅在ScrollViewer的静态构造函数中调用。

更多关于ScrollViewer与该问题无太大关系,留待以后补充。

猜想

里层的ScrollViewer将滚动的事件传递至上层的ScrollViewer处理,在上层ScrollViewer设置了IsDeferredScrollingEnabled的某些情况下,可能会造成底层ScrollViewer命令不可用。

补充

在这篇博文都快写完的时候,发现了Nested scrollbar and deferred scrolling bug,学习到了一些知识。

将里层的ScrollViewer的IsDeferredScrollingEnabled设为true,可解决这个问题。(由于我电脑为Win7 SP1,未测试)。

在外层ScrollViewer与里层ScrollViewer的逻辑树上的某个控件上更改命令绑定。将相关命令的CanExecute总是设为true,可解决问题(同样未测试)。

更多内容,请移步上述链接。