Introduction
If you take a look at the Silverlight DataGrid
, you'll see that you can't control scrolling by default. This could be a hurdle if you're working with data driven applications. What if you want to preserve the scroll position after you reload the DataGrid
?
In this article, we're going to do 2 things in regard to scrolling:
- Create extension methods to control the
DataGrid
scrolling
- Create a custom
DataGrid
for advanced scenarios
Scrollbars on the DataGrid
Before we get started, here is a quick overview of the DataGrid
with scrollbars. Each DataGrid
could have up to 2 scrollbars (1 horizontal and 1 vertical). And the button you can drag around is called a thumb (from System.Windows.Controls.Primitives.Thumb
).
Creating the DataGridScrollExtensions Class
The class we'll create should provide the following functionalities:
- Give us access to both the horizontal and vertical scrollbars
- Inform us about the position (and changes) of the scrollbar thumbs
- Move the scrollbar thumbs
This is how the class looks like:
Finding the Scrollbars
The first thing to do is make sure we can access the scrollbars. This is where we'll use the recursive method GetScrollbars
. Using the VisualTreeHelper
, we can drill down through a DependencyObject
(in our case the DataGrid
) until we find the Scrollbar
. The names you can use to find the right scrollbars are VerticalScrollbar
and HorizontalScrollbar
(case sensitive!).
private static ScrollBar GetScrollbar(this DependencyObject dep, string name)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dep); i++)
{
var child = VisualTreeHelper.GetChild(dep, i);
if (child != null && child is ScrollBar && ((ScrollBar)child).Name == name)
return child as ScrollBar;
else
{
ScrollBar sub = GetScrollbar(child, name);
if (sub != null)
return sub;
}
}
return null;
}
Now that we have access to the scrollbars, we can get to all the information we need. You'll probably want to look at the Maximum
and Value
properties and also the Scroll
event.
Moving the Scrollbars
Moving the scrollbars around is a little trickier. I bet you're thinking the same as I was thinking. Just change the Value
property of a scrollbar. Well, I suggest you try that. You'll see that the Thumb
will move to the new value of the scrollbar, but the data won't follow. Let's take a look at the DataGrid
using Reflector:
public override void OnApplyTemplate()
{
...
this._hScrollBar = base.GetTemplateChild("HorizontalScrollbar") as ScrollBar;
if (this._hScrollBar != null)
{
this._hScrollBar.IsTabStop = false;
this._hScrollBar.Maximum = 0.0;
this._hScrollBar.Orientation = Orientation.Horizontal;
this._hScrollBar.Visibility = Visibility.Collapsed;
this._hScrollBar.Scroll +=
new ScrollEventHandler(this.HorizontalScrollBar_Scroll);
}
...
this._vScrollBar = base.GetTemplateChild("VerticalScrollbar") as ScrollBar;
if (this._vScrollBar != null)
{
this._vScrollBar.IsTabStop = false;
this._vScrollBar.Maximum = 0.0;
this._vScrollBar.Orientation = Orientation.Vertical;
this._vScrollBar.Visibility = Visibility.Collapsed;
this._vScrollBar.Scroll += new ScrollEventHandler(this.VerticalScrollBar_Scroll);
}
...
}
As you can see, the DataGrid
subscribes to each ScrollBar
's Scroll
event. When this event is raised, the DataGrid
will move the data around. But if you just change the Value
of a ScrollBar
, the Scroll
event won't be triggered.
We'll need to simulate a user moving the ScrollBar
around. And maybe you know this, but Silverlight (and .NET) provide a concept that was made for what we're trying to achieve, Microsoft UI Automation. One of its features is simulating user interaction. You can read more about it here and here. The following code gets the AutomationPeer
for the DataGrid
and this object can provide us with the IScrollProvider
.
private static IScrollProvider GetScrollProvider(DataGrid grid)
{
var p = FrameworkElementAutomationPeer.FromElement(grid)
?? FrameworkElementAutomationPeer.CreatePeerForElement(grid);
return p.GetPattern(PatternInterface.Scroll) as IScrollProvider;
}
Finally, using this IScrollProvider
, you can simulate a scroll interaction:
switch (mode)
{
case ScrollMode.Vertical:
scrollProvider.SetScrollPercent(
System.Windows.Automation.ScrollPatternIdentifiers.NoScroll, percent);
break;
case ScrollMode.Horizontal:
scrollProvider.SetScrollPercent(percent,
System.Windows.Automation.ScrollPatternIdentifiers.NoScroll);
break;
}
The Code
Here is the code with all the necessary extension methods:
public static class DataGridScrollExtensions
{
public static void ScrollToStart(this DataGrid grid, ScrollMode mode)
{
switch (mode)
{
case ScrollMode.Vertical:
grid.ScrollToPercent(ScrollMode.Vertical, 0);
break;
case ScrollMode.Horizontal:
grid.ScrollToPercent(ScrollMode.Horizontal, 0);
break;
}
}
public static void ScrollToEnd(this DataGrid grid, ScrollMode mode)
{
switch (mode)
{
case ScrollMode.Vertical:
grid.ScrollToPercent(ScrollMode.Vertical, 100);
break;
case ScrollMode.Horizontal:
grid.ScrollToPercent(ScrollMode.Horizontal, 100);
break;
}
}
public static void ScrollToPercent
(this DataGrid grid, ScrollMode mode, double percent)
{
if (percent < 0)
percent = 0;
else if (percent > 100)
percent = 100;
var scrollProvider = GetScrollProvider(grid);
switch (mode)
{
case ScrollMode.Vertical:
scrollProvider.SetScrollPercent
(System.Windows.Automation.ScrollPatternIdentifiers.NoScroll, percent);
break;
case ScrollMode.Horizontal:
scrollProvider.SetScrollPercent
(percent, System.Windows.Automation.ScrollPatternIdentifiers.NoScroll);
break;
}
}
public static double GetScrollPosition(this DataGrid grid, ScrollMode mode)
{
var scrollBar = grid.GetScrollbar(mode);
return scrollBar.Value;
}
public static double GetScrollMaximum(this DataGrid grid, ScrollMode mode)
{
var scrollBar = grid.GetScrollbar(mode);
return scrollBar.Maximum;
}
public static void Scroll(this DataGrid grid, ScrollMode mode, double position)
{
var scrollBar = grid.GetScrollbar(mode);
double positionPct = ((position / scrollBar.Maximum) * 100);
grid.ScrollToPercent(mode, positionPct);
}
private static IScrollProvider GetScrollProvider(DataGrid grid)
{
var p = FrameworkElementAutomationPeer.FromElement(grid) ??
FrameworkElementAutomationPeer.CreatePeerForElement(grid);
return p.GetPattern(PatternInterface.Scroll) as IScrollProvider;
}
public static ScrollBar GetScrollbar(this DataGrid grid, ScrollMode mode)
{
if (mode == ScrollMode.Vertical)
return grid.GetScrollbar("VerticalScrollbar");
else
return grid.GetScrollbar("HorizontalScrollbar");
}
private static ScrollBar GetScrollbar(this DependencyObject dep, string name)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(dep); i++)
{
var child = VisualTreeHelper.GetChild(dep, i);
if (child != null && child is ScrollBar && ((ScrollBar)child).Name == name)
return child as ScrollBar;
else
{
ScrollBar sub = GetScrollbar(child, name);
if (sub != null)
return sub;
}
}
return null;
}
}
Creating the ScrollDataGrid control
Now that we can control the scrolling, we could go even further. What about creating a DataGrid
where you can subscribe to both Scroll
events or even store the current scroll position? This could be useful for when we're reloading the items and we want to preserve the scroll position.
The most important things to note are the SaveScrollPosition
/ ReloadScrollPosition
methods and the HorizontalScroll
/ VerticallScroll
events. Here is a small demo of the ScrollDataGrid
.
The method SaveScrollPosition
saves the current position in an internal field and the ReloadScrollPosition
applies the value of that field back to the ScrollBar
. To find the ScrollBar
s, we're applying another technique, using GetTemplateChild
. The rest of the code calls our extension methods:
public class ScrollDataGrid : DataGrid
{
private ScrollBar verticalScrollBar;
private ScrollBar horizontalScrollBar;
private double savedVerticalScrollPosition;
private double savedHorizontalScrollPosition;
public event EventHandler<scrolleventargs> VerticalScroll;
public event EventHandler<scrolleventargs> HorizontalScroll;
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
this.LoadScrollBars();
}
private void LoadScrollBars()
{
verticalScrollBar = this.GetTemplateChild("VerticalScrollbar") as ScrollBar;
if (verticalScrollBar != null)
verticalScrollBar.Scroll += new ScrollEventHandler(OnVerticalScroll);
horizontalScrollBar = this.GetTemplateChild("HorizontalScrollbar") as ScrollBar;
if (horizontalScrollBar != null)
horizontalScrollBar.Scroll += new ScrollEventHandler(OnHorizontalScroll);
}
private void OnVerticalScroll(object sender, ScrollEventArgs e)
{
if (VerticalScroll != null)
VerticalScroll(sender, e);
}
private void OnHorizontalScroll(object sender, ScrollEventArgs e)
{
if (HorizontalScroll != null)
HorizontalScroll(sender, e);
}
public void SaveScrollPosition(ScrollMode mode)
{
switch (mode)
{
case ScrollMode.Vertical:
this.savedVerticalScrollPosition = verticalScrollBar.Value;
break;
case ScrollMode.Horizontal:
this.savedHorizontalScrollPosition = horizontalScrollBar.Value;
break;
default:
break;
}
}
public void ReloadScrollPosition(ScrollMode mode)
{
switch (mode)
{
case ScrollMode.Vertical:
this.Scroll(ScrollMode.Vertical, savedVerticalScrollPosition);
break;
case ScrollMode.Horizontal:
this.Scroll(ScrollMode.Horizontal, savedHorizontalScrollPosition);
break;
}
}
}
Happy scrolling.
History
- 14th September, 2010: Initial post