2009-07-09 101 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。

我更喜歡這個解決方案,因爲它將視圖模型中的數據列表中的分組邏輯與我的列表分開。其他解決方案是實現對視圖模型中原始項目列表的分組支持,這意味着以某種方式識別標題。對於一次性使用應該沒問題,但可能需要重新創建集合以用於不同或不分組的目的,這是不太好的。