I hope its not too much off topic but I had recently a similar problem. As stated above it is only .NET 4.0 issue. I would even agree that in most cases with combo box you should not normally need virtualization because it should not have that many items and if there is need for grouping then some kind of master-detail solution should be implemented. But there might be some gray areas.
The link provided by Luke about Grouping and Virtualization on MSDN helped me a lot. In my case that was the only approach which I was able to come up or find anywhere that is in a direction i need. It does not support all functionality from ListViewCollection. I had to override few methods otherwise selection of items would not work correctly. There are obviously more work to do.
So here is an updated solution of FlatGroupListCollectionView from here :
///
/// 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
///
public class FlatGroupListCollectionView : ListCollectionView
{
///
/// Initializes a new instance of the class.
///
/// A list used in this collection
public FlatGroupListCollectionView(IList list)
: base(list)
{
}
///
/// 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
///
/// Index from which get an item
/// Item that was found on given index
public override object GetItemAt(int index)
{
int delta = 0;
ReadOnlyObservableCollection 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;
}
///
/// 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
///
public override int Count
{
get
{
int count = base.Count;
if (this.BaseGroups != null)
{
count += this.BaseGroups.Count;
}
return count;
}
}
///
/// By returning null, we trick the generator into thinking that we are not grouping
/// Thus, we avoid the default grouping code
///
public override ReadOnlyObservableCollection Groups
{
get
{
return null;
}
}
///
/// Gets the Groups collection from the base class
///
private ReadOnlyObservableCollection BaseGroups
{
get
{
return base.Groups;
}
}
///
/// 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
///
public static readonly DependencyProperty DetectGroupHeadersProperty =
DependencyProperty.RegisterAttached("DetectGroupHeaders", typeof(bool), typeof(FlatGroupListCollectionView), new FrameworkPropertyMetadata(false, OnDetectGroupHeaders));
///
/// Gets the Detect Group Headers property
///
/// Dependency Object from which the property is get
/// Value of Detect Group Headers property
public static bool GetDetectGroupHeaders(DependencyObject obj)
{
return (bool)obj.GetValue(DetectGroupHeadersProperty);
}
///
/// Sets the Detect Group Headers property
///
/// Dependency Object on which the property is set
/// Value to set to property
public static void SetDetectGroupHeaders(DependencyObject obj, bool value)
{
obj.SetValue(DetectGroupHeadersProperty, value);
}
///
/// IsHeader can be used to style the container differently when it is a header
/// For instance, it can be disabled to prevent selection
///
public static readonly DependencyProperty IsHeaderProperty =
DependencyProperty.RegisterAttached("IsHeader", typeof(bool), typeof(FlatGroupListCollectionView), new FrameworkPropertyMetadata(false));
///
/// Gets the Is Header property
///
/// Dependency Object from which the property is get
/// Value of Is Header property
public static bool GetIsHeader(DependencyObject obj)
{
return (bool)obj.GetValue(IsHeaderProperty);
}
///
/// Sets the Is Header property
///
/// Dependency Object on which the property is set
/// Value to set to property
public static void SetIsHeader(DependencyObject obj, bool value)
{
obj.SetValue(IsHeaderProperty, value);
}
///
/// Raises the System.Windows.Data.CollectionView.CollectionChanged event.
///
/// The System.Collections.Specialized.NotifyCollectionChangedEventArgs object to pass to the event handler
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;
}
}
///
/// 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
///
/// The item to set as the System.Windows.Data.CollectionView.CurrentItem
/// true if the resulting System.Windows.Data.CollectionView.CurrentItem is within the view; otherwise, false
public override bool MoveCurrentTo(object item)
{
int index = this.IndexOf(item);
int newIndex = this.ConvertFromItemToFlat(index, false);
return this.MoveCurrentToPositionBase(newIndex);
}
///
/// 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
///
/// The index to set the System.Windows.Data.CollectionView.CurrentItem to
/// true if the resulting System.Windows.Data.CollectionView.CurrentItem is an item within the view; otherwise, false
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 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;
}
///
/// Move to the item at the given index.
/// This is a replacement for base method
///
/// Move CurrentItem to this index
/// true if points to an item within the view.
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 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;
}
///
/// Helper to raise a PropertyChanged event
///
/// Name of the property
private void OnPropertyChanged(string propertyName)
{
base.OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
}
}
XAML part stays as it is in sample code. View model stays as it is as well which means using FlatGroupListCollectionView and set up GroupDescriptions.
I prefer this solution because it separates grouping logic from my list of data in view model. Other solution would be to implement support of grouping on original list of items in view model which means somehow identify headers. For a one time usage it should be fine but the collection might need to be recreated for a purpose of different or no grouping which is not so nice.