逻辑分页是非常常用的一种性能优化的技术手段。虽然像ListView,ListBox有提供虚拟化的技术,能让我们加载大量数据而不会出现卡顿。但是……

实际上,虚拟化技术即便是开启后,我们在快速浏览和滚动或者是初始化加载数据的时候,会出现明显的卡顿,这并不符合我们的实际业务需要。因此,实现逻辑分页是非常有必要的。

实现逻辑分页也比较简单,主要是通过ScrollView来实现滚动加载,通常用于垂直滚动,分别可以像上滚动和像下滚动,水平滚动我们就不去考虑了。

常规的分页逻辑判断代码如下:

        //向上滚动
        if (e.VerticalOffset < 10 && e.VerticalChange < 0)
        {
            //do something
        }

        //向下滚动
        if ((e.ExtentHeight - (e.VerticalOffset + e.ViewportHeight)) < 10)
        {
            //do something
        }

        //滚动条是否可见,可用于针对数据模版不确定的场景,动态需要计算初始分页的条数。
        bool IsScrollBarVisible
        {
            get
            {
                return scrollViewer != null && scrollViewer.ExtentHeight > scrollViewer.ViewportHeight;
            }
        }

为了方便实现分页逻辑,我写了一个基于ItemsControlScrollViewer的基类,方便实现我们的分页逻辑:

/// <summary>
    /// 提供逻辑分页的基础实现
    /// 目标对象:ItemsControl
    /// </summary>
    public abstract class LogicPagingOperator : IDisposable
    {
        public LogicPagingOperator(ItemsControl itemsControl, int pageCapacity = 10)
        {
            ItemsSource = new ObservableCollection<object>();
            Initialize(itemsControl, pageCapacity);
        }

        protected int _pageCapacity;
        protected ScrollViewer ScrollViewer;
        protected ItemsControl ItemsControl;
        public IList OriginalSource { get; private set; }
        protected readonly ObservableCollection<object> ItemsSource;

        /// <summary>
        /// 当前页
        /// </summary>
        protected int PageIndex
        {
            get; set;
        }

        /// <summary>
        /// 逻辑源数据是否已经全部加载完成
        /// </summary>
        public bool IsLoaded
        {
            get; protected set;
        }

        private void Initialize(ItemsControl itemsControl, int pageCapacity)
        {
            ItemsControl = itemsControl;
            if (pageCapacity < 1)
            {
                ExceptionHelper.ThrowForDebug(new ArgumentException("无效的LogicPagingOperatorBase.PageCapacity值,值不能小于1"));
                pageCapacity = 1;
            }
            PageCapacity = pageCapacity;
            PageIndex = -1;
            ItemsControl.Loaded += ItemsControl_Loaded;
            ItemsControl.Unloaded += ItemsControl_Unloaded;
        }

        private void InnerReset()
        {
            ItemsSource.Clear();
            PageIndex = -1;
            IsLoaded = false;
        }

        public void SetItemsSource(IEnumerable value)
        {
            if (value == null)
            {
                OriginalSource = null;
                InnerReset();
                return;
            }

            if (value is IList list)
            {
                UnregistryNotifyCollectionChanged();

                InnerReset();

                OriginalSource = list;

                LoadNext();

                RegistryNotifyCollectionChanged();

                ItemsControl.ItemsSource = ItemsSource;
            }
            else
            {
                throw new NotSupportedException("只支持IList集合");
            }

        }

        /// <summary>
        /// 每逻辑页显示的数量
        /// </summary>
        public int PageCapacity
        {
            get
            {
                return _pageCapacity;
            }
            set
            {
                _pageCapacity = value;

                InnerReset();

                if (ItemsControl != null && ItemsControl.IsLoaded)
                {
                    LoadNext();
                }
            }
        }

        #region 数据加载

        /// <summary>
        /// 加载下一页
        /// </summary>
        public void LoadNext()
        {
            if (OriginalSource == null)
            {
                return;
            }

            if (IsLoaded)
            {
                return;
            }

            PageIndex++;
            OnLoadNext(OriginalSource);
        }

        protected virtual void OnLoadNext(IList list)
        {

        }

        #endregion

        #region 数据源变更处理

        void UnregistryNotifyCollectionChanged()
        {
            if (OriginalSource is INotifyCollectionChanged notifyCollectionChanged)
            {
                notifyCollectionChanged.CollectionChanged -= NotifyCollectionChanged_CollectionChanged;
            }
        }

        void RegistryNotifyCollectionChanged()
        {
            if (OriginalSource is INotifyCollectionChanged notifyCollectionChanged)
            {
                notifyCollectionChanged.CollectionChanged -= NotifyCollectionChanged_CollectionChanged;
                notifyCollectionChanged.CollectionChanged += NotifyCollectionChanged_CollectionChanged;
            }
        }

        void NotifyCollectionChanged_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:
                    OnSourceAdd(e);
                    break;
                case NotifyCollectionChangedAction.Remove:
                    OnSourceRemove(e);
                    break;
                case NotifyCollectionChangedAction.Replace:
                    OnSourceReplace(e);
                    break;
                case NotifyCollectionChangedAction.Move:
                    OnSourceMove(e);
                    break;
                case NotifyCollectionChangedAction.Reset:
                    OnSourceClear(e);
                    break;
                default:
                    break;
            }
        }

        protected virtual void OnSourceAdd(NotifyCollectionChangedEventArgs e)
        {
            if (e.NewStartingIndex < ItemsSource.Count && e.NewStartingIndex > -1)
            {
                ItemsSource.Insert(e.NewStartingIndex, e.NewItems[0]);
            }
            else
            {
                foreach (var item in e.NewItems)
                {
                    ItemsSource.Add(item);
                }
            }
        }

        protected virtual void OnSourceRemove(NotifyCollectionChangedEventArgs e)
        {
            if (e.OldStartingIndex >= 0 && e.OldStartingIndex < ItemsSource.Count)
                ItemsSource.RemoveAt(e.OldStartingIndex);
        }

        protected virtual void OnSourceMove(NotifyCollectionChangedEventArgs e)
        {
            OnSourceRemove(e);
            OnSourceAdd(e);
        }

        protected virtual void OnSourceReplace(NotifyCollectionChangedEventArgs e)
        {
            if (e.NewStartingIndex < ItemsSource.Count && e.NewStartingIndex >= 0 && e.NewItems != null)
            {
                ItemsSource[e.NewStartingIndex] = e.NewItems[0];
            }
        }

        protected virtual void OnSourceClear(NotifyCollectionChangedEventArgs e)
        {
            ItemsSource.Clear();
            PageIndex = -1;
        }

        #endregion

        #region 滚动条逻辑处理

        void FindScrollView(ItemsControl itemsControl)
        {
            if (itemsControl != null && ScrollViewer == null)
            {
                ScrollViewer = itemsControl.VisualAncestor<ScrollViewer>();
                if (ScrollViewer == null)
                {
                    ScrollViewer = itemsControl.VisualDescendant<ScrollViewer>();
                }
            }
        }

        void RegistryScrollChanged(ScrollViewer scrollViewer)
        {
            if (scrollViewer != null)
            {
                scrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
                scrollViewer.ScrollChanged += ScrollViewer_ScrollChanged;
            }
        }

        void UnregistryScrollChanged()
        {
            if (ScrollViewer != null)
            {
                ScrollViewer.ScrollChanged -= ScrollViewer_ScrollChanged;
            }
        }

        void ScrollViewer_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            OnScrollChanged(e);
        }

        protected virtual void OnScrollChanged(ScrollChangedEventArgs e)
        {

        }

        #endregion

        #region 事件处理

        private void ItemsControl_Unloaded(object sender, System.Windows.RoutedEventArgs e)
        {
            UnregistryNotifyCollectionChanged();
            UnregistryScrollChanged();
            OnUnloaded();
        }

        private void ItemsControl_Loaded(object sender, System.Windows.RoutedEventArgs e)
        {
            FindScrollView(ItemsControl);
            RegistryNotifyCollectionChanged();
            RegistryScrollChanged(ScrollViewer);
            OnLoaded();
        }

        protected virtual void OnLoaded()
        {

        }

        protected virtual void OnUnloaded()
        {

        }

        #endregion

        public void Dispose()
        {
            ItemsSource.Clear();
            ItemsControl = null;
        }
    }

觉得文章好,请点个赞,给点动力^_^


本文会经常更新,请阅读原文: https://huchengv5.gitee.io//post/WPF-ScrollView%E5%AE%9E%E7%8E%B0%E7%BF%BB%E9%A1%B5%E7%9A%84%E5%9F%BA%E7%A1%80%E6%93%8D%E4%BD%9C%E4%BA%8B%E4%BB%B6.html ,以避免陈旧错误知识的误导,同时有更好的阅读体验。

知识共享许可协议 本作品采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 进行许可。欢迎转载、使用、重新发布,但务必保留文章署名胡承(包含链接: https://huchengv5.gitee.io/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。如有任何疑问,请 与我联系