Wednesday, 19 March 2014

NDC Oslo 2014

NDC Is Norway’s largest conference dedicated to .NET and Agile development, and is backed by Microsoft. It attracts speakers and participants from all around, and this year I’ll be speaking there.

This year the sessions include content on ASP.NET Web API, Azure, Big Data, Continuous Delivery, DevOps, F#, HTML5, Knockout, Windows 8.1, and Windows Phone. For more info about what can be expected at NDC see the trailer below, and go to the website.

Monday, 10 March 2014

Capturing and setting a ScrollViewer’s scroll position to maintain scroll position across view states

Previously I’ve written about how Prism for the Windows Runtime can be used to save and restore view state. This is achieved by overriding the SaveState and LoadState methods in a page that derives from the VisualStateAwarePage class, and then saving the desired state. I extended the sample photo viewer app so that the user will see the page content scrolled to the exact location it was at prior to termination or navigation, regardless of whether the page orientation has changed in between termination and reactivation. However, I didn’t document how the scroll position is obtained from a control, and re-applied when the window size changes.

In this blog post I’ll explain how the scroll position is obtained from a control, and re-applied when the window size changes. The approach is to capture the ScrollViewer’s offset value as a proportion of its total size. Then, once the user rotates the device or changes the size of the frame (thus effectively resizing the window), the offset proportion is restored to the control. This means that if a GridView control has enough content to pan, and you pan to 80% of the content, you can rotate the device, or switch to MinimalLayout, and the scroll bar should be vertical but still at 80% of the content. In addition, if you navigate forward and then back, the scroll bar should still be at 80% of the content.

This technique was devised as an intuitive user experience for the AdventureWorks Shopper reference implementation.

Implementation

When the page loads the ScrollViewer embedded in the GridView is captured by the Loaded event handler for the page. The Loaded event fires when the control has been constructed and added to the object tree, meaning that it’s ready for interaction.

private void photosGridView_Loaded(object sender, RoutedEventArgs e)
{
    _photosGridViewScrollViewer = VisualTreeUtilities.GetVisualChild<ScrollViewer>(photosGridView);
}

This method uses the GetVisualChild method in the VisualTreeUtilites class (not shown here). In this usage the method walks the visual tree to obtain the ScrollViewer instance located in the GridView instance.

The LayoutUpdated event handler is then used to capture the ScrollViewer’s scroll position. The LayoutUpdated event fires when the layout of the visual tree changes, due to layout-relevant properties changing value or some other action that refreshes the layout.

private void photosGridView_LayoutUpdated(object sender, object e)
{
    _scrollViewerOffsetProportion = ScrollViewerUtilities.GetScrollViewerOffsetProportion(_photosGridViewScrollViewer);
}

This method simply uses the GetScrollViewerOffsetProportion in the ScrollViewerUtilites class to calculate the offset proportion.

public static double GetScrollViewerOffsetProportion(ScrollViewer scrollViewer)
{
    if (scrollViewer == null) return 0;
 
    var horizontalOffsetProportion = (scrollViewer.ScrollableWidth == 0) ? 0 : (scrollViewer.HorizontalOffset / scrollViewer.ScrollableWidth);
    var verticalOffsetProportion = (scrollViewer.ScrollableHeight == 0) ? 0 : (scrollViewer.VerticalOffset / scrollViewer.ScrollableHeight);
 
    var scrollViewerOffsetProportion = Math.Max(horizontalOffsetProportion, verticalOffsetProportion);
    return scrollViewerOffsetProportion;
}

The offset proportion is calculated as the maximum of either the HorizontalOffset divided by ScrollableWidth or the VerticalOffset divided by the ScrollableHeight. This value can then be restored when the user rotates their device or resizes the frame, in order to scroll to the same location.

After the page size changes the ScrollViewer can be set with the offset proportion. However, we must first wait for the ScrollViewer to finish rendering its items. One way to determine this is to examine the ComputedHorizontalScrollBarVisibility and ComputedVerticalScrollBarVisibilty properties in the SizeChanged event handler. The SizeChanged event fires when either the ActualHeight or ActualWidth property value changes on the page.

private void Page_SizeChanged(object sender, SizeChangedEventArgs e)
{
    var scrollViewer = VisualTreeUtilities.GetVisualChild<ScrollViewer>(photosGridView);
 
    if (scrollViewer != null)
    {
        if (scrollViewer.ComputedHorizontalScrollBarVisibility == Visibility.Visible && scrollViewer.ComputedVerticalScrollBarVisibility == Visibility.Visible)
        {
            ScrollViewerUtilities.ScrollToProportion(scrollViewer, _scrollViewerOffsetProportion);
        }
        else
        {        
            DependencyPropertyChangedHelper horizontalHelper = new DependencyPropertyChangedHelper(scrollViewer, "ComputedHorizontalScrollBarVisibility");
            horizontalHelper.PropertyChanged += ScrollBarVisibilityChanged;
 
            DependencyPropertyChangedHelper verticalHelper = new DependencyPropertyChangedHelper(scrollViewer, "ComputedVerticalScrollBarVisibility");
            verticalHelper.PropertyChanged += ScrollBarVisibilityChanged;
        }
    }
}

Here, if the ComputedHorizontalScrollBarVisibility and ComputedVerticalScrollBarVisibility both equal Visibility.Visible then the ScrollViewer can be scrolled by calling the ScrollToProportion method of the ScrollViewerUtilities class. If the two properties are not set to Visibility.Visible, then the DependencyPropertyChangedHelper class is used to listen for changes in the two properties. The ScrollBarVisibilityChanged event handler is executed when either of the two properties change.

private void ScrollBarVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e)
{
    var helper = (DependencyPropertyChangedHelper)sender;
    var scrollViewer = VisualTreeUtilities.GetVisualChild<ScrollViewer>(photosGridView);
 
    if (((Visibility)e.NewValue) == Visibility.Visible)
    {
       ScrollViewerUtilities.ScrollToProportion(scrollViewer, _scrollViewerOffsetProportion);
        helper.PropertyChanged -= ScrollBarVisibilityChanged;
    }
 
    if (_isPageLoading)
    {
        photosGridView.LayoutUpdated += photosGridView_LayoutUpdated;
        _isPageLoading = false;
    }
}

This event handler checks if either of the two properties have changed to Visibility.Visible. If they have, the ScrollViewer is updated by calling the ScrollToProportion method of the ScrollViewerUtilities class.

public static void ScrollToProportion(ScrollViewer scrollViewer, double scrollViewerOffsetProportion)
{
    if (scrollViewer == null) return;
    var scrollViewerHorizontalOffset = scrollViewerOffsetProportion * scrollViewer.ScrollableWidth;
    var scrollViewerVerticalOffset = scrollViewerOffsetProportion * scrollViewer.ScrollableHeight;
 
    scrollViewer.ChangeView(scrollViewerHorizontalOffset, scrollViewerVerticalOffset, null);
}

In the ScrollToProportion method the offset is calculated as the offset proportion multiplied by the ScrollableHeight, and the offset proportion multiplied by the ScrollableWidth (one for landscape, one for portrait). Then the ChangeView method is called on the ScrollViewer instance to load a new view into the viewport using the offsets.

If the user navigates to another page or the app is terminated, the offset proportion is lost. Therefore, the SaveState and LoadState methods are used to preserve this value. For more information about this see one of my previous blog posts on this topic.

Summary

In this blog post I’ve explained how the scroll position is obtained from a control, and re-applied when the window size changes. The approach is to capture the ScrollViewer’s offset value as a proportion of its total size. Then, once the user rotates the device or changes the size of the frame, the offset proportion is restored to the control

The sample app can be downloaded here.

Monday, 3 March 2014

Creating a custom GridView control that responds to layout changes

In Windows Store apps for Windows 8.1 one of the UX guidelines is to scroll vertically when in PortraitLayout or MinimalLayout, and to scroll horizontally when in DefaultLayout (landscape). The conventional approach to implementing this is to provide two separate controls to handle the scenario – a GridView to handle horizontal scrolling views, and a ListView to handle vertical scrolling views. However, its possible to run into difficulties with this approach quite easily as it can be difficult to synchronize the two controls. For instance, if you have scrolled to a certain position in a GridView in DefaultLayout and then switch to the ListView in MinimalLayout, it can be difficult to make the ListView scroll to the same position. Similarly, if you’ve selected an item in a GridView in DefaultLayout and then switch to the ListView in MinimalLayout, it can be difficult in certain scenarios to automatically select the same item in the ListView.

This is a topic that we gave much consideration while working on Prism for the Windows Runtime for Windows 8.1, and the solution we came up with is the AutoRotatingGridView custom control. This control is a view state detecting GridView control created for the AdventureWorks Shopper reference implementation, which ships as part of Prism for the Windows Runtime. When, for example, the view state changes from DefaultLayout to PortraitLayout the items displayed by the control will be automatically rearranged to use an appropriate layout for the view state. The advantage of this approach is that only one control is required to handle all the view states, rather than having to define multiple controls to handle the different view states.

The control listens to the SizeChanged event and updates its layout between DefaultLayout, PortraitLayout, and MinimalLayout based on the window size and shape. In order to take advantage of the functionality provided by the control you must specify additional properties on your AutoRotatingGridView instance. The control allows you to set the ItemTemplate, ItemsPanel, and GroupStyle properties, which are used for defining how the control responds in DefaultLayout. The control also defines the PortraitItemTemplate, PortraitItemsPanel, and PortraitGroupStyle properties, which are used for defining how the control responds in PortraitLayout. In addition, the control defines the MinimalItemTemplate, MinimalItemsPanel, and MinimalGroupStyle properties, which are used for defining how the control responds in MinimalLayout. Finally, the control defines the MinimalLayoutWidth property which has a default value of 500. All of these additional properties are defined in the AutoRotatingGridView class.

In this blog post I’ll extended my sample PhotoViewer app to use the AutoRotatingGridView custom control to handle view states changes, and to ensure that content scrolls in the correct direction when in a specific layout.

Implementation

Using the AutoRotatingGridView custom control in the MainPage is simply a case of replacing the GridView control with the AutoRotatingGridView custom control.

<controls:AutoRotatingGridView x:Name="photosGridView"
                               Grid.Row="1"
                               IsItemClickEnabled="True"
                               ItemsSource="{Binding Photos}"
                               Loaded="photosGridView_Loaded"
                               Margin="140,0,0,0"
                               ScrollViewer.IsHorizontalScrollChainingEnabled="False"
                               SelectionMode="None">
    <interactivity:Interaction.Behaviors>
        <core:EventTriggerBehavior EventName="ItemClick">
            <behaviours:NavigateWithEventArgsToPageAction TargetPage="PhotoViewer.Views.PhotoPage"
                                                          EventArgsParameterPath="ClickedItem.Path" />
        </core:EventTriggerBehavior>
    </interactivity:Interaction.Behaviors>
    <controls:AutoRotatingGridView.ItemTemplate>
        <DataTemplate>
            <Border Background="#FF939598">
                <Image Height="130"
                       Source="{Binding Path=Thumbnail, Converter={StaticResource ThumbnailConverter}}"
                       Stretch="Uniform"
                       Width="190" />
            </Border>
        </DataTemplate>
    </controls:AutoRotatingGridView.ItemTemplate>
</controls:AutoRotatingGridView>

This XAML defines an AutoRotatingGridView custom control with an ItemTemplate for DefaultLayout. Because no values are specified for PortraitLayout or MinimalLayout, the control falls back to using the ItemTemplate specified for DefaultLayout. This is adequate for the sample app because the DataTemplate is sufficiently simple that it can be displayed in DefaultLayout, PortraitLayout, and MinimalLayout, without modification. For information about the purpose of the NavigateWithEventArgsToPageAction custom interaction, see Writing a custom Blend interaction to perform page navigation in Windows Store apps.

The result of using the AutoRotatingGridView custom control is that when in DefaultLayout scrolling is horizontal, and when in PortraitLayout or MinimalLayout scrolling is vertical. Contrast this to using the GridView control in which scrolling is horizontal in all layouts.

This scenario only demonstrates a very simple example of the AutoRotatingGridView custom control. It’s real power lies in displaying more complex data where it becomes necessary to define ItemTemplates, ItemsPanels, and GroupStyles for each layout. For examples of these scenarios see the AdventureWorks Shopper reference implementation.

Summary

In this blog post I’ve further extended the sample photo viewer app so that the MainPage uses the AutoRotatingGridView custom control to respond to view state changes. This simply involved replacing the GridView control in the MainPage with the AutoRotatingGridView custom control. Take a look at the AdventureWorks Shopper reference implementation for more complex examples of the AutoRotatingGridView custom control.

The sample app can be downloaded here.