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
}