1

I'm showing a set of pictures in a page. I use a GridView to show the pictures. However, when the user resizes the screen to make it narrow, I switch to a ListView.
The problem now is synchronizing the scroll position for the two lists. My approach to the solution is,
1. Get the first visible item of the first list.
2. Scroll the second list to that item using ScrollIntoView
However I'm unable to see any property in GridView/ListView that gives me the first information. Any ideas?
Also any other ways of doing this are appreciated.

iandayman
  • 4,357
  • 31
  • 38
Abhishek
  • 1,349
  • 8
  • 17

1 Answers1

4

That seems to be just about the way I would first try to do it. You can use the ItemsPanelRoot property of the GridView/ListView and get the Children of the panel, then use TransformToVisual().TransformPoint() relative to the list control on each child to find the first one that is visible.

The one gotcha I can think of is when ScrollIntoView() would scroll the item that was first in view port in one list to show as last in view in the other one. Maybe you could get the ScrollViewer from the template of the list control (e.g. by using VisualTreeHelper) and scroll to the beginning of the list first?

The most simple way to do it all might be to just scroll to the same relative offset in the list coming into view as the one going out. It might not be very precise, but it could work.

You could even do a nice animated transition of elements in one list into the elements in the other one.

*Update

I asked around and it seems like I forgot that the default panels in GridView and ListView - the ItemsWrapGrid and ItemsStackPanel contain a FirstVisibleIndex property that could be used to get the object and then call ScrollIntoView() on the list control, which in turns takes a ScrollIntoViewAlignment enum you can use to say you want the scrolled-to-item to be the first visible (aligned to the leading edge).

*Update 2

For ListViewBase - you can also use the ListViewPersistenceHelper to get and set relative offsets.

This upcoming update to WinRT XAML Toolkit might be helpful as it would allow you to simply call: gridView.SynchronizeScrollOffset(listView); or vice versa.

public static class ItemsControlExtensions
{
    public static ScrollViewer GetScrollViewer(this ItemsControl itemsControl)
    {
        return itemsControl.GetFirstDescendantOfType<ScrollViewer>();
    }

    public static int GetFirstVisibleIndex(this ItemsControl itemsControl)
    {
        // First checking if no items source or an empty one is used
        if (itemsControl.ItemsSource == null)
        {
            return -1;
        }

        var enumItemsSource = itemsControl.ItemsSource as IEnumerable;

        if (enumItemsSource != null && !enumItemsSource.GetEnumerator().MoveNext())
        {
            return -1;
        }

        // Check if a modern panel is used as an items panel
        var sourcePanel = itemsControl.ItemsPanelRoot;

        if (sourcePanel == null)
        {
            throw new InvalidOperationException("Can't get first visible index from an ItemsControl with no ItemsPanel.");
        }

        var isp = sourcePanel as ItemsStackPanel;

        if (isp != null)
        {
            return isp.FirstVisibleIndex;
        }

        var iwg = sourcePanel as ItemsWrapGrid;

        if (iwg != null)
        {
            return iwg.FirstVisibleIndex;
        }

        // Check containers for first one in view
        if (sourcePanel.Children.Count == 0)
        {
            return -1;
        }

        if (itemsControl.ActualWidth == 0)
        {
            throw new InvalidOperationException("Can't get first visible index from an ItemsControl that is not loaded or has zero size.");
        }

        for (int i = 0; i < sourcePanel.Children.Count; i++)
        {
            var container = (FrameworkElement)sourcePanel.Children[i];
            var bounds = container.TransformToVisual(itemsControl).TransformBounds(new Rect(0, 0, container.ActualWidth, container.ActualHeight));

            if (bounds.Left < itemsControl.ActualWidth &&
                bounds.Top < itemsControl.ActualHeight &&
                bounds.Right > 0 &&
                bounds.Bottom > 0)
            {
                return itemsControl.IndexFromContainer(container);
            }
        }

        throw new InvalidOperationException();
    }

    public static void SynchronizeScrollOffset(this ItemsControl targetItemsControl, ItemsControl sourceItemsControl, bool throwOnFail = false)
    {
        var firstVisibleIndex = sourceItemsControl.GetFirstVisibleIndex();

        if (firstVisibleIndex == -1)
        {
            if (throwOnFail)
            {
                throw new InvalidOperationException();
            }

            return;
        }

        var targetListBox = targetItemsControl as ListBox;

        if (targetListBox != null)
        {
            targetListBox.ScrollIntoView(sourceItemsControl.IndexFromContainer(sourceItemsControl.ContainerFromIndex(firstVisibleIndex)));
            return;
        }

        var targetListViewBase = targetItemsControl as ListViewBase;

        if (targetListViewBase != null)
        {
            targetListViewBase.ScrollIntoView(sourceItemsControl.IndexFromContainer(sourceItemsControl.ContainerFromIndex(firstVisibleIndex)), ScrollIntoViewAlignment.Leading);
            return;
        }

        var scrollViewer = targetItemsControl.GetScrollViewer();

        if (scrollViewer != null)
        {
            var container = (FrameworkElement) targetItemsControl.ContainerFromIndex(firstVisibleIndex);
            var position = container.TransformToVisual(scrollViewer).TransformPoint(new Point());
            scrollViewer.ChangeView(scrollViewer.HorizontalOffset + position.X, scrollViewer.VerticalOffset + position.Y, null);
        }
    }
}
Filip Skakun
  • 31,624
  • 6
  • 74
  • 100