在书中圣的版本更新历程中,碰到了诸多问题,其中一个就是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下载:http://files.cnblogs.com/zjfeiye/MoHoo.MultiColumnListBox.Demo.zip
我的另一个产品博客,关于桌面小工具和Windows Phone 7应用作品的,欢迎访问:http://mohoo.cc