Maintain ScrollViewer scroll position across page navigation/app reactivation

When navigating around your app, it could be really helpful to maintain scroll position on various ScrollViewer controls. This also applies when your app reactivates after being suspended and shut down.

Doing it is a bit trickier than it looks since the two scroll  position properties on a ScrollViewer are read-only (you need to use a method to navigate around). I can up with the following code to take care of this. Note that this could be done better by hooking directly to the LayoutAwarePage.  (Note that the code as presented relies on you inheriting from LayoutAwarePage, but you could implement the same thing with any other data binding mechanism you employ.

How it works

  1. You maintain the scroll position in DefaultDataView. When SaveState() is called, you take the value from DefaultDataView and place it in state.
  2. Similarly, when LoadState() is called, you copy the value into DefaultDataView (or zero if there is no such value).
  3. You use the supplied dependency properties (below) to bind the ScrollViewer.
  4. Profit!

SaveState/LoadState

Here’s what the code looks like when saving state – note how we simply take the ScrollerPosoition key from the DefaultViewModel and copy it to the page state.

protected override void SaveState(Dictionary<string, object> pageState)
{
base.SaveState(pageState);
pageState["ScrollerPosition"] = DefaultViewModel["ScrollerPosition"];
}

Load is similar, but in the other direction:

protected override void LoadState(Object navigationParameter,

Dictionary<String, Object> pageState)
{
base.LoadState(navigationParameter, pageState);
DefaultViewModel["ScrollerPosition"] =

GetPageStateOrDefault("ScrollerPosition", pageState, 0.0);
}

Here’s the implementation for GetPageStateOrDefault() added to LayoutAwarePage:

protected T GetPageStateOrDefault<T>(string key, Dictionary<string, object> pageState, T def)
{
T result = def;
if (pageState != null)
{
result = pageState.GetValueOrDefaultTyped(key, result);
}

return result;
}

 

Attached property + binding

You define the following attached property in your code (it’s a self contained class):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NotSystem.Windows.Interactivity;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;

namespace SocialEbola.Lib.Behaviors
{
public class ScrollPositionBehavior : DependencyObject
{
public static readonly DependencyProperty HorizontalScrollOffsetForStateProperty =
DependencyProperty.RegisterAttached("HorizontalScrollOffsetForState", typeof(double), typeof(ScrollPositionBehavior), new PropertyMetadata(Double.NaN, DependencyPropertyChanged));
public static readonly DependencyProperty HookedProperty =
DependencyProperty.RegisterAttached("Hooked", typeof(bool), typeof(ScrollPositionBehavior), new PropertyMetadata(false));

private static bool GetHooked(FrameworkElement obj)
{
return (bool)obj.GetValue(HookedProperty);
}

private static void SetHooked(FrameworkElement obj, bool value)
{
obj.SetValue(HookedProperty, value);
}

public static double GetHorizontalScrollOffsetForState(FrameworkElement obj)
{
return (double)obj.GetValue(HorizontalScrollOffsetForStateProperty);
}

public static void SetHorizontalScrollOffsetForState(FrameworkElement obj, double value)
{
obj.SetValue(HorizontalScrollOffsetForStateProperty, value);
}

private static void DependencyPropertyChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
if (args.Property == HorizontalScrollOffsetForStateProperty)
{
HookIfNeeded((FrameworkElement)obj);
}
}

private static void HookIfNeeded(FrameworkElement element)
{
if (!GetHooked(element))
{
element.Loaded += Obj_Loaded;
element.Unloaded += Obj_Unloaded;
var viewer = GetScrollViewer(element);
if (viewer != null)
{
viewer.ViewChanged += Viewer_ViewChanged;
}
SetHooked(element, true);
}
}

static void Viewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (!e.IsIntermediate)
{
var viewer = GetScrollViewer(sender);
if (viewer != null)
{
SetHorizontalScrollOffsetForState((FrameworkElement)sender, viewer.HorizontalOffset);
}
}
}

static void Obj_Unloaded(object sender, RoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
element.Loaded -= Obj_Loaded;
element.Unloaded -= Obj_Unloaded;
var viewer = GetScrollViewer(element);
if (viewer != null)
{
viewer.ViewChanged -= Viewer_ViewChanged;
}

SetHooked(element, false);
}

private static ScrollViewer GetScrollViewer(object element)
{
return element as ScrollViewer;
}

static void Obj_Loaded(object sender, RoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
var viewer = element as ScrollViewer;
if (viewer != null)
{
viewer.ScrollToHorizontalOffset(GetHorizontalScrollOffsetForState(element));
}
}
}
}

 

Finaly, in XAML, you bind this property to your DefaultViewModel:

 

<ScrollViewer x:Name="scroller" 
Grid.RowSpan="2"
Padding="0,137,0,0"
Style="{StaticResource scrollerStyleLandscape}"
sebehaviors:ScrollPositionBehavior.HorizontalScrollOffsetForState="{Binding ScrollerPosition, Mode=TwoWay}">

Once all of this is done you are good – any navigation back into your page will cause the system to restore it’s position.

Advertisements
This entry was posted in Dev, Windows8 and tagged , , , , , , , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s