博客園客戶端(Universal App)開發隨筆 -- 增量加載 (incremental loading)


在我們的應用(博客園UAP)中,加入了ListView上拉獲取更多內容的功能(GridView也可以),這個功能是通過ISupportIncrementalLoading接口實現的,這是個從Windows 8就開始提供的接口(當然你可以通過ScrollViewer來實現這個功能,只不過稍微麻煩點,還要自己再封裝。。)。

這個接口的定義十分簡單:

public interface ISupportIncrementalLoading
    {

        bool HasMoreItems { get; }


        IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count);
    }

LoadMoreItemsAsync是由ListView自動調用(當IncrementalLoadingTrigger = IncrementalLoadingTrigger.Edge時), count參數表示每次取多少個項目,這個參數由ListView.DataFetchSizeListView.IncrementalLoadingThreshold決定的,HasMoreItems用來告訴ListView是否還有更多的項目可以加載,False的話即使ListView已經滾到底,也不會再觸發LoadMoreIemsAsync。

 

PS:使用ISupportIncrementalLoading的過程中,你會發現LoadMoreItemsAsync被連續調用了2次,第一次調用的時候總是count=1,這是因為ListView需要用第一個項目來確定虛擬化項目的數量(關於虛擬化的介紹有點多,請參考MSDN,簡單點說效果就是你的ListView/GridView即使要顯示幾萬個項目,內存也不會爆掉)。這對於MSDN上的sample來說是沒什么問題的,因為數據出來的很快,但是在需要聯網的應用中你會發現初始只有一個項目,過一會兒其他的幾十個又出來了,雖然這么做是有理由的,但看上去有點詭異,所以我們在實現中忽略掉了count參數,每次都加載固定數量項目。

實現

MSDN上提供了一個簡單通用的實現方式,這個sample通過實現IList來存放數據,然后再實現INotifyCollectionChanged來通知項目的變化。

public abstract class IncrementalLoadingBase: IList, ISupportIncrementalLoading, INotifyCollectionChanged 
    { 
        // 省略IList的實現
        
        // 省略INotifyCollectionChanged的實現

        public bool HasMoreItems 
        { 
            get { return HasMoreItemsOverride(); } 
        } 
 
        public Windows.Foundation.IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count) 
        { 
            if (_busy) 
            { 
                throw new InvalidOperationException("Only one operation in flight at a time"); 
            } 
 
            _busy = true; 
 
            return AsyncInfo.Run((c) => LoadMoreItemsAsync(c, count)); 
        } 

        async Task<LoadMoreItemsResult> LoadMoreItemsAsync(CancellationToken c, uint count) 
        { 
            try 
            { 
                var items = await LoadMoreItemsOverrideAsync(c, count); 
                var baseIndex = _storage.Count; 
 
                _storage.AddRange(items); 
 
                NotifyOfInsertedItems(baseIndex, items.Count); 
 
                return new LoadMoreItemsResult { Count = (uint)items.Count }; 
            } 
            finally 
            { 
                _busy = false; 
            } 
        } 
}

為了簡化實現,我們通過繼承 ObservableCollection<T>來達到存放項目、通知變化的目的,並且添加了OnLoadMoreStarted和OnLoadMoreCompleted事件,方便對UI進行更新操作(比如進度條)。

public abstract class IncrementalLoadingBase<T> : ObservableCollection<T>, ISupportIncrementalLoading
    {
        public bool HasMoreItems
        {
            get { return HasMoreItemsOverride(); }
        }

        public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
        {
            if (_busy)
            {
                throw new InvalidOperationException("Only one operation in flight at a time"); 
            }

            _busy = true;

            return AsyncInfo.Run((c) => LoadMoreItemsAsync(c, count));
        }

        protected async Task<LoadMoreItemsResult> LoadMoreItemsAsync(CancellationToken c, uint count)
        {
            try
            {
                // 加載開始事件
                if (this.OnLoadMoreStarted != null)
                {
                    this.OnLoadMoreStarted(count);
                }

                var items = await LoadMoreItemsOverrideAsync(c, count);

                AddItems(items);

                // 加載完成事件
                if (this.OnLoadMoreCompleted != null)
                {
                    this.OnLoadMoreCompleted(items == null ? 0 : items.Count);
                }

                return new LoadMoreItemsResult { Count = items == null ? 0 : (uint)items.Count };
            }
            finally
            {
                _busy = false;
            }
        }


        public delegate void LoadMoreStarted(uint count);
        public delegate void LoadMoreCompleted(int count);

        public event LoadMoreStarted OnLoadMoreStarted;
        public event LoadMoreCompleted OnLoadMoreCompleted;

        /// <summary>
        /// 將新項目添加進來,之所以是virtual的是為了方便特殊要求,比如不重復之類的
        /// </summary>
        protected virtual void AddItems(IList<T> items)
        {
            if (items != null)
            {
                foreach (var item in items)
                {
                    this.Add(item);
                }
            }
        }

        protected abstract Task<IList<T>> LoadMoreItemsOverrideAsync(CancellationToken c, uint count);

        protected abstract bool HasMoreItemsOverride();


        protected bool _busy = false;
    }

這樣一個簡單的增量加載的基類就有了,只需要在子類中實現LoadMoreItemsOverrideAsync方法,並綁定到ListView.ItemsSource即可。

使用效果如下兩張圖(請注意首頁右上角的數字變化和右下的滾動條):

   

你喜歡MVVM?那你就需要在這個類的基礎上進行下修改了,把取數據的邏輯從中分離,並當作一個collection使用,下面是一個簡單的實現。

public class IncrementalLoadingCollection<T> : ObservableCollection<T>, ISupportIncrementalLoading
{
    // 這里為了簡單使用了Tuple<IList<T>, bool>作為返回值,第一項是新項目集合,第二項是否還有更多,也可以自定義實體類
    Func<uint, Task<Tuple<List<T>, bool>>> _dataFetchDelegate = null;

    public IncrementalLoadingCollection(Func<uint, Task<Tuple<List<T>, bool>>> dataFetchDelegate)
    {
        if (dataFetchDelegate == null) throw new ArgumentNullException("dataFetchDelegate");

        this._dataFetchDelegate = dataFetchDelegate;
    }

    public bool HasMoreItems
    {
        get;
        private set;
    }

    public IAsyncOperation<LoadMoreItemsResult> LoadMoreItemsAsync(uint count)
    {
        if (_busy)
        {
            throw new InvalidOperationException("Only one operation in flight at a time");
        }

        _busy = true;

        return AsyncInfo.Run((c) => LoadMoreItemsAsync(c, count));
    }

    protected async Task<LoadMoreItemsResult> LoadMoreItemsAsync(CancellationToken c, uint count)
    {
        try
        {
            if (this.OnLoadMoreStarted != null)
            {
                this.OnLoadMoreStarted(count);
            }

            // 我們忽略了CancellationToken,因為我們暫時不需要取消,需要的可以加上
            var result = await this._dataFetchDelegate(count);

            var items = result.Item1;

            if (items != null)
            {
                foreach (var item in items)
                {
                    this.Add(item);
                }
            }

            // 是否還有更多
            this.HasMoreItems = result.Item2;

            // 加載完成事件
            if (this.OnLoadMoreCompleted != null)
            {
                this.OnLoadMoreCompleted(items == null ? 0 : items.Count);
            }

            return new LoadMoreItemsResult { Count = items == null ? 0 : (uint)items.Count };
        }
        finally
        {
            _busy = false;
        }
    }


    public delegate void LoadMoreStarted(uint count);
    public delegate void LoadMoreCompleted(int count);

    public event LoadMoreStarted OnLoadMoreStarted;
    public event LoadMoreCompleted OnLoadMoreCompleted;

    protected bool _busy = false;
}

現在這個家伙變成了一個collection,創建實例的時候傳入對應的代理,然后在你的View里面把他綁定到ItemsSource上吧。

var collection = new IncrementalLoadingCollection<string>(count =>
            {
                // 對應的get more邏輯

                return Task.Run(() => Tuple.Create(new List<string> { "我是假數據" }, true));
            });

小結

增量加載可以在需要顯示大量數據的時候,給用戶提供一個更平滑的體驗,而通過使用ISupportIncrementalLoading,我們只需要實現取數據的邏輯,什么時候調用就不需要我們關心了。歡迎大家繼續關注。

 

Windows Phone Store App link:

http://www.windowsphone.com/zh-cn/store/app/博客園-uap/500f08f0-5be8-4723-aff9-a397beee52fc

 

Windows Store App link:

http://apps.microsoft.com/windows/zh-cn/app/c76b99a0-9abd-4a4e-86f0-b29bfcc51059

 

GitHub open source link:

https://github.com/MS-UAP/cnblogs-UAP

 

MSDN Sample Code:

https://code.msdn.microsoft.com/CNBlogs-Client-Universal-477943ab


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM