しっぽを追いかけて

ぐるぐるしながら考えています

Unity と猫の話題が中心   掲載内容は個人の私見であり、所属組織の見解ではありません

UI に表示されるデータの仮想化(2)

UI に表示されるデータの仮想化(1) - しっぽを追いかけて では表示用にコピーされるデータを絞り込む仮想化をご紹介しましたが、データの仮想化には他にも方法があります

それが段階的な仮想化と呼ばれるもので下記のようなイメージになります

f:id:matatabi_ux:20140802131106p:plain

前回のランダムアクセスの仮想化 では全件確保していたデータソースですが、ここを仮想化する仕組みになります

最初から全件のデータを取得しておくのではなく、画面スクロールにより必要になったタイミングでデータを生成し表示することで、画面の初期表示を速めようという遅延ローディングの仕組みというわけです

具体的には ListView や GridView の ItemsSource に ISupportIncrementalLoading のインターフェースを実装したコレクションを設定すれば段階的な仮想化が動作するようになります

※ ただし、UI 仮想化が必要なので、ItemsPanel に StackPanel や VariableSizedWrapGrid などを設定すると動作しません!

作り方はそんなに難しくはありません

まずは ObservableCollection を継承し、ISupportIncrementalLoading を実装したコレクションを作ります

/// <summary>
/// 段階的な仮想化をサポートする ObservableCollection
/// </summary>
/// <typeparam name="T">コレクションアイテムの型</typeparam>
public class IncrementalObservableCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading where T : new() 
{
    /// <summary>
    /// 段階的な仮想化をサポートするプレゼンテーションロジック
    /// </summary>
    public ISupportIncrementalLoading Presenter { get; set; }

    /// <summary>
    /// コレクションをもとに新たなインスタンスを生成する
    /// </summary>
    /// <param name="collection">生成元のコレクション</param>
    public IncrementalObservableCollection(IEnumerable<T> collection)
        : base(collection)
    {
    }

    #region ISupportIncrementalLoading
    /// <summary>
    /// 読み込めるデータがこれ以上あるかどうか
    /// </summary>
    public bool HasMoreItems
    {
        get { return this.Presenter.HasMoreItems; }
    }

    /// <summary>
    /// ビューから段階的な仮想化を初期化する
    /// </summary>
    /// <param name="count">読み込まれた項目の数</param>
    /// <returns>読み込み操作の折り返し結果</returns>
    public Windows.Foundation.IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
    {
        return this.Presenter.LoadMoreItemsAsync(count);
    }
    #endregion //ISupportIncrementalLoading
}

ただのいれものとして使いたいので ISupportIncrementalLoading の実際の処理は下記のような感じで MVPVM の Presenter に任せています

/// <summary>
/// トップ画面の Presenter
/// </summary>
public class TopPagePresenter : PresenterBase, IPresenter<TopPage, TopPageViewModel>, ISupportIncrementalLoading
{
    #region IPresenter<View, ViewModel>
    /// <summary>
    /// View
    /// </summary>
    public TopPage View
    {
        get { return this.PresenterView as TopPage; }
        set { this.PresenterView = value; }
    }

    /// <summary>
    /// ViewModel
    /// </summary>
    public TopPageViewModel ViewModel
    {
        get { return this.PresenterViewModel as TopPageViewModel; }
        set { this.PresenterViewModel = value; }
    }
    #endregion //IPresenter<View, ViewModel>

    /// <summary>
    /// 画面に遷移したときの処理
    /// </summary>
    /// <param name="navigationParameter">遷移パラメータ</param>
    /// <param name="navigationMode">遷移モード</param>
    /// <param name="viewModelState">画面状態データ</param>
    public override void OnNavigatedTo(object navigationParameter, NavigationMode navigationMode, Dictionary<string, object> viewModelState)
    {
        base.OnNavigatedTo(navigationParameter, navigationMode, viewModelState);

        if (this.ViewModel.Items.Count > 0)
        {
            return;
        }

        this.ViewModel.Items = new IncrementalObservableCollection<ItemContainerViewModel>(this.GetItems()) { Presenter = this };
    }

    /// <summary>
    /// データを取得する
    /// </summary>
    /// <returns>写真情報</returns>
    private IEnumerable<ItemContainerViewModel> GetItems()
    {
        foreach (var photo in App.AppSettings.Data.Items)
        {
            var item = PhotoViewModel.ConvertFrom(photo);

            yield return new ItemContainerViewModel()
            {
                UniqueId = Guid.NewGuid().ToString(),
                ContentId = item.UniqueId,
                Content = item,
                IsClickable = true,
            };
        }
    }

    #region ISupportIncrementalLoading
    /// <summary>
    /// 読み込めるデータがこれ以上あるかどうか
    /// </summary>
    public bool HasMoreItems
    {
        // 無限に取得する
        get { return true; }
    }

    /// <summary>
    /// ビューから段階的な仮想化を初期化する
    /// </summary>
    /// <param name="count">読み込まれた項目の数</param>
    /// <returns>読み込み操作の折り返し結果</returns>
    public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
    {
        return Task.Run(() =>
        {
            var items = this.GetItems();

            foreach (var item in items)
            {
                this.View.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                {
                    this.ViewModel.Items.Add(item);
                });
            }

            return new LoadMoreItemsResult() { Count = (uint)items.Count() };
        })
        .AsAsyncOperation<LoadMoreItemsResult>();
    }
    #endregion //ISupportIncrementalLoading
}

このコードでスクロールし続けてみた様子が下記の動画です


段階的なデータ仮想化 - YouTube

初期表示が一瞬なのに、画面に現れるタイミングで延々とスクロールし続けていおり、スクロールバーの位置が何度も戻りつつスライダー幅が徐々に小さくなっているのがわかると思います