Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does ItemContainerGenerator.ContainerFromItem work with a grouped list?

Tags:

.net

wpf

listbox

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();     } } 
like image 640
Matt Hamilton Avatar asked Oct 03 '08 02:10

Matt Hamilton


2 Answers

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();     } 
like image 130
David Schmitt Avatar answered Oct 11 '22 10:10

David Schmitt


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" ..../> 
like image 21
D.Kempkes Avatar answered Oct 11 '22 10:10

D.Kempkes