31

I have a ListBox which until recently was displaying a flat list of items. I was able to use myList.ItemContainerGenerator.ConainerFromItem(thing) to retrieve the ListBoxItem hosting "thing" in the list.

This week I've modified the ListBox slightly in that the CollectionViewSource that it binds to for its items has grouping enabled. Now the items within the ListBox are grouped underneath nice headers.

However, since doing this, ItemContainerGenerator.ContainerFromItem has stopped working - it returns null even for items I know are in the ListBox. Heck - ContainerFromIndex(0) is returning null even when the ListBox is populated with many items!

How do I retrieve a ListBoxItem from a ListBox that's displaying grouped items?

Edit: Here's the XAML and code-behind for a trimmed-down example. This raises a NullReferenceException because ContainerFromIndex(1) is returning null even though there are four items in the list.

XAML:

<Window x:Class="WpfApplication1.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:scm="clr-namespace:System.ComponentModel;assembly=WindowsBase"
    Title="Window1">

    <Window.Resources>
        <XmlDataProvider x:Key="myTasks" XPath="Tasks/Task">
            <x:XData>
                <Tasks xmlns="">
                    <Task Name="Groceries" Type="Home"/>
                    <Task Name="Cleaning" Type="Home"/>
                    <Task Name="Coding" Type="Work"/>
                    <Task Name="Meetings" Type="Work"/>
                </Tasks>
            </x:XData>
        </XmlDataProvider>

        <CollectionViewSource x:Key="mySortedTasks" Source="{StaticResource myTasks}">
            <CollectionViewSource.SortDescriptions>
                <scm:SortDescription PropertyName="@Type" />
                <scm:SortDescription PropertyName="@Name" />
            </CollectionViewSource.SortDescriptions>

            <CollectionViewSource.GroupDescriptions>
                <PropertyGroupDescription PropertyName="@Type" />
            </CollectionViewSource.GroupDescriptions>
        </CollectionViewSource>
    </Window.Resources>

    <ListBox 
        x:Name="listBox1" 
        ItemsSource="{Binding Source={StaticResource mySortedTasks}}" 
        DisplayMemberPath="@Name"
        >
        <ListBox.GroupStyle>
            <GroupStyle>
                <GroupStyle.HeaderTemplate>
                    <DataTemplate>
                        <TextBlock Text="{Binding Name}"/>
                    </DataTemplate>
                </GroupStyle.HeaderTemplate>
            </GroupStyle>
        </ListBox.GroupStyle>
    </ListBox>
</Window>

CS:

public Window1()
{
    InitializeComponent();
    listBox1.ItemContainerGenerator.StatusChanged += ItemContainerGenerator_StatusChanged;
}

void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
{
    if (listBox1.ItemContainerGenerator.Status == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
    {
        listBox1.ItemContainerGenerator.StatusChanged -= ItemContainerGenerator_StatusChanged;

        var i = listBox1.ItemContainerGenerator.ContainerFromIndex(1) as ListBoxItem;

        // select and keyboard-focus the second item
        i.IsSelected = true;
        i.Focus();
    }
}
Matt Hamilton
  • 200,371
  • 61
  • 386
  • 320
  • What are you doing with the container? Can you possible exapnd on what the previous code did that actually worked? There are a few ways to get the container... it just depends on what you want to do with it? – rudigrobler Oct 03 '08 at 06:10

3 Answers3

41

You have to listen and react to the ItemsGenerator.StatusChanged Event and wait until the ItemContainers are generated before you can access them with ContainerFromElement.


Searching further, I've found a thread in the MSDN forum from someone who has the same problem. This seems to be a bug in WPF, when one has a GroupStyle set. The solution is to punt the access of the ItemGenerator after the rendering process. Below is the code for your question. I tried this, and it works:

    void ItemContainerGenerator_StatusChanged(object sender, EventArgs e)
    {
        if (listBox1.ItemContainerGenerator.Status
            == System.Windows.Controls.Primitives.GeneratorStatus.ContainersGenerated)
        {
            listBox1.ItemContainerGenerator.StatusChanged
                -= ItemContainerGenerator_StatusChanged;
            Dispatcher.BeginInvoke(System.Windows.Threading.DispatcherPriority.Input,
                new Action(DelayedAction));
        }
    }

    void DelayedAction()
    {
        var i = listBox1.ItemContainerGenerator.ContainerFromIndex(1) as ListBoxItem;

        // select and keyboard-focus the second item
        i.IsSelected = true;
        i.Focus();
    }
Tatiana Racheva
  • 1,289
  • 1
  • 13
  • 31
David Schmitt
  • 58,259
  • 26
  • 121
  • 165
  • Yeah, I'm doing that. I'll update the question early next week with some code. – Matt Hamilton Oct 03 '08 at 23:17
  • Just tried that out myself. Helps me out on something I had trouble with this weekend. – Joel B Fant Oct 06 '08 at 17:40
  • Awesome - thanks David. I've not had a chance to test this yet but I'll accept this as "the" answer for now. – Matt Hamilton Oct 08 '08 at 08:37
  • You should be using System.Action instead of VoidDelegate. – justin.m.chase May 17 '10 at 20:00
  • @justin: I've removed the VoidDelegate – David Schmitt May 18 '11 at 09:44
  • 1
    I don't believe this problem is limited to the case where there is a GroupStyle set. See also: http://stackoverflow.com/questions/7363777 – Cheeso Sep 09 '11 at 17:06
  • @Cheeso: that may be the case. I only explored this specific use case. – David Schmitt Sep 12 '11 at 06:46
  • 1
    +1 Thanks for the answer David, it helped solve a similar problem. One change that I would like to point out was to first check if the `ItemContainerGenerator` is ready and if so dispatch the action instead of waiting for the status to change. This solves a bug where it wouldn't work the first time. http://stackoverflow.com/questions/7366961/listbox-scrollintoview-when-using-collectionviewsource-with-groupdescriptions-i/7414200#7414200 – Dennis Sep 14 '11 at 09:42
  • Note for others, in my case I'm wanting to reference the ListBoxItem after each item is added but the status change event could be firing for a previous added item with status set to ContainersGenerated. So instead I'm checking to see if ContainerFromIndex returns null or not before proceeding. Thanks for your help David. – Michael Dec 10 '11 at 22:27
  • An update on this one (you might like to include this comment in your answer, David). I had another grouped ListBox that was experiencing this problem, and I only got it to work after removing the "unsubscribe from event" line in the code above. The event was getting fired multiple times even after the containers were first generated. Hope this helps someone else out there. – Matt Hamilton Jan 15 '14 at 04:43
2

If the above code doesn't work for you, give this a try

public class ListBoxExtenders : DependencyObject
{
    public static readonly DependencyProperty AutoScrollToCurrentItemProperty = DependencyProperty.RegisterAttached("AutoScrollToCurrentItem", typeof(bool), typeof(ListBoxExtenders), new UIPropertyMetadata(default(bool), OnAutoScrollToCurrentItemChanged));

    public static bool GetAutoScrollToCurrentItem(DependencyObject obj)
    {
        return (bool)obj.GetValue(AutoScrollToSelectedItemProperty);
    }

    public static void SetAutoScrollToCurrentItem(DependencyObject obj, bool value)
    {
        obj.SetValue(AutoScrollToSelectedItemProperty, value);
    }

    public static void OnAutoScrollToCurrentItemChanged(DependencyObject s, DependencyPropertyChangedEventArgs e)
    {
        var listBox = s as ListBox;
        if (listBox != null)
        {
            var listBoxItems = listBox.Items;
            if (listBoxItems != null)
            {
                var newValue = (bool)e.NewValue;

                var autoScrollToCurrentItemWorker = new EventHandler((s1, e2) => OnAutoScrollToCurrentItem(listBox, listBox.Items.CurrentPosition));

                if (newValue)
                    listBoxItems.CurrentChanged += autoScrollToCurrentItemWorker;
                else
                    listBoxItems.CurrentChanged -= autoScrollToCurrentItemWorker;
            }
        }
    }

    public static void OnAutoScrollToCurrentItem(ListBox listBox, int index)
    {
        if (listBox != null && listBox.Items != null && listBox.Items.Count > index && index >= 0)
            listBox.ScrollIntoView(listBox.Items[index]);
    }

}

Usage in XAML

<ListBox IsSynchronizedWithCurrentItem="True" extenders:ListBoxExtenders.AutoScrollToCurrentItem="True" ..../>
D.Kempkes
  • 345
  • 3
  • 5
0

Try parsing the VisualTree up from the 'thing' until you reach a ListBoxItem type

Jobi Joy
  • 49,102
  • 20
  • 108
  • 119
  • Jobi - "thing" in this case isn't a visual element - it's an instance of a business object. Part of an IList that the ListBox's ItemsSource is bound to. Can I do what you're describing under that scenario? – Matt Hamilton Oct 03 '08 at 02:27
  • Yeah got you. thanks for the clarification. From which part of the code behind you are trying to do this? any DataTemplate click or in some other event handler? – Jobi Joy Oct 03 '08 at 02:32
  • When the page (it's a Navigation-style application) loads, I want to set keyboard focus to a specific element. So in the Loaded event handler. – Matt Hamilton Oct 03 '08 at 02:36
  • Actually that's not quite true. I'm doing it in the ItemContainerGenerator.StatusChanged event handler on the ListBox, which I hooked up in the Loaded event. – Matt Hamilton Oct 03 '08 at 02:42