事情起因
測試報告說存在滾動條不能拖動的情況,我們幾個開發人員多次測試都未重現該問題。后面發現是操作系統的問題,在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,可解決問題(同樣未測試)。
更多內容,請移步上述鏈接。