WPF ListView虚拟化分组

时间:2009-07-09 05:48:20

标签: .net wpf performance listview virtualization

有没有人知道在启用分组时支持UI虚拟化的ListView实现?默认情况下,在设置分组时禁用VirtualizingStackPanel。

似乎微软不打算在.NET Framework的v4.0中实现这一点,所以我正在寻找替代解决方案。

4 个答案:

答案 0 :(得分:6)

我在Grouping and Virtualization MSDN Code Sample找到了一个样本,它将分组的ListView转换为支持虚拟化的平面列表。但是我无法弄清楚如何模仿标题的扩展动作。

答案 1 :(得分:3)

wpf / .net 4.5现在支持此https://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingpanel.isvirtualizingwhengrouping(v=vs.110).aspx

如果您仍然定位4.0,则可以使用反射设置它,这样至少有些用户可以获得好处。

答案 2 :(得分:0)

一个选项是看看Bea Stollniz的系列文章,以改善TreeView的性能: Part 1Part 2Part 3。虽然她所做的更适合TreeViews,它没有任何虚拟化,因为它们默认分组,所学到的经验教训肯定可以应用于具有虚拟化组的自定义ListView。实际上,在第3部分中,她使用ListBox作为创建虚拟化树的基础,这也是虚拟化分组的良好开端。显然,在TreeView中显示项目有一些差异,例如从具有分组的ListView中选择组节点,但可以通过捕获SelectionChanged来修复。

答案 3 :(得分:0)

我希望它的主题不是太多,但最近我遇到了类似的问题。如上所述,它只是.NET 4.0问题。我甚至同意在大多数情况下使用组合框你通常不需要虚拟化,因为它不应该有那么多项,如果需要分组,那么应该实现某种主 - 细节解决方案。但可能会有一些灰色地带。

Luke提供的有关MSDN上的分组和虚拟化的链接对我帮助很大。在我的情况下,这是我能够出现或找到我需要的任何方向的唯一方法。它不支持ListViewCollection的所有功能。我必须覆盖几个方法,否则选择的项目将无法正常工作。显然还有更多的工作要做。

所以这是来自here的FlatGroupListCollectionView的更新解决方案:

/// <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));
    }
}

XAML部分保留在示例代码中。视图模型保持原样,这意味着使用FlatGroupListCollectionView并设置GroupDescriptions。

我更喜欢这种解决方案,因为它将分组逻辑与视图模型中的数据列表分开。其他解决方案是在视图模型中实现对原始项目列表的分组支持,这意味着以某种方式识别标题。对于一次性使用它应该没问题但是可能需要重新创建集合以用于不同的分组或不分组,这不是很好。