2009-07-09 8 views
7

Kennt jemand eine ListView-Implementierung, die UI-Virtualisierung unterstützt, wenn die Gruppierung aktiviert ist? Standardmäßig ist das VirtualizingStackPanel beim Gruppieren deaktiviert.WPF ListView Virtualization Grouping

Es scheint, dass Microsoft dies nicht innerhalb von v4.0 von .NET Framework implementieren wird, also suche ich nach alternativen Lösungen.

Antwort

0

Eine Option ist ein TreeView Leistung einen Blick ein Bea Stollniz Serie nehmen auf die Verbesserung: Part 1, Part 2 und Part 3. Während das, was sie tut, mehr auf TreeViews ausgerichtet ist, die keine Virtualisierung haben, weil sie sich standardmäßig gruppieren, könnten die gelernten Lektionen definitiv auf ein benutzerdefiniertes ListView mit Virtualisierungsgruppen angewendet werden. Tatsächlich verwendet sie in Teil 3 eine ListBox als Basis, um den Virtualisierungsbaum zu erstellen, was auch ein guter Start für die virtualisierte Gruppierung ist. Offensichtlich hat die Anzeige von Elementen in einer Baumansicht einige Unterschiede, wie zum Beispiel die Auswahl der Gruppenknoten, aus einer ListView mit Gruppierung, aber das könnte durch Abfangen von SelectionChanged behoben werden.

+0

Danke! Ich habe mir den Beispielcode in Teil 3 angeschaut. Die Hauptschwierigkeit, die ich habe, ist, wie ich den Gruppierungseintrag mithilfe der GroupDescriptions hinzufügen würde. – Luke

5

Ich habe ein Beispiel bei Grouping and Virtualization MSDN Code Sample gefunden, das das gruppierte ListView in eine flache Liste konvertiert, die Virtualisierung unterstützt. Ich kann jedoch nicht herausfinden, wie man die expandierenden Aktionen der Header imitiert.

+0

Hatten Sie Glück, die Sichtbarkeit der gruppierten Elemente zu ändern? Haben sie sich so verhalten, als wären sie Expander mit den gruppierten Gegenständen als Inhalt? – bigfoot

+0

Tut mir leid, ich habe keinen Fortschritt auf diesem – Luke

+0

gemacht Der Link zur Beispielcode funktioniert nicht mehr, wie die Code-Bibliothek wurde hier verschoben ist der Link zu den verschoben [Gruppierung und Virtualisierung MSDN-Code-Beispiel] (https: // code.msdn.microsoft.com/windowsdesktop/Grouping-and-Virtualization-56e7d3fe) – XAMlMAX

0

Ich hoffe es ist nicht zu viel vom Thema, aber ich hatte in letzter Zeit ein ähnliches Problem. Wie oben erwähnt, ist es nur .NET 4.0 Problem. Ich würde sogar zustimmen, dass in den meisten Fällen mit der Combo-Box normalerweise keine Virtualisierung benötigt wird, da es nicht so viele Elemente haben sollte und wenn eine Gruppierung erforderlich ist, sollte eine Art Master-Detail-Lösung implementiert werden. Aber es könnte einige Grauzonen geben.

Der Link von Luke über Grouping und Virtualisierung auf MSDN hat mir sehr geholfen. In meinem Fall war das die einzige Herangehensweise, die ich irgendwo finden konnte oder fand, die in eine Richtung führt, die ich brauche. Es unterstützt nicht alle Funktionen von ListViewCollection. Ich musste einige Methoden außer Kraft setzen, sonst würde die Auswahl der Artikel nicht korrekt funktionieren. Es gibt offensichtlich mehr Arbeit zu tun.

Also hier ist eine aktualisierte Lösung von FlatGroupListCollectionView von here:

/// <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 Teil bleibt, wie es in Beispielcode ist. Das Modell bleibt auch so wie es ist, was bedeutet, FlatGroupListCollectionView zu verwenden und GroupDescriptions einzurichten.

Ich bevorzuge diese Lösung, weil sie Gruppierungslogik von meiner Liste der Daten im View-Modell trennt. Eine andere Lösung wäre, die Unterstützung der Gruppierung auf der ursprünglichen Liste von Elementen im Ansichtsmodell zu implementieren, was bedeutet, dass Kopfzeilen identifiziert werden. Für eine einmalige Verwendung sollte es in Ordnung sein, aber die Sammlung muss möglicherweise für einen Zweck unterschiedlicher oder keiner Gruppierung neu erstellt werden, was nicht so nett ist.