ideaki's blog

WinRT C#/XAML の開発について

UWP GridView/ListViewのスクロール位置を復元したい

探すとたくさんあるスクロール位置の復元ですが

  • ViewModel:スクロール位置の保持
  • View:スクロール位置の復元/監視

と役割分担が明確になっているサンプルがなかったのでそれっぽく作りました。

がっちゃんこできるBehaviorで実装

以下、
1. スクロール位置復元できるBehavior
2. 使ってみよう
3. サンプルsln


1. スクロール位置復元できるBehavior

public class ScrollOffsetControlBehavior : DependencyObject, IBehavior
{
    public int RestoreScrollOffset
    {
        get { return (int)GetValue(RestoreScrollOffsetProperty); }
        set { SetValue(RestoreScrollOffsetProperty, value); }
    }

    // Using a DependencyProperty as the backing store for RestoreScrollOffset.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty RestoreScrollOffsetProperty =
        DependencyProperty.Register("RestoreScrollOffset", typeof(int), typeof(ScrollOffsetControlBehavior), new PropertyMetadata(0));

    int CurrentItemViewIndex
    {
        get
        {
            var listviewbase = AssociatedObject as ListViewBase;
            var item = listviewbase.ItemsPanelRoot as ItemsStackPanel;
            return item.FirstVisibleIndex > 0 ? item.FirstVisibleIndex : 0;
        }
    }

    public DependencyObject AssociatedObject
    {
        get;
        set;
    }

    private ScrollViewer sv;

    public void Attach(DependencyObject associatedObject)
    {
        if ((associatedObject != AssociatedObject) && !Windows.ApplicationModel.DesignMode.DesignModeEnabled)
        {
            AssociatedObject = associatedObject;
            var listviewbase = AssociatedObject as ListViewBase;
            if (listviewbase != null)
            {
                listviewbase.DataContextChanged += Itemscontrol_DataContextChanged;
                listviewbase.Loaded += Listviewbase_Loaded;
            }
        }
    }

    private void Listviewbase_Loaded(object sender, RoutedEventArgs e)
    {
        var listviewbase = sender as ListViewBase;
        AutomationPeer ap = ListViewAutomationPeer.CreatePeerForElement(listviewbase);
        ScrollViewerAutomationPeer avap = (ScrollViewerAutomationPeer)ap.GetPattern(PatternInterface.Scroll);
        sv = (ScrollViewer)avap.Owner;
        sv.ViewChanged += sv_ViewChanged;
    }

    private void Itemscontrol_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
    {
        if (args.NewValue == null) return;
        var listviewbase = AssociatedObject as ListViewBase;
        listviewbase.ScrollIntoView(listviewbase.Items.ElementAt(RestoreScrollOffset), ScrollIntoViewAlignment.Leading);
    }

    private void sv_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
    {
        if (e.IsIntermediate) return;
        RestoreScrollOffset = CurrentItemViewIndex;
    }

    public void Detach()
    {
        var listviewbase = AssociatedObject as ListViewBase;
        listviewbase.Loaded -= Listviewbase_Loaded;
        sv.ViewChanged -= sv_ViewChanged;
        sv = null;
    }
}

2. 使ってみよう
MainPage.xaml

<Page
    x:Class="App1.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:App1"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
      xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
      xmlns:Interactivity="using:Microsoft.Xaml.Interactivity"
      xmlns:Interactions="using:Microsoft.Xaml.Interactions.Core"
      mc:Ignorable="d">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="2*" />
            <ColumnDefinition Width="3*" />
        </Grid.ColumnDefinitions>
        <ListView x:Name="MasterListView"
                  ItemsSource="{x:Bind ViewModel.DetailViewModels}">
            <ListView.ItemTemplate>
                <DataTemplate x:DataType="local:DetailViewModel">
                    <TextBlock Text="{x:Bind Title}" />
                </DataTemplate>
            </ListView.ItemTemplate>
        </ListView>
        <ContentPresenter x:Name="DetailContent"
                          Grid.Column="1"
                          Content="{x:Bind Path=MasterListView.SelectedItem, Mode=OneWay}">
            <ContentPresenter.ContentTemplate>
                <DataTemplate x:Name="ListTemplate"
                              x:DataType="local:DetailViewModel">
                    <ListView ItemsSource="{x:Bind Items}">
                        <Interactivity:Interaction.Behaviors>
                            <local:ScrollOffsetControlBehavior RestoreScrollOffset="{x:Bind ReadOffset, Mode=TwoWay}" />
                        </Interactivity:Interaction.Behaviors>
                    </ListView>
                </DataTemplate>
            </ContentPresenter.ContentTemplate>
        </ContentPresenter>
    </Grid>
</Page>

MainPage.xaml.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Controls.Primitives;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Navigation;

// 空白ページのアイテム テンプレートについては、http://go.microsoft.com/fwlink/?LinkId=402352&clcid=0x409 を参照してください

namespace App1
{
    /// <summary>
    /// それ自体で使用できる空白ページまたはフレーム内に移動できる空白ページ。
    /// </summary>
    public sealed partial class MainPage : Page
    {
        MainPageViewModel ViewModel => new MainPageViewModel();

        public MainPage()
        {
            this.InitializeComponent();
        }
    }

    public class MainPageViewModel
    {
        public List<DetailViewModel> DetailViewModels { get; set; }

        

        public MainPageViewModel()
        {
            DetailViewModels = Enumerable.Range(1, 10).Select(i => new DetailViewModel(i)).ToList();
        }
    }
    
    public class DetailViewModel
    {
        public int ReadOffset { get; set; }
        public string Title { get; set; }
        public List<string> Items { get; set; }

        public DetailViewModel(int num)
        {
            Title = num.ToString();
            Items = Enumerable.Range(1, 100).Select(i => $"{num} : {i}").ToList();
        }
    }
}

3. サンプルsln
onedrive.live.com