2009-07-09 106 views
7

有谁知道启用分组时支持UI虚拟化的ListView实现吗?默认情况下,当设置分组时,VirtualizingStackPanel被禁用。WPF ListView虚拟化分组

看来微软不会在.NET框架的4.0版本中实现这个功能,所以我正在寻找替代解决方案。

回答

0

一种选择是去看一看东亚银行Stollniz的一系列改进TreeView的表现: Part 1Part 2Part 3。虽然她所做的更适合于TreeView,因为它们默认进行了分组,所以它们没有任何虚拟化,所获得的教训肯定可以应用于具有虚拟化组的自定义ListView。实际上,在第3部分中,她使用ListBox作为其基础来创建虚拟化树,这也是虚拟化分组的良好开端。很显然,在TreeView中显示项目有一些不同点,比如选择组节点,但是可以通过捕获SelectionChanged来修复。

+0

谢谢!我查看了第3部分中的示例代码。我遇到的主要困难是如何使用GroupDescriptions添加分组项目 – Luke 2009-07-09 07:16:59

5

我找到了一个示例Grouping and Virtualization MSDN Code Sample,它将组合的ListView转换为支持虚拟化的平面列表。但是我无法解决如何模仿头文件的扩展动作。

+0

您是否有幸找到如何切换分组项目的可见性?让他们表现得像是将分组项目作为内容的扩展器? – bigfoot 2011-11-09 22:23:14

0

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

卢克关于MSDN上的分组和虚拟化提供的链接帮助我了很多。就我而言,这是我能够在任何需要的方向上找到或找到的唯一方法。它不支持来自ListViewCollection的所有功能。我不得不重写一些方法,否则选择项目将无法正常工作。显然还有更多的工作要做。

因此,这里是FlatGroupListCollectionView从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一部分留,因为它在示例代码。视图模型保持原样,这意味着使用FlatGroupListCollectionView并设置GroupDescriptions。

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