2

I have looked here and here and many other places, but I just can't seem to get the ItemContainerGenerator.ContainerFromItem method to work on a WPF TreeView! I have tried to pass in the actual item I want to see, but not getting anywhere with that, I just tried to get the first item in my TreeView. Here's my sample code:

private static bool ExpandAndSelectItem(ItemsControl parentContainer, object itemToSelect)
{
    // This doesn't work.
    parentContainer.BringIntoView();
    // May be virtualized, bring into view and try again.
    parentContainer.UpdateLayout();
    parentContainer.ApplyTemplate();

    TreeViewItem topItem = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(parentContainer.Items[0]);

    // Can't find child container unless the parent node is Expanded once
    if ((topItem != null) && !topItem.IsExpanded) 
    {
        topItem.IsExpanded = true;
        parentContainer.UpdateLayout();
    }
}

As you can see, I have tried to call many "updating" methods to try to get the TreeView to be "visible" and "accessible". The Catch-22 seems to be that you can't use ContainerFromItem() unless the first TreeViewItem is expanded, but I can't grab the TreeViewItem to Expand it until ContainerFromItem() works!

Another funny thing that is happening is this: When I open this window (it is a UserControl), ContainerFromItem() returns nulls, but if I close the window and open it back up, ContainerFromItem() starts returning non-nulls. Is there any event I should be looking for or forcing to fire?

Lauraducky
  • 674
  • 11
  • 25
Clay Acord
  • 305
  • 1
  • 4
  • 13

3 Answers3

4

Turns out the event I was looking for was "Loaded". I just attached an event handler onto my treeview in the XAML, and called my logic in that event handler.

<TreeView x:Name="MyTreeView"
          Margin="0,5,0,5"
          HorizontalAlignment="Left"
          BorderThickness="0"
          FontSize="18"
          FontFamily="Segoe WP"
          MaxWidth="900"
          Focusable="True"
          Loaded="MyTreeView_Load">
    ...
</TreeView>

The event handler:

private void MyTreeView_Load(object sender, RoutedEventArgs e)
{
    ShowSelectedThing(MyTreeView, ThingToFind);
}
// Gotta call the TreeView an ItemsControl to cast it between TreeView and TreeViewItem
// as you recurse
private static bool ShowSelectedThing(ItemsControl parentContainer, object ThingToFind)
{
    // check current level of tree
    foreach (object item in parentContainer.Items)
    {
        TreeViewItem currentContainer = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
        if ((currentContainer != null) && (item == ThingToFind)
        {
            currentContainer.IsSelected = true;
            currentContainer.BringIntoView();
            return true;
        }
    }
    // item is not found at current level, check the kids
    foreach (object item in parentContainer.Items)
    {
        TreeViewItem currentContainer = (TreeViewItem)parentContainer.ItemContainerGenerator.ContainerFromItem(item);
        if ((currentContainer != null) && (currentContainer.Items.Count > 0))
        {
            // Have to expand the currentContainer or you can't look at the children
            currentContainer.IsExpanded = true;
            currentContainer.UpdateLayout();
            if (!ShowSelectedThing(currentContainer, ThingToFind))
            {
                // Haven't found the thing, so collapse it back
                currentContainer.IsExpanded = false;
            }
            else
            {
                // We found the thing
                return true;
            }
        }
    }
    // default
    return false;
}

Hope this helps someone. Sometimes in the real world, with demanding customers, weird requirements and short deadlines, ya gotta hack!

Lauraducky
  • 674
  • 11
  • 25
Clay Acord
  • 305
  • 1
  • 4
  • 13
  • I appreciated the "HACK" as it has been referred. I am unsure why this one has a -1. I understand that it doesn't implement a ViewModel/ModelView, however, not everything needs to be done via a ViewModel. In time, people may become comfortable with WPF to bind it all. Companies don't always want to spend the extra time/money having someone create all the extra stuff that now exists, just to make things easier in the long run. Sometimes there is no long run. In my opinion, this is why there are event handlers on objects still, because everything cannot be bound. – ConcernedDeveloper Feb 18 '14 at 20:51
  • Thank you for this! It helped me a lot! – Stone Mar 09 '16 at 13:57
2

When the container generator's status is 'NotStarted' or 'ContainersGenerating', you can't find the container.

Use this method to find the container of data item.

    private static async Task<TreeViewItem> FindItemContainer(ItemsControl itemsControl, object item)
    {
        var generator = itemsControl.ItemContainerGenerator;
        if (generator.Status != GeneratorStatus.ContainersGenerated)
        {
            var tcs = new TaskCompletionSource<object>();
            EventHandler handler = null;
            handler = (s, e) =>
            {
                if (generator.Status == GeneratorStatus.ContainersGenerated)
                {
                    generator.StatusChanged -= handler;
                    tcs.SetResult(null);
                }
                else if (generator.Status == GeneratorStatus.Error)
                {
                    generator.StatusChanged -= handler;
                    tcs.SetException(new InvalidOperationException());
                }
            };
            generator.StatusChanged += handler;
            if (itemsControl is TreeViewItem tvi)
                tvi.IsExpanded = true;
            itemsControl.UpdateLayout();
            await tcs.Task;
        }

        var container = (TreeViewItem)generator.ContainerFromItem(item);
        if(container == null)
        {
            foreach (var parentItem in itemsControl.Items)
            {
                var parentContainer = (TreeViewItem)generator.ContainerFromItem(parentItem);
                container = await FindItemContainer(parentContainer, item);
                if (container != null)
                    return container;
            }
        }
        return container;
    }
SunnyCase
  • 39
  • 3
0
    private void Lv_SelectionChanged(object sender, SelectionChangedEventArgs e)
    {
        ListView Lv = (ListView)sender;
        Lv.UpdateLayout();           // 1.step
        DependencyObject Dep = Lv.ItemContainerGenerator
            .ContainerFromItem(Lv.SelectedItem);
        ((ListViewItem)Dep).Focus(); //2.step
    }

I had come across this issue time ago and now again I got stuck with it for quite a while. Any MessageBox launch or an expand or dropdown on your particular control type, any of these do the job and start the ItemContainerGenerator. The .UpdateLayout() however is the right thing to do, before the .Focus(). Should be analogous for a Treeview, or one of its Items.

Josh
  • 79
  • 3