ItemContainerGenerator.ContainerFromItem() возвращает ноль?

У меня немного странное поведение, с которым я не могу справиться. Когда я перебираю элементы в своем свойстве ListBox.ItemsSource, я не могу получить контейнер? Я ожидаю увидеть возвращенный ListBoxItem, но получаю только NULL.

Есть идеи?

Вот фрагмент кода, который я использую:

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

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

ItemsSource в настоящее время установлен в словарь и содержит несколько KVP.

10 ответов

Решение

Наконец-то разобрался с проблемой... Добавив VirtualizingStackPanel.IsVirtualizing="False" в мой XAML все теперь работает как положено.

С другой стороны, я упускаю все преимущества производительности виртуализации, поэтому я изменил свою нагрузочную маршрутизацию на асинхронную и добавил "спиннер" в мой список во время загрузки...

В этом вопросе Stackru я нашел кое-что, что сработало лучше для моего случая:

Получить строку в датагрид

Помещая UpdateLayout и вызовы ScrollIntoView перед вызовом ContainerFromItem или ContainerFromIndex, вы вызываете реализацию той части DataGrid, которая позволяет ему возвращать значение для ContainerFromItem/ContainerFromIndex:

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

Если вы не хотите, чтобы текущее местоположение в DataGrid изменялось, это, вероятно, не очень хорошее решение для вас, но если все в порядке, оно работает без необходимости выключать виртуализацию.

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");
}

Пройдите по коду с помощью отладчика и посмотрите, действительно ли ничего не восстановлено или as-каст это просто неправильно и, следовательно, превращает его в null (Вы можете просто использовать обычное приведение, чтобы получить правильное исключение).

Одна из часто возникающих проблем заключается в том, что когда ItemsControl Виртуализируется для большинства элементов, контейнер не будет существовать в любой момент времени.

Также я бы не рекомендовал иметь дело с контейнерами элементов напрямую, а связывать свойства и подписываться на события (через ItemsControl.ItemContainerStyle).

Используйте эту подписку:

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

Я немного опаздываю на вечеринку, но вот еще одно решение, которое в моем случае безотказно,

Перепробовав множество решений, предлагающих добавить IsExpanded а также IsSelected к базовому объекту и привязки к ним в TreeViewItem стиль, в то время как в большинстве случаев это работает, но все равно не работает...

Примечание: моя цель состояла в том, чтобы написать мини-/ пользовательский вид, похожий на проводник, в котором, когда я щелкаю папку на правой панели, она выбирается в TreeView, как в Проводнике.

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;
    }
}

Несколько трюков, используемых здесь:

  • стек для расширения каждого элемента сверху вниз
  • использовать генератор текущего уровня, чтобы найти предмет (очень важно)
  • тот факт, что генератор для предметов верхнего уровня никогда не вернется null

Пока это работает очень хорошо,

  • нет необходимости загрязнять ваши типы новыми свойствами
  • нет необходимости отключать виртуализацию вообще.

Хотя отключение виртуализации из XAML работает, я думаю, что лучше отключить его из файла.cs, который использует ContainerFromItem

 VirtualizingStackPanel.SetIsVirtualizing(listBox, false);

Таким образом, вы уменьшаете связь между XAML и кодом; так что вы можете избежать риска взлома кода, касаясь XAML.

Скорее всего, это проблема, связанная с виртуализацией, так ListBoxItem контейнеры генерируются только для видимых на данный момент элементов (см. https://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel(v=vs.110).aspx)

Если вы используете ListBox Я бы предложил перейти на ListView вместо этого - он наследует от ListBoxи это поддерживает ScrollIntoView() метод, который вы можете использовать для управления виртуализацией;

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

(пример выше также использует DoEvents() статический метод объяснен более подробно здесь; WPF, как ждать обновления связывания перед обработкой большего количества кода?)

Есть несколько других незначительных различий между ListBox а также ListView элементы управления (в чем разница между ListBox и ListView) - которые не должны существенно влиять на ваш вариант использования.

VirtualizingStackPanel.IsVirtualizing="False" Делает элемент управления нечетким. Смотрите ниже реализацию. Что помогает мне избежать той же проблемы. Установите ваше приложение VirtualizingStackPanel.IsVirtualizing="True" всегда.

Смотрите ссылку для подробной информации

/// <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;
}

Для тех, у кого все еще есть проблемы с этим, я смог обойти эту проблему, проигнорировав первое событие изменения выбора и используя поток, чтобы в основном повторить вызов. Вот что я в итоге сделал:

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}

РЕДАКТИРОВАНИЕ 29.10.2014: Вам даже не нужен код диспетчера потоков. Вы можете установить все, что вам нужно, чтобы задать нулевое значение, чтобы инициировать первое измененное событие выбора, а затем вернуться из события, чтобы будущие события работали, как ожидалось.

        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
        }
Другие вопросы по тегам