Groupe de virtualisation de WPF ListView

est-ce que quelqu'un connaît une implémentation ListView qui supporte la virtualisation de L'UI lorsque le regroupement est activé? Par défaut, le VirtualizingStackPanel est désactivé lorsque le groupement est configuré.

il semble que Microsoft ne va pas mettre en œuvre cela dans v4.Je suis donc à la recherche de solutions alternatives.

7
demandé sur XAMlMAX 2009-07-09 09:48:20

4 réponses

j'ai localisé un échantillon à Grouping and Virtualization MSDN Code Sample qui convertit la ListView groupée en une liste plate qui supporte la virtualisation. Cependant, je ne peux pas trouver comment imiter les actions croissantes des en-têtes.

6
répondu Luke 2014-11-19 18:30:03

wpf / .net 4.5 supporte maintenant ce https://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingpanel.isvirtualizingwhengrouping(v=vs. 110).aspx

si vous ciblez toujours 4.0, vous pouvez le régler avec réflexion de sorte qu'au moins certains utilisateurs peuvent obtenir l'avantage.

3
répondu Jack Woodward 2015-01-23 00:23:35

une option est de jeter un oeil à la série de Bea Stollniz sur l'amélioration de la performance D'un TreeView: Partie 1 , Partie 2 , et Partie 3 . Alors que ce qu'elle fait est plus adapté à TreeViews, qui n'ont pas de virtualisation car ils groupent par défaut, les leçons apprises pourraient certainement être appliquées à une ListView personnalisée qui a des groupes de virtualisation. En fait, dans la partie 3, elle utilise une liste de diffusion comme base pour créer arbre de virtualisation, qui est un bon début pour le regroupement virtualisé aussi bien. Il est évident que l'affichage des éléments comme dans une arborescence A quelques différences, comme la sélection des noeuds de groupe, à partir d'une ListView avec groupage, mais cela pourrait être corrigé en saisissant la selection changée.

0
répondu rmoore 2009-07-09 06:14:13

j'espère que ses pas trop hors sujet, mais j'ai eu récemment un problème similaire. Comme indiqué ci-dessus, il ne s'agit que de L'émission.net 4.0. Je conviens même que dans la plupart des cas avec combo box vous ne devriez pas normalement avoir besoin de virtualisation parce qu'il ne devrait pas avoir que beaucoup d'articles et s'il y a besoin de grouper alors une sorte de solution de maître-détail devrait être mis en œuvre. Mais il peut y avoir des zones grises.

le lien fourni par Luc sur le regroupement et la virtualisation sur MSDN m'a beaucoup aidé. Dans mon cas, c'était la seule approche que j'ai pu trouver ou trouver n'importe où qui est dans une direction dont j'ai besoin. Il ne supporte pas toutes les fonctionnalités de ListViewCollection. J'ai dû outrepasser quelques méthodes sinon la sélection des éléments ne fonctionnerait pas correctement. Il y a évidemment encore du travail à faire.

voici donc une solution mise à jour de Flatgroupistcollectionview à partir de ici :

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

partie XAML reste comme dans sample code. Voir le modèle reste comme il est, ce qui signifie utiliser Flatgroupplistcollectionview et configurer Groupdescriptionsdescriptions.

je préfère cette solution parce qu'elle sépare la logique de groupement de ma liste de données dans le modèle de vue. Une autre solution serait de mettre en œuvre le soutien de regroupement sur la liste originale des éléments dans le modèle de vue qui signifie en quelque sorte identifier les en-têtes. Pour une utilisation unique il devrait être très bien, mais la collection pourrait avoir besoin d'être recréé pour un but de groupement différent ou pas qui n'est pas si gentil.

0
répondu Tomas Kosar 2017-07-04 10:33:18