11

Short version

I would like to scroll the ListBox item into view when the selection is changed.

Long version

I have a ListBox with the ItemsSource bound to a CollectionViewSource with a GroupDescription, as per the example below.

<Window.Resources>
    <CollectionViewSource x:Key="AnimalsView" Source="{Binding Source={StaticResource Animals}, Path=AnimalList}">
        <CollectionViewSource.GroupDescriptions>
            <PropertyGroupDescription PropertyName="Category"/>
        </CollectionViewSource.GroupDescriptions>
    </CollectionViewSource>  
</Window.Resources>

<ListBox x:Name="AnimalsListBox"ItemsSource="{Binding Source={StaticResource AnimalsView}}" ItemTemplate="{StaticResource AnimalTemplate}" SelectionChanged="ListBox_SelectionChanged">
    <ListBox.GroupStyle>
        <GroupStyle HeaderTemplate="{StaticResource CategoryTemplate}" />
    </ListBox.GroupStyle>
</ListBox>

There is a SelectionChanged event in the a code-behind file.

public List<Animal> Animals { get; set; }

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ListBox control = (ListBox)sender;
    control.ScrollIntoView(control.SelectedItem);
}

Now. If I set the AnimalsListBox.SelectedItem to an item that is currently not visible I would like to have it scroll in view. This is where it gets tricky, as the ListBox is being groups (the IsGrouped property is true) the call to ScrollIntoView fails.

System.Windows.Controls.ListBox via Reflector. Note the base.IsGrouping in the OnBringItemIntoView.

public void ScrollIntoView(object item)
{
    if (base.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
    {
        this.OnBringItemIntoView(item);
    }
    else
    {
        base.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new DispatcherOperationCallback(this.OnBringItemIntoView), item);
    }
}

private object OnBringItemIntoView(object arg)
{
    FrameworkElement element = base.ItemContainerGenerator.ContainerFromItem(arg) as FrameworkElement;
    if (element != null)
    {
        element.BringIntoView();
    }
    else if (!base.IsGrouping && base.Items.Contains(arg))
    {
        VirtualizingPanel itemsHost = base.ItemsHost as VirtualizingPanel;
        if (itemsHost != null)
        {
            itemsHost.BringIndexIntoView(base.Items.IndexOf(arg));
        }
    }
    return null;
}

Questions

  1. Can anyone explain why it does not work when using grouping?
    • The ItemContainerGenerator.ContainerFromItem always returns null, even though it's status states that all the containers have been generated.
  2. How I can achieve the scrolling into view when using grouping?
Dennis
  • 20,275
  • 4
  • 64
  • 80

2 Answers2

10

I have found a solution to my problem. I was certain that I wasn't the first person to hit this issue so I continued to search StackOverflow for solutions and I stumbled upon this answer by David about how ItemContainerGenerator works with a grouped list.

David's solution was to delay accessing the ItemContainerGenerator until after the rendering process.

I have implemented this solution, with a few changes that I will detail after.

private void ListBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
    ListBox control = (ListBox)sender;

    if (control.IsGrouping)
    {
         if (control.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated)
              Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
         else
              control.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
    }
    else
        control.ScrollIntoView(control.SelectedItem);
}

private void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
    if (ItemContainerGenerator.Status != GeneratorStatus.ContainersGenerated)
        return;

    ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;
    Dispatcher.BeginInvoke(DispatcherPriority.Render, new Action(DelayedBringIntoView));
}

private void DelayedBringIntoView()
{
    var item = ItemContainerGenerator.ContainerFromItem(SelectedItem) as ListBoxItem;
    if (item != null)
        item.BringIntoView();
}

Changes:

  • Only uses the ItemContainerGenerator approach when it IsGrouping is true, otherwise continue to use the default ScrollIntoView.
  • Check if the ItemContainerGenerator is ready, if so dispatch the action, otherwise listen for the ItemContainerGenerator status to change.. This is important as if it is ready then the StatusChanged event will never fire.
Community
  • 1
  • 1
Dennis
  • 20,275
  • 4
  • 64
  • 80
  • You should change your answer to the the correct one, not the one above. – Valentein Apr 13 '12 at 16:35
  • @Valentein: I have change the marked answer. HOWEVER as [crazyarabian](http://stackoverflow.com/a/7375646/73025) advice did help me in diagnosing the issue it would good to up-vote **both** answers if you used the my eventual solution. – Dennis Apr 13 '12 at 17:04
  • Using .NET 4.5.1 and MVVM you can use a behavior to do this. The behavior works in both scenarios as it fires late already. – Kelly Sep 19 '14 at 01:50
3
  1. The out of the box VirtualizingStackPanel does not support virtualizing grouped collection views. When a grouped collection is rendered in an ItemsControl, each group as a whole is an item as opposed to each item in the collection which results in "jerky" scrolling to each group header and not each item.

  2. You'll probably need to roll your own VirtualizingStackPanel or ItemContainerGenerator in order to keep track of the containers displayed in a group. It sounds ridiculous, but the default virtualization with grouping in WPF is lacking to say the least.

sellmeadog
  • 7,437
  • 1
  • 31
  • 45
  • That is what I thought, however I was hoping that I wouldn't have to as writing virtualizing panels can be tricky. ... As it each group as a whole is the item, is that why the `ItemContainerGenerator` always return `null` when passed the `SelectedItem`? – Dennis Sep 11 '11 at 10:23
  • 1
    I believe so yes. You should check out Bea Stollnitz blog. She's got alot of good posts on grouping and virtualization: http://www.beacosta.com/ – sellmeadog Sep 11 '11 at 19:40
  • Thanks. I've read several of Bea Stollnitz posts on WPF and CollectionViewSource and Grouping - I actually used her grouping examples in my question. – Dennis Sep 11 '11 at 20:04
  • Update: I have found an solution and have posted it as an answer. – Dennis Sep 14 '11 at 10:32