ListBox优化初步(一)
在书中圣的版本更新历程中,碰到了诸多问题,其中一个就是ListBox当使用WrapPanel时的性能问题
WrapPanel不是原生控件,它来自于
使用它可以实现ListBox的多列布局(如图一),但是它是不支持虚拟化的,关于虚拟化,请自行搜索相关资料。
这里简单提一下,虚拟化分为视觉虚拟化和数据虚拟化,这里我们只关注视觉虚拟化,以下提到的虚拟化也都特指视觉虚拟化
虚拟化可以让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