.net WPF ListView虚拟化分组
有人知道在启用分组时支持UI虚拟化的ListView实现吗?默认情况下,在设置分组时禁用VirtualzingStackPanel.net WPF ListView虚拟化分组,.net,wpf,performance,listview,virtualization,.net,Wpf,Performance,Listview,Virtualization,有人知道在启用分组时支持UI虚拟化的ListView实现吗?默认情况下,在设置分组时禁用VirtualzingStackPanel 微软似乎不打算在.NET Framework的v4.0版本中实现这一点,因此我正在寻找替代解决方案。一个选择是看看Bea Stollniz关于改进TreeView性能的系列文章: ,及。虽然她所做的更适合于TreeView,因为默认情况下它们是分组的,所以没有任何虚拟化,但所学到的经验肯定可以应用于具有虚拟化组的自定义ListView。事实上,在第3部分中,她使用列
微软似乎不打算在.NET Framework的v4.0版本中实现这一点,因此我正在寻找替代解决方案。一个选择是看看Bea Stollniz关于改进TreeView性能的系列文章:
,及。虽然她所做的更适合于TreeView,因为默认情况下它们是分组的,所以没有任何虚拟化,但所学到的经验肯定可以应用于具有虚拟化组的自定义ListView。事实上,在第3部分中,她使用列表框作为基础来创建虚拟化树,这也是虚拟化分组的一个良好开端。显然,在树状视图中显示项目与分组的ListView有一些不同,例如组节点的选择,但这可以通过捕获SelectionChanged来解决。我找到了一个示例,该示例将分组的ListView转换为支持虚拟化的平面列表。但是,我无法想象如何模拟头的扩展动作。wpf/.net4.5现在支持这一点
如果您仍然以4.0为目标,您可以将其设置为反射,这样至少一些用户可以从中受益。我希望这不会太离题,但我最近遇到了类似的问题。如上所述,它只是.NET4.0版本。我甚至同意,在大多数情况下,对于组合框,您通常不需要虚拟化,因为它不应该有那么多的项,如果需要分组,那么应该实现某种主-细节解决方案。但可能存在一些灰色区域 Luke提供的关于MSDN上分组和虚拟化的链接对我帮助很大。在我的情况下,这是唯一的方法,我能够找到或找到任何地方,在我需要的方向。它不支持ListViewCollection中的所有功能。我必须覆盖一些方法,否则项目的选择将无法正常工作。显然还有更多的工作要做 以下是FlatGroupListCollectionView的更新解决方案:
//
///提供一个视图,用于将组展平到列表中
///这是为了避免ListCollectionView在.NET 4.0中的限制,如果使用分组,则虚拟化将不起作用
///为了支持这种分组方式,它假设视图中有一些适当的推进(XAML)
///注意:在实现时,它不支持嵌套分组
///注意:只有重写的属性和方法行为正确,与项选择相关的某些方法和属性可能无法按预期工作,需要新的实现
///
公共类FlatGroupListCollectionView:ListCollectionView
{
///
///初始化类的新实例。
///
///此集合中使用的列表
公共FlatGroupListCollectionView(IList列表)
:基本(列表)
{
}
///
///目前只支持一级分组
///如果索引与标题匹配,则返回CollectionViewGroups
///否则,将索引映射到基本范围以获取实际项目
///
///从中获取项目的索引
///在给定索引中找到的项
公共重写对象GetItemAt(int索引)
{
int delta=0;
ReadOnlyObservableCollection groups=this.BaseGroups;
如果(组!=null)
{
int totalCount=0;
对于(int i=0;i/// <summary>
/// Provides a view that flattens groups into a list
/// This is used to avoid limitation that ListCollectionView has in .NET 4.0, if grouping is used then Virtialuzation would not work
/// It assumes some appropriate impelmentation in view(XAML) in order to support this way of grouping
/// Note: As implemented, it does not support nested grouping
/// Note: Only overriden properties and method behaves correctly, some of methods and properties related to selection of item might not work as expected and would require new implementation
/// </summary>
public class FlatGroupListCollectionView : ListCollectionView
{
/// <summary>
/// Initializes a new instance of the <see cref="FlatGroupListCollectionView"/> class.
/// </summary>
/// <param name="list">A list used in this collection</param>
public FlatGroupListCollectionView(IList list)
: base(list)
{
}
/// <summary>
/// This currently only supports one level of grouping
/// Returns CollectionViewGroups if the index matches a header
/// Otherwise, maps the index into the base range to get the actual item
/// </summary>
/// <param name="index">Index from which get an item</param>
/// <returns>Item that was found on given index</returns>
public override object GetItemAt(int index)
{
int delta = 0;
ReadOnlyObservableCollection<object> groups = this.BaseGroups;
if (groups != null)
{
int totalCount = 0;
for (int i = 0; i < groups.Count; i++)
{
CollectionViewGroup group = groups[i] as CollectionViewGroup;
if (group != null)
{
if (index == totalCount)
{
return group;
}
delta++;
int numInGroup = group.ItemCount;
totalCount += numInGroup + 1;
if (index < totalCount)
{
break;
}
}
}
}
object item = base.GetItemAt(index - delta);
return item;
}
/// <summary>
/// In the flat list, the base count is incremented by the number of groups since there are that many headers
/// To support nested groups, the nested groups must also be counted and added to the count
/// </summary>
public override int Count
{
get
{
int count = base.Count;
if (this.BaseGroups != null)
{
count += this.BaseGroups.Count;
}
return count;
}
}
/// <summary>
/// By returning null, we trick the generator into thinking that we are not grouping
/// Thus, we avoid the default grouping code
/// </summary>
public override ReadOnlyObservableCollection<object> Groups
{
get
{
return null;
}
}
/// <summary>
/// Gets the Groups collection from the base class
/// </summary>
private ReadOnlyObservableCollection<object> BaseGroups
{
get
{
return base.Groups;
}
}
/// <summary>
/// DetectGroupHeaders is a way to get access to the containers by setting the value to true in the container style
/// That way, the change handler can hook up to the container and provide a value for IsHeader
/// </summary>
public static readonly DependencyProperty DetectGroupHeadersProperty =
DependencyProperty.RegisterAttached("DetectGroupHeaders", typeof(bool), typeof(FlatGroupListCollectionView), new FrameworkPropertyMetadata(false, OnDetectGroupHeaders));
/// <summary>
/// Gets the Detect Group Headers property
/// </summary>
/// <param name="obj">Dependency Object from which the property is get</param>
/// <returns>Value of Detect Group Headers property</returns>
public static bool GetDetectGroupHeaders(DependencyObject obj)
{
return (bool)obj.GetValue(DetectGroupHeadersProperty);
}
/// <summary>
/// Sets the Detect Group Headers property
/// </summary>
/// <param name="obj">Dependency Object on which the property is set</param>
/// <param name="value">Value to set to property</param>
public static void SetDetectGroupHeaders(DependencyObject obj, bool value)
{
obj.SetValue(DetectGroupHeadersProperty, value);
}
/// <summary>
/// IsHeader can be used to style the container differently when it is a header
/// For instance, it can be disabled to prevent selection
/// </summary>
public static readonly DependencyProperty IsHeaderProperty =
DependencyProperty.RegisterAttached("IsHeader", typeof(bool), typeof(FlatGroupListCollectionView), new FrameworkPropertyMetadata(false));
/// <summary>
/// Gets the Is Header property
/// </summary>
/// <param name="obj">Dependency Object from which the property is get</param>
/// <returns>Value of Is Header property</returns>
public static bool GetIsHeader(DependencyObject obj)
{
return (bool)obj.GetValue(IsHeaderProperty);
}
/// <summary>
/// Sets the Is Header property
/// </summary>
/// <param name="obj">Dependency Object on which the property is set</param>
/// <param name="value">Value to set to property</param>
public static void SetIsHeader(DependencyObject obj, bool value)
{
obj.SetValue(IsHeaderProperty, value);
}
/// <summary>
/// Raises the System.Windows.Data.CollectionView.CollectionChanged event.
/// </summary>
/// <param name="args">The System.Collections.Specialized.NotifyCollectionChangedEventArgs object to pass to the event handler</param>
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
switch (args.Action)
{
case NotifyCollectionChangedAction.Add:
{
int flatIndex = this.ConvertFromItemToFlat(args.NewStartingIndex, false);
int headerIndex = Math.Max(0, flatIndex - 1);
object o = this.GetItemAt(headerIndex);
CollectionViewGroup group = o as CollectionViewGroup;
if ((group != null) && (group.ItemCount == args.NewItems.Count))
{
// Notify that a header was added
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new object[] { group }, headerIndex));
}
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, args.NewItems, flatIndex));
}
break;
case NotifyCollectionChangedAction.Remove:
// TODO: Implement this action
break;
case NotifyCollectionChangedAction.Move:
// TODO: Implement this action
break;
case NotifyCollectionChangedAction.Replace:
// TODO: Implement this action
break;
default:
base.OnCollectionChanged(args);
break;
}
}
/// <summary>
/// Sets the specified item to be the System.Windows.Data.CollectionView.CurrentItem in the view
/// This is an override of base method, an item index is get first and its needed to convert that index to flat version which includes groups
/// Then adjusted version of MoveCurrentToPosition base method is called
/// </summary>
/// <param name="item">The item to set as the System.Windows.Data.CollectionView.CurrentItem</param>
/// <returns>true if the resulting System.Windows.Data.CollectionView.CurrentItem is within the view; otherwise, false</returns>
public override bool MoveCurrentTo(object item)
{
int index = this.IndexOf(item);
int newIndex = this.ConvertFromItemToFlat(index, false);
return this.MoveCurrentToPositionBase(newIndex);
}
/// <summary>
/// Sets the item at the specified index to be the System.Windows.Data.CollectionView.CurrentItem in the view
/// This is an override of base method, Its called when user selects new item from this collection
/// A delta is get of which is the possition shifted because of groups and we shift this position by this delta and then base method is called
/// </summary>
/// <param name="position">The index to set the System.Windows.Data.CollectionView.CurrentItem to</param>
/// <returns>true if the resulting System.Windows.Data.CollectionView.CurrentItem is an item within the view; otherwise, false</returns>
public override bool MoveCurrentToPosition(int position)
{
int delta = this.GetDelta(position);
int newPosition = position - delta;
return base.MoveCurrentToPosition(newPosition);
}
private static void OnDetectGroupHeaders(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
// This assumes that a container will not change between being a header and not
// If using ContainerRecycling this may not be the case
((FrameworkElement)d).Loaded += OnContainerLoaded;
}
private static void OnContainerLoaded(object sender, RoutedEventArgs e)
{
FrameworkElement element = (FrameworkElement)sender;
element.Loaded -= OnContainerLoaded; // If recycling, remove this line
// CollectionViewGroup is the type of the header in this sample
// Add more types or change the type as necessary
if (element.DataContext is CollectionViewGroup)
{
SetIsHeader(element, true);
}
}
private int ConvertFromItemToFlat(int index, bool removed)
{
ReadOnlyObservableCollection<object> groups = this.BaseGroups;
if (groups != null)
{
int start = 1;
for (int i = 0; i < groups.Count; i++)
{
CollectionViewGroup group = groups[i] as CollectionViewGroup;
if (group != null)
{
index++;
int end = start + group.ItemCount;
if ((start <= index) && ((!removed && index < end) || (removed && index <= end)))
{
break;
}
start = end + 1;
}
}
}
return index;
}
/// <summary>
/// Move <seealso cref="CollectionView.CurrentItem"/> to the item at the given index.
/// This is a replacement for base method
/// </summary>
/// <param name="position">Move CurrentItem to this index</param>
/// <returns>true if <seealso cref="CollectionView.CurrentItem"/> points to an item within the view.</returns>
private bool MoveCurrentToPositionBase(int position)
{
// VerifyRefreshNotDeferred was removed
bool result = false;
// Instead of property InternalCount we use Count property
if (position < -1 || position > this.Count)
{
throw new ArgumentOutOfRangeException("position");
}
if (position != this.CurrentPosition || !this.IsCurrentInSync)
{
// Instead of property InternalCount we use Count property from this class
// Instead of InternalItemAt we use GetItemAt from this class
object proposedCurrentItem = (0 <= position && position < this.Count) ? this.GetItemAt(position) : null;
// ignore moves to the placeholder
if (proposedCurrentItem != CollectionView.NewItemPlaceholder)
{
if (this.OKToChangeCurrent())
{
bool oldIsCurrentAfterLast = this.IsCurrentAfterLast;
bool oldIsCurrentBeforeFirst = this.IsCurrentBeforeFirst;
this.SetCurrent(proposedCurrentItem, position);
this.OnCurrentChanged();
// notify that the properties have changed.
if (this.IsCurrentAfterLast != oldIsCurrentAfterLast)
{
this.OnPropertyChanged(PropertySupport.ExtractPropertyName(() => this.IsCurrentAfterLast));
}
if (this.IsCurrentBeforeFirst != oldIsCurrentBeforeFirst)
{
this.OnPropertyChanged(PropertySupport.ExtractPropertyName(() => this.IsCurrentBeforeFirst));
}
this.OnPropertyChanged(PropertySupport.ExtractPropertyName(() => this.CurrentPosition));
this.OnPropertyChanged(PropertySupport.ExtractPropertyName(() => this.CurrentItem));
result = true;
}
}
}
// Instead of IsCurrentInView we return result
return result;
}
private int GetDelta(int index)
{
int delta = 0;
ReadOnlyObservableCollection<object> groups = this.BaseGroups;
if (groups != null)
{
int totalCount = 0;
for (int i = 0; i < groups.Count; i++)
{
CollectionViewGroup group = groups[i] as CollectionViewGroup;
if (group != null)
{
if (index == totalCount)
{
break;
}
delta++;
int numInGroup = group.ItemCount;
totalCount += numInGroup + 1;
if (index < totalCount)
{
break;
}
}
}
}
return delta;
}
/// <summary>
/// Helper to raise a PropertyChanged event
/// </summary>
/// <param name="propertyName">Name of the property</param>
private void OnPropertyChanged(string propertyName)
{
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}