ItemContainerGenerator.ContainerFromItem() returns null?
Asked Answered
V

10

52

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.

Vilayet answered 15/7, 2011 at 21:31 Comment(4)
Can't you just iterate through Items, which would be a readonly collection (but its contents would not be readonly)?Stilla
I tried that, too. Using .ContainerFromIndex() also returns null.Vilayet
[Check the following link to get the answer][1] [1]: #10591891Cheese
The following link contains its answer. [Click here][1] [1]: #10591891Cheese
V
25

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...

Vilayet answered 18/7, 2011 at 16:43 Comment(3)
PS - Thanks for the hint H.B. Your comment on the virtualization is what led me down that path.Vilayet
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.Professorate
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.Extensible
P
61

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.

Prosenchyma answered 27/1, 2013 at 19:19 Comment(3)
Thanks, this solved my problem. I was trying to set focus to an item that was not available yet. UpdateLayout() worked great.Colombia
I am also doing same but after 109 items it return null :(Lilililia
Oh my... The point is UpdateLayout. Worked thanks!Bowleg
V
25

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...

Vilayet answered 18/7, 2011 at 16:43 Comment(3)
PS - Thanks for the hint H.B. Your comment on the virtualization is what led me down that path.Vilayet
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.Professorate
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.Extensible
W
23
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");
}
Wainscoting answered 1/2, 2016 at 23:10 Comment(6)
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.Warsle
Can you explain this answer, instead of just pasting code? What is list?Youngling
@Stealth Rabbi: a ListBox or ListView. What do you actually want an item container for otherwise?Rothenberg
@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.Youngling
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.Rothenberg
Holy, this is so obvious. Thanks for pointing that out xDMiscegenation
S
8

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).

Sallysallyann answered 17/7, 2011 at 2:49 Comment(0)
I
7

Use this subscription:

TheListBox.ItemContainerGenerator.StatusChanged += (sender, e) =>
{
  TheListBox.Dispatcher.Invoke(() =>
  {
     var TheOne = TheListBox.ItemContainerGenerator.ContainerFromIndex(0);
     if (TheOne != null)
       // Use The One
  });
};
Intracutaneous answered 26/4, 2014 at 13:18 Comment(2)
Why are you using the dispatcher as well?Ihs
This should be the correct answer--it's the only one that directly answers the question.Mendiola
J
4

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.

Jubilant answered 2/9, 2013 at 13:9 Comment(1)
When exactly do you need to do this? I'm working with a DataGrid. When I put the IsVirtualising(false) in the XAML, the performance drops like a rock and when I do this after the GUI has been shown entirely, it seems to be too late.Hemp
A
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.
Apicella answered 5/1, 2016 at 20:21 Comment(2)
Works great. Might be better to migrate this to a question (perhaps a new question) specifically about TreeViews, since this one is about ListBox, to give this awesome answer a bit more visibility.Bombardon
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.Eidetic
S
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.

Strafford answered 13/1, 2016 at 17:7 Comment(1)
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"!...Sheepshearing
F
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;
}
Falcone answered 23/7, 2013 at 8:34 Comment(0)
E
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
        }
Eudoxia answered 28/10, 2014 at 14:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.