42

I'm having a bit of weird behavior that I can't seem to work out. When I iterate through the items in my ListBox.ItemsSource property, I can't seem to get the container? I'm expecting to see a ListBoxItem returned, but I only get null.

Any ideas?

Here's the bit of code I'm using:

this.lstResults.ItemsSource.ForEach(t =>
    {
        ListBoxItem lbi = this.lstResults.ItemContainerGenerator.ContainerFromItem(t) as ListBoxItem;

        if (lbi != null)
        {
            this.AddToolTip(lbi);
        }
    });

The ItemsSource is currently set to a Dictionary and does contain a number of KVPs.

H.B.
  • 136,471
  • 27
  • 285
  • 357
Sonny Boy
  • 7,244
  • 16
  • 67
  • 101
  • Can't you just iterate through Items, which would be a readonly collection (but its contents would not be readonly)? – Ed Bayiates Jul 15 '11 at 21:54
  • I tried that, too. Using .ContainerFromIndex() also returns null. – Sonny Boy Jul 15 '11 at 22:25
  • [Check the following link to get the answer][1] [1]: http://stackoverflow.com/questions/10591391/why-itemcontainergenerator-containerfromindex-returns-null-and-how-to-avoid-th/27792628#27792628 – itzmebibin Jan 06 '15 at 06:50
  • The following link contains its answer. [Click here][1] [1]: http://stackoverflow.com/questions/10591391/why-itemcontainergenerator-containerfromindex-returns-null-and-how-to-avoid-th/27792628#27792628 – itzmebibin Jan 06 '15 at 06:53

10 Answers10

54

I found something that worked better for my case in this StackOverflow question:

Get row in datagrid

By putting in UpdateLayout and a ScrollIntoView calls before calling ContainerFromItem or ContainerFromIndex, you cause that part of the DataGrid to be realized which makes it possible for it return a value for ContainerFromItem/ContainerFromIndex:

dataGrid.UpdateLayout();
dataGrid.ScrollIntoView(dataGrid.Items[index]);
var row = (DataGridRow)dataGrid.ItemContainerGenerator.ContainerFromIndex(index);

If you don't want the current location in the DataGrid to change, this probably isn't a good solution for you but if that's OK, it works without having to turn off virtualizing.

Community
  • 1
  • 1
Phred Menyhert
  • 2,250
  • 4
  • 18
  • 19
  • 1
    Thanks, this solved my problem. I was trying to set focus to an item that was not available yet. UpdateLayout() worked great. – Tejo Feb 14 '13 at 13:12
  • 1
    I am also doing same but after 109 items it return null :( – Yawar Oct 20 '15 at 11:43
19

Finally sorted out the problem... By adding VirtualizingStackPanel.IsVirtualizing="False" into my XAML, everything now works as expected.

On the downside, I miss out on all the performance benefitst of the virtualization, so I changed my load routing to async and added a "spinner" into my listbox while it loads...

Sonny Boy
  • 7,244
  • 16
  • 67
  • 101
  • PS - Thanks for the hint H.B. Your comment on the virtualization is what led me down that path. – Sonny Boy Jul 18 '11 at 16:43
  • 1
    As you said, it is not advised to switch off virtualization due too performance degradation. I once had few thousands of long rows which after switching virtualization off consumed >1Gb memory. Scary. – Jānis Gruzis Oct 16 '13 at 07:43
  • This solved the problem of nulls for me, but like the previous commenter it brought a previously instantaneously loading DataGrid of 2000 rows to a grinding halt. – Charles Clayton Jan 19 '15 at 18:12
15
object viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
if (viewItem == null)
{
    list.UpdateLayout();
    viewItem = list.ItemContainerGenerator.ContainerFromItem(item);
    Debug.Assert(viewItem != null, "list.ItemContainerGenerator.ContainerFromItem(item) is null, even after UpdateLayout");
}
Misa Lazovic
  • 2,765
  • 9
  • 30
  • 38
epox
  • 4,537
  • 36
  • 26
  • 2
    This worked perfectly to my case! You saved my day. ;) In my case, this issue happened right after CustomSort is used, and Misa's solution solved the issue. – Aki24x Feb 09 '18 at 23:47
  • Can you explain this answer, instead of just pasting code? What is list? – Stealth Rabbi Jan 09 '19 at 15:11
  • @Stealth Rabbi: a ListBox or ListView. What do you actually want an item container for otherwise? – Gábor Jan 24 '19 at 15:09
  • 5
    @Gabor Again, this answer does not explain anything, and is just pasted code. It also just starts using variables named `item` and `list`, without identifying what they are. – Stealth Rabbi Jan 25 '19 at 18:00
  • 2
    I don't see any problems with that. The question asks about listboxes and their items. So, in this context, both variables should be self-explanatory. – Gábor Jan 27 '19 at 09:17
7

Step through the code with the debugger and see if there is actually nothing retured or if the as-cast is just wrong and thus turns it to null (you could just use a normal cast to get a proper exception).

One problem that frequently occurs is that when an ItemsControl is virtualizing for most of the items no container will exist at any point in time.

Also i would not recommend dealing with the item containers directly but rather binding properties and subscribing to events (via the ItemsControl.ItemContainerStyle).

H.B.
  • 136,471
  • 27
  • 285
  • 357
5

Use this subscription:

TheListBox.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
  TheListBox.Dispatcher.Invoke(() =>
  {
     var TheOne = TheListBox.ItemContainerGenerator.ContainerFromIndex(0);
     if (TheOne != null)
       // Use The One
  });
};
Amir
  • 720
  • 8
  • 21
4

I'm a bit late for the party but here's another solution that's fail-proof in my case,

After trying many solutions suggesting to add IsExpanded and IsSelected to underlying object and binding to them in TreeViewItem style, while this mostly works in some case it still fails ...

Note: my objective was to write a mini/custom Explorer-like view where when I click a folder in the right pane it gets selected on the TreeView, just like in Explorer.

private void ListViewItem_MouseDoubleClick(object sender, MouseButtonEventArgs e)
{
    var item = sender as ListViewItem;
    var node = item?.Content as DirectoryNode;
    if (node == null) return;

    var nodes = (IEnumerable<DirectoryNode>)TreeView.ItemsSource;
    if (nodes == null) return;

    var queue = new Stack<Node>();
    queue.Push(node);
    var parent = node.Parent;
    while (parent != null)
    {
        queue.Push(parent);
        parent = parent.Parent;
    }

    var generator = TreeView.ItemContainerGenerator;
    while (queue.Count > 0)
    {
        var dequeue = queue.Pop();
        TreeView.UpdateLayout();
        var treeViewItem = (TreeViewItem)generator.ContainerFromItem(dequeue);
        if (queue.Count > 0) treeViewItem.IsExpanded = true;
        else treeViewItem.IsSelected = true;
        generator = treeViewItem.ItemContainerGenerator;
    }
}

Multiple tricks used in here:

  • a stack for expanding every item from top to bottom
  • ensure to use current level generator to find the item (really important)
  • the fact that generator for top-level items never return null

So far it works very well,

  • no need to pollute your types with new properties
  • no need to disable virtualization at all.
aybe
  • 12,940
  • 7
  • 49
  • 93
  • Works great. Might be better to migrate this to a question (perhaps a new question) specifically about `TreeView`s, since this one is about ListBox, to give this awesome answer a bit more visibility. – Lauraducky Dec 05 '17 at 04:11
  • 3
    This works well... sometimes. For some reason I can't explain, treeViewItem can be null even though when debugging the generator and looking at the items I can see the node it is trying to select. It walks the stack but the ContainerFromItem() sometimes returns null, and I have no idea why. Sometimes it works though, even when the container is not visible. – Mgamerz Sep 18 '18 at 02:03
3

Although disabling virtualization from XAML works, I think it's better to disable it from the .cs file which uses ContainerFromItem

 VirtualizingStackPanel.SetIsVirtualizing(listBox, false);

That way, you reduce the coupling between the XAML and the code; so you avoid the risk of someone breaking the code by touching the XAML.

Benoit Blanchon
  • 10,737
  • 3
  • 56
  • 68
3

Most probably this is a virtualization-related issue so ListBoxItem containers get generated only for currently visible items (see https://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel(v=vs.110).aspx#Anchor_9)

If you are using ListBox I'd suggest switching to ListView instead - it inherits from ListBoxand it supports ScrollIntoView() method which you can utilize to control virtualization;

targetListView.ScrollIntoView(itemVM);
DoEvents();
ListViewItem itemContainer = targetListView.ItemContainerGenerator.ContainerFromItem(itemVM) as ListViewItem;

(the example above also utilizes the DoEvents() static method explained in more detail here; WPF how to wait for binding update to occur before processing more code?)

There are a few other minor differences between the ListBox and ListView controls (What is The difference between ListBox and ListView) - which should not essentially affect your use case.

Community
  • 1
  • 1
fvojvodic
  • 146
  • 3
  • DoEvents() as well as ScrollIntoView do not help. I tested in .NET 4.7.2 on ListView. Control just "white", but when you scroll items, they are appeared and STILL some of 'em has no generated container yet. How I'm tired of stupid MS "optimizations"!... – Vincent Jul 17 '18 at 15:04
2

VirtualizingStackPanel.IsVirtualizing="False" Makes the control fuzzy . See the below implementation. Which helps me to avoid the same issue. Set your application VirtualizingStackPanel.IsVirtualizing="True" always.

See the link for detailed info

/// <summary>
/// Recursively search for an item in this subtree.
/// </summary>
/// <param name="container">
/// The parent ItemsControl. This can be a TreeView or a TreeViewItem.
/// </param>
/// <param name="item">
/// The item to search for.
/// </param>
/// <returns>
/// The TreeViewItem that contains the specified item.
/// </returns>
private TreeViewItem GetTreeViewItem(ItemsControl container, object item)
{
    if (container != null)
    {
        if (container.DataContext == item)
        {
            return container as TreeViewItem;
        }

        // Expand the current container
        if (container is TreeViewItem && !((TreeViewItem)container).IsExpanded)
        {
            container.SetValue(TreeViewItem.IsExpandedProperty, true);
        }

        // Try to generate the ItemsPresenter and the ItemsPanel.
        // by calling ApplyTemplate.  Note that in the 
        // virtualizing case even if the item is marked 
        // expanded we still need to do this step in order to 
        // regenerate the visuals because they may have been virtualized away.

        container.ApplyTemplate();
        ItemsPresenter itemsPresenter = 
            (ItemsPresenter)container.Template.FindName("ItemsHost", container);
        if (itemsPresenter != null)
        {
            itemsPresenter.ApplyTemplate();
        }
        else
        {
            // The Tree template has not named the ItemsPresenter, 
            // so walk the descendents and find the child.
            itemsPresenter = FindVisualChild<ItemsPresenter>(container);
            if (itemsPresenter == null)
            {
                container.UpdateLayout();

                itemsPresenter = FindVisualChild<ItemsPresenter>(container);
            }
        }

        Panel itemsHostPanel = (Panel)VisualTreeHelper.GetChild(itemsPresenter, 0);


        // Ensure that the generator for this panel has been created.
        UIElementCollection children = itemsHostPanel.Children; 

        MyVirtualizingStackPanel virtualizingPanel = 
            itemsHostPanel as MyVirtualizingStackPanel;

        for (int i = 0, count = container.Items.Count; i < count; i++)
        {
            TreeViewItem subContainer;
            if (virtualizingPanel != null)
            {
                // Bring the item into view so 
                // that the container will be generated.
                virtualizingPanel.BringIntoView(i);

                subContainer = 
                    (TreeViewItem)container.ItemContainerGenerator.
                    ContainerFromIndex(i);
            }
            else
            {
                subContainer = 
                    (TreeViewItem)container.ItemContainerGenerator.
                    ContainerFromIndex(i);

                // Bring the item into view to maintain the 
                // same behavior as with a virtualizing panel.
                subContainer.BringIntoView();
            }

            if (subContainer != null)
            {
                // Search the next level for the object.
                TreeViewItem resultContainer = GetTreeViewItem(subContainer, item);
                if (resultContainer != null)
                {
                    return resultContainer;
                }
                else
                {
                    // The object is not under this TreeViewItem
                    // so collapse it.
                    subContainer.IsExpanded = false;
                }
            }
        }
    }

    return null;
}
1

For anyone still having issues with this, I was able to work around this issue by ignoring the first selection changed event and using a thread to basically repeat the call. Here's what I ended up doing:

private int _hackyfix = 0;
    private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        //HACKYFIX:Hacky workaround for an api issue
        //Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason.  Basically we ignore the
        //first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
        //with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need I ignore the event twice just in case but I think you can get away with ignoring only the first one.
        if (_hackyfix == 0 || _hackyfix == 1)
        {
            _hackyfix++;
            Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            OnMediaSelectionChanged(sender, e);
        });
        }
        //END OF HACKY FIX//Actual code you need to run goes here}

EDIT 10/29/2014: You actually don't even need the thread dispatcher code. You can set whatever you need to null to trigger the first selection changed event and then return out of the event so that future events work as expected.

        private int _hackyfix = 0;
    private void OnMediaSelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        //HACKYFIX: Daniel note:  Very hacky workaround for an api issue
        //Microsoft's api for getting item controls for the flipview item fail on the very first media selection change for some reason.  Basically we ignore the
        //first media selection changed event but spawn a thread to redo the ignored selection changed, hopefully allowing time for whatever is going on
        //with the api to get things sorted out so we can call the "ContainerFromItem" function and actually get the control we need
        if (_hackyfix == 0)
        {
            _hackyfix++;
            /*
            Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
        {
            OnMediaSelectionChanged(sender, e);
        });*/
            return;
        }
        //END OF HACKY FIX
        //Your selection_changed code here
        }