ListBox优化初步(一)

在书中圣的版本更新历程中,碰到了诸多问题,其中一个就是ListBox当使用WrapPanel时的性能问题

WrapPanel不是原生控件,它来自于



使用它可以实现ListBox的多列布局(如图一),但是它是不支持虚拟化的,关于虚拟化,请自行搜索相关资料。
这里简单提一下,虚拟化分为视觉虚拟化和数据虚拟化,这里我们只关注视觉虚拟化,以下提到的虚拟化也都特指视觉虚拟化

002

虚拟化可以让ListBox(ItemsControl)的项只有位于显示区域时才去渲染,大大节约了内存和CPU/GPU消耗,对于大量数据可以说是必须的

ListBox的默认的ItemsPanel容器是VirtualizingStackPanel,从名字可以看出是支持虚拟化的,但是WrapPanel不支持虚拟化,它继承自Panel,所以当你的ListBox的ItemsPanel的容器替换成WrapPanel后也就失去了虚拟化的能力,当数据变得很多时就会非常的卡

那么如何解决呢?
1) 你自己写个VirtualizingWrapPanel,我认为这是王道,目前正在查资料尝试,看能不能写出来
2) 用取巧的方法,使一行容纳多列数据,仍然使用VirtualizingStackPanel作为容器,那么也就是能保证虚拟化了

这里我们介绍第二种方式

那么如何使一行容纳多个数据呢?答案是在ListBoxItem中嵌套ListBox/ItemsControl

当时经过一番Google后发现已经有人解决了该问题,呃,抱歉时间有点长了,原文找不到了,这里将他的代码给出,我自己写的就不献丑啦

其原理是将一个长列表,切分成若干个长度为列数的小列表,下面是实现代码:

    public class RowAdapter<TItemType> : IList<IEnumerable<TItemType>>, INotifyCollectionChanged
    {
        private readonly IList<TItemType> _sourceList;
        private readonly int _columns;

        public IList<TItemType> SourceList
        {
            get { return _sourceList; }
        } 

        private class RowObject : IEnumerable<TItemType>
        {
            internal readonly RowAdapter<TItemType> Parent;
            internal readonly int StartIndex;

            public RowObject(RowAdapter<TItemType> parent, int startIndex)
            {
                Parent = parent;
                StartIndex = startIndex;
            }

            #region IEnumerable<TItemType> Members

            public IEnumerator<TItemType> GetEnumerator()
            {
                int limit = Parent._sourceList.Count;
                int end = Math.Min(StartIndex + Parent._columns, limit);

                for (int pos = StartIndex; pos < end; ++pos)
                {
                    yield return Parent._sourceList[pos];
                }
            }

            #endregion

            #region IEnumerable Members

            System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
            {
                return GetEnumerator();
            }

            #endregion
        }

        public RowAdapter(IList<TItemType> sourceList, int columns)
        {
            if (null == sourceList)
                throw new ArgumentNullException("sourceList", "Resource.RowAdapter_RowAdapter_sourceList_is_null");
            if (columns <= 0)
                throw new ArgumentOutOfRangeException("columns", "Resource.RowAdapter_RowAdapter_ColumnsGreaterOne");

            // We require the source list to implement IList because we
            // need to know how many item there are
            _sourceList = sourceList;
            _columns = columns;

            var sourceNotify = sourceList as INotifyCollectionChanged;
            if (null != sourceNotify)
            {
                sourceNotify.CollectionChanged += OnSourceCollectionChanged;
            }
        }

        #region IList<IEnumerable<TItemType>> Members

        public int IndexOf(IEnumerable<TItemType> item)
        {
            var realItem = item as RowObject;
            if (null == realItem || !ReferenceEquals(realItem.Parent, this))
                return -1;          // It does not belong to this collection

            Debug.Assert(0 == realItem.StartIndex % _columns, "RowObject item has a wierd index");
            return realItem.StartIndex / _columns;
        }

        public void Insert(int index, IEnumerable<TItemType> item)
        {
            throw new NotSupportedException();
        }

        public IEnumerable<TItemType> this[int index]
        {
            get
            {
                if (index < 0 || index > Count)
                    return null;

                return InternalGetRow(index);
            }
            set
            {
                throw new NotSupportedException();
            }
        }

        public void RemoveAt(int index)
        {
            throw new NotSupportedException();
        }

        #endregion

        #region ICollection<IEnumerable<TItemType>> Members

        public void Add(IEnumerable<TItemType> item)
        {
            throw new NotSupportedException();
        }

        public bool Contains(IEnumerable<TItemType> item)
        {
            var realItem = item as RowObject;
            return null != realItem && object.ReferenceEquals(realItem.Parent, this);
        }

        public void CopyTo(IEnumerable<TItemType>[] array, int arrayIndex)
        {
            // I haven't implemented this. It is easy to implement if you need it
            throw new NotImplementedException();
        }

        public bool Remove(IEnumerable<TItemType> item)
        {
            throw new NotSupportedException();
        }
        public void Clear()
        {
            throw new NotSupportedException();
        }

        public int Count
        {
            get
            {
                return (_sourceList.Count + (_columns - 1)) / _columns;
            }
        }

        public bool IsReadOnly
        {
            get { return true; }
        }

        #endregion

        #region IEnumerable<IEnumerable<TItemType>> Members

        public IEnumerator<IEnumerable<TItemType>> GetEnumerator()
        {
            for (int i = 0; i < Count; ++i)
            {
                yield return InternalGetRow(i);
            }
        }

        #endregion

        #region IEnumerable Members

        System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region INotifyCollectionChanged Members

        public event NotifyCollectionChangedEventHandler CollectionChanged;

        private void FireCollectionChanged()
        {

            var handler = CollectionChanged;
            if (null != handler)
            {
                handler(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
            }
        }

        private void OnSourceCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            FireCollectionChanged();
        }

        #endregion

        private RowObject InternalGetRow(int index)
        {
            return new RowObject(this, index * _columns);
        }
    }

 

那么如何使用呢?

            <ListBox x:Name="demoList">
                <ListBox.ItemTemplate>
                    <DataTemplate>
                        <ListBox ItemsSource="{Binding}">
                            <ListBox.ItemsPanel>
                                <ItemsPanelTemplate>
                                    <StackPanel Orientation="Horizontal"/>
                                </ItemsPanelTemplate>
                            </ListBox.ItemsPanel>
                            <ListBox.ItemTemplate>
                                <DataTemplate>
                                    <Border Width="180" Height="180" Margin="10" Background="{StaticResource PhoneAccentBrush}">
                                        <TextBlock Text="{Binding}"/>
                                    </Border>
                                </DataTemplate>
                            </ListBox.ItemTemplate>
                        </ListBox>
                    </DataTemplate>
                </ListBox.ItemTemplate>
            </ListBox>
 
this.demoList.ItemsSource = new RowAdapter<string>(DataGen.Gen(), 2);
 

大家可以对比我的Demo,使用WrapPanel和使用这种切分方式,性能相差非常大

 

Demo下载:https://files.cnblogs.com/zjfeiye/MoHoo.MultiColumnListBox.Demo.zip

 

我的另一个产品博客,关于桌面小工具和Windows Phone 7应用作品的,欢迎访问:http://mohoo.cc

posted @ 2011-07-15 10:05  阿干@NET  阅读(4374)  评论(10编辑  收藏  举报