locked
Restore listview scrollbar position when navigating back RRS feed

  • Question

  • Hi,

    in my app I have a page with a GridView with incremental loading collection as items source bound to a viewmodel property.
    When I click an item a navigate to item detail page. I would like to restore GridView horizontal scrollbar position when I navigate back.

    I'm using Prism for Windows Runtime that allows to save viewmodel and page state and I can save horizontal scrollbar position when navigating to page detail.
    The problem is that i'm using an incremental loading collection so I could have loaded more items and when I go back to the page and restore the page state I should load all items loaded previously and then set the previous horizontal scrollbar position.

    Which is the best solution to do this?

    I thought I could save the entire incremental loading collection in viewmodel state and restore it later and then restore gridview horizontal scrollbar position.

    Are there better solutions?

    Sunday, October 27, 2013 4:32 AM

All replies

  • Have you looked at the previous thread How to restore scroll position of the GridView when navigating back.? It shows up for me as the top related thread in the left column here.

    If that doesn't help then please provide details on where it fails for your scenario.

    --Rob

    Sunday, October 27, 2013 3:24 PM
    Moderator
  • Hi,

    I looked at the reference implementation that comes with Prism and this is what I've done in my app:

    private double _myGridViewPanelScrollViewerHorizontalOffset;
    
    protected override void LoadState(object navigationParameter, System.Collections.Generic.Dictionary<string, object> pageState)
            {
                if (pageState == null) return;
    
                base.LoadState(navigationParameter, pageState);            
    
                if (pageState.ContainsKey("MyGridViewPanelScrollViewerHorizontalOffset"))
                {
                    _myGridViewPanelScrollViewerHorizontalOffset = double.Parse(pageState["MyGridViewPanelScrollViewerHorizontalOffset"].ToString(), CultureInfo.InvariantCulture.NumberFormat);
                }
            }
    
            protected override void SaveState(System.Collections.Generic.Dictionary<string, object> pageState)
            {
                if (pageState == null) return;
    
                base.SaveState(pageState);
    
                ScrollViewer scrollViewer = MyGridView.FindVisualChild<ScrollViewer>();
    
                if (scrollViewer != null)
                {
                    pageState["MyGridViewPanelScrollViewerHorizontalOffset"] = scrollViewer.HorizontalOffset;
                }
            }
    
            private void MyGridViewPanel_Loaded(object sender, RoutedEventArgs e)
            {
                ItemsWrapGrid myGridViewPanel = (ItemsWrapGrid)sender;
                
                // Find the ScrollViewer inside the GridView
                var scrollViewer = MyGridView.FindVisualChild<ScrollViewer>();
    
                if (scrollViewer != null)
                {
                    if (scrollViewer.ComputedHorizontalScrollBarVisibility == Visibility.Visible)
                    {                    
                        // Setting the horizontal offset of the ScrollViewer is necessary
                        // to update the position of the scroll bar at the bottom of the GridView.
                        // Without updating the ScrollViewer, the scroll bar would appear to be on the 
                        // far left, even though the VirtualizingStackPanel has scrolled to the right.                    
                        scrollViewer.ChangeView(_myGridViewPanelScrollViewerHorizontalOffset, null, null, true);
                    }
                    else
                    {
                        DependencyPropertyListener helper = new DependencyPropertyListener(scrollViewer, "ComputedHorizontalScrollBarVisibility");
                        helper.PropertyChanged += MyGridViewPanelScrollViewer_PropertyChanged;
                    }
                }
            }
    
            private void MyGridViewPanelScrollViewer_PropertyChanged(object sender, DependencyPropertyChangedEventArgs e)
            {
                DependencyPropertyListener helper = (DependencyPropertyListener)sender;
                ScrollViewer scrollViewer = MyGridView.FindVisualChild<ScrollViewer>();            
    
                if (((Visibility)e.NewValue) == Visibility.Visible)
                {
                    // Update the Horizontal offset                
                    scrollViewer.ChangeView(_myGridViewPanelScrollViewerHorizontalOffset, null, null, true);
                    helper.PropertyChanged -= MyGridViewPanelScrollViewer_PropertyChanged;
                };
            }

    I save and restore GridView scrollviewer horizontal position but it's not working when I load more items in GridVew.

    Since I'm using an incremental loading collection, items are added on demand.
    If I scroll to add two more "page" items, then when I restore the position, it seems that the scrollviewer is positioned at the end of the first "page" and not in the third "page".

    In Prism reference implementation there is a GridView with a VirtualizingStackPanel as ItemsPanel and its HorizontalOffset is also saved and restored in page state.
    Since I'm using an ItemsWrapGrid as panel, I don't have HorizontalOffset so I cannot save and restore it, I only save and restore scrollviewr HorizontalOffset.

    What am I doing wrong?


    Tuesday, October 29, 2013 5:04 AM
  • When you are navigating to Itemdetailpage from a page having gridview, in save state you could save the selected item of a group or category in gridview pagestate right?, when you come back from itemdetail page to gridview page yu could use that group from pagestate and use it under gridviewloaded event write the below code ..generally here you need to give the group object( means which group item you was previously clicked) to the gridview scrollIntoview method

     private void itemGridView_Loaded(object sender, RoutedEventArgs e)
            { 
    
     itemGridView.ScrollIntoView(SampleDataSource.GetGroup(groupId), ScrollIntoViewAlignment.Default);
    
    
    }
    If you didnt understand or didnt work please give reply.



    Tuesday, October 29, 2013 5:54 AM
  • Hi,

    my datasource is a class that inherits from ObservableCollection<T> and implements ISupportIncrementalLoading so I cannot apply your solution since I don't have groups.

    Tuesday, October 29, 2013 6:07 AM
  • If you are not using WinRTXamlToolKit use it ,it might help you , Install the WinRTXamlTooKit, reference the WinRtXamlToolKit.Extensions namespace, and use GridView.GetFirstDescendantOfType<ScrollViewer>() method.  The scrollviewer has a horizontal and vertical offset properties , This might help you....
    Tuesday, October 29, 2013 6:50 AM
  • The problem is not how to get scrollviewer offset, I got it also without using WinRTXamlToolkit.

    The problem is that the data source is an incremental loading collection and elements are not loaded all at once, but on demand when the user scrolls the list.

     
    Tuesday, October 29, 2013 7:24 AM
  • Did you ever find an answer for this?
    Friday, September 26, 2014 3:55 AM
  • Yes, I found a solution. In my app I'my using Prism that simplifies viemodel and view state management:

    - In OnNavigatedFrom method of the viewmodel I save items list (the entre incremental loading collection) in viewModelState dictionary.
    - In OnNavigatedTo method of the viewmodel I restore items collection getting it from viewModelState dictionary and then remove it from viewModelState. This only if navigating back from previous page (navigationMode == NavigationMode.Back).
    - In the view I set ItemsWrapGrid as ItemsPanelTemplate in order to attach to its Loaded event.
    - In SaveState method of the view I save GridView scrollviewer HorizontalOffset in pageState dictionary. This only if app is not suspending since in my app I want to start from the home page each time the app is started.
    - In LoadState method of the view I read HorizontalOffset saved in the viewstate and store it in a variable and then remove it from pageState dictionary.
    - In Loaded event of ItemsWrapGrid I get the scrollviewer contained in the panel and then I call ChangeView method passing HorizontalOffset saved before.


    It was not easy to find this solution, It's a little complicated but it works very well.

    Saturday, September 27, 2014 11:26 AM
  • - In Loaded event of ItemsWrapGrid I get the scrollviewer contained in the panel and then I call ChangeView method passing HorizontalOffset saved before.

      This part doesn't work for me. Scroll position is set at the bottom of screen, only default items count loaded.

    So, I found another solution:

    attach to the vertical offset property of scrollViewer and call changeView again.

    private void PoiList_Loaded(object sender, RoutedEventArgs e)
            {
                _gridScrollViewer = UIHelper.FindFirstChild<ScrollViewer>(PoiList);
    
                DependencyPropertyChangedHelper verticalHelper = new DependencyPropertyChangedHelper(_gridScrollViewer, "VerticalOffset");
                verticalHelper.PropertyChanged += ScrollOffsetChanged;
    
                _gridScrollViewer.ChangeView(null, _scrollOffset, null, true);
            }


    private void ScrollOffsetChanged(object sender, DependencyPropertyChangedEventArgs e)
            {
                var helper = (DependencyPropertyChangedHelper)sender;
                if (_scrollOffset > _gridScrollViewer.VerticalOffset)
                {
                    CoreApplication.MainView.CoreWindow.Dispatcher.RunAsync(
                                CoreDispatcherPriority.Normal,
                                new DispatchedHandler(() =>
                        {
                            _gridScrollViewer.ChangeView(null, _scrollOffset, null, true);
                    }));
                }
                else
                {
                    _scrollOffset = 0;
                    helper.PropertyChanged -= ScrollOffsetChanged;
                }
            }

    Helper to attach to dependencyProperty:

    public class DependencyPropertyChangedHelper : DependencyObject
        {
            /// <summary>
            /// Constructor for the helper. 
            /// </summary>
            /// <param name="source">Source object that exposes the DependencyProperty you wish to monitor.</param>
            /// <param name="propertyPath">The name of the property on that object that you want to monitor.</param>
            public DependencyPropertyChangedHelper(DependencyObject source, string propertyPath)
            {
                // Set up a binding that flows changes from the source DependencyProperty through to a DP contained by this helper 
                Binding binding = new Binding
                {
                    Source = source,
                    Path = new PropertyPath(propertyPath)
                };
                BindingOperations.SetBinding(this, HelperProperty, binding);
            }
    
            /// <summary>
            /// Dependency property that is used to hook property change events when an internal binding causes its value to change.
            /// This is only public because the DependencyProperty syntax requires it to be, do not use this property directly in your code.
            /// </summary>
            public static DependencyProperty HelperProperty =
                DependencyProperty.Register("Helper", typeof(object), typeof(DependencyPropertyChangedHelper), new PropertyMetadata(null, OnPropertyChanged));
    
            /// <summary>
            /// Wrapper property for a helper DependencyProperty used by this class. Only public because the DependencyProperty syntax requires it.
            /// DO NOT use this property directly.
            /// </summary>
            public object Helper
            {
                get { return (object)GetValue(HelperProperty); }
                set { SetValue(HelperProperty, value); }
            }
    
            // When our dependency property gets set by the binding, trigger the property changed event that the user of this helper can subscribe to
            private static void OnPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
            {
                var helper = (DependencyPropertyChangedHelper)d;
                helper.PropertyChanged(d, e);
            }
    
            /// <summary>
            /// This event will be raised whenever the source object property changes, and carries along the before and after values
            /// </summary>
            public event EventHandler<DependencyPropertyChangedEventArgs> PropertyChanged = delegate { };
        }

    Thursday, February 26, 2015 6:08 PM