I want to verify that the items in my ListBox
are displayed correctly in the UI. I figured one way to do this is to go through all of the children of the
For anyone else wondering about this, in Andy's case, perhaps swapping out the VirtualizingStackPanel with a normal StackPanel would be the best solution here.
The reason calling PrepareItemContainer on the ItemContainerGenerator isn't working is that an item must be in the visual tree for PrepareItemContainer to work. With a VirtualizingStackPanel, the item won't be set as a visual child of the panel until the VirtualizingStackPanel determines that it is/is about to be on screen.
Another solution (the one I use) is to create your own VirtualizingPanel, so you can control when items are added to the visual tree.
In my case, I found that calling UpdateLayout()
on the ItemsControl
(ListBox
, ListView
, etc.) started up its ItemContainerGenerator
, such that the generator's status changed from "NotStarted" to "GeneratingContainers", and null
containers were no longer being returned by ItemContainerGenerator.ContainerFromItem
and/or ItemContainerGenerator.ContainerFromIndex
.
For example:
public static bool FocusSelectedItem(this ListBox listbox)
{
int ix;
if ((ix = listbox.SelectedIndex) < 0)
return false;
var icg = listbox.ItemContainerGenerator;
if (icg.Status == GeneratorStatus.NotStarted)
listbox.UpdateLayout();
var el = (UIElement)icg.ContainerFromIndex(ix);
if (el == null)
return false;
listbox.ScrollIntoView(el);
return el == Keyboard.Focus(el);
}
The Solution from Andy is a very good idea, but is incomplete. For example, the first 5 containers are created and in the panel. The list hast 300 > items. I request the last container, with this logic, ADD. Then I request the last index - 1 container, with this logis ADD! That's the problem. The order of the Children inside the panel is not valid.
A Solution for this:
private FrameworkElement GetContainerForIndex(int index)
{
if (ItemsControl == null)
{
return null;
}
var container = ItemsControl.ItemContainerGenerator.ContainerFromIndex(index -1);
if (container != null && container != DependencyProperty.UnsetValue)
{
return container as FrameworkElement;
}
else
{
var virtualizingPanel = FindVisualChild<VirtualizingPanel>(ItemsControl);
if (virtualizingPanel == null)
{
// do something to load the (perhaps currently unloaded panel) once
}
virtualizingPanel = FindVisualChild<VirtualizingPanel>(ItemsControl);
IItemContainerGenerator generator = ItemsControl.ItemContainerGenerator;
using (generator.StartAt(generator.GeneratorPositionFromIndex(index), GeneratorDirection.Forward))
{
bool isNewlyRealized = false;
container = generator.GenerateNext(out isNewlyRealized);
if (isNewlyRealized)
{
generator.PrepareItemContainer(container);
bool insert = false;
int pos = 0;
for (pos = virtualizingPanel.Children.Count - 1; pos >= 0; pos--)
{
var idx = ItemsControl.ItemContainerGenerator.IndexFromContainer(virtualizingPanel.Children[pos]);
if (!insert && idx < index)
{
////Add
virtualizingPanel.GetType().InvokeMember("AddInternalChild", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, Type.DefaultBinder, virtualizingPanel, new object[] { container });
break;
}
else
{
insert = true;
if (insert && idx < index)
{
break;
}
}
}
if (insert)
{
virtualizingPanel.GetType().InvokeMember("InsertInternalChild", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod, Type.DefaultBinder, virtualizingPanel, new object[] { pos + 1, container });
}
}
return container as FrameworkElement;
}
}
}
Just quick looking, if the ListBox uses VirtualizingStackPanel - maybe it will be enough to substitute it with StackPanel like
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel/>
<ItemsPanelTemplate>
<ListBox.ItemsPanel>
You may be going about this the wrong way. What I did is hook up the Loaded event of [the content of] my DataTemplate:
<DataTemplate DataType="{x:Type local:ProjectPersona}">
<Grid Loaded="Row_Loaded">
<!-- ... -->
</Grid>
</DataTemplate>
...and then process the newly-displayed row in the event handler:
private void Row_Loaded(object sender, RoutedEventArgs e)
{
Grid grid = (Grid)sender;
Carousel c = (Carousel)grid.FindName("carousel");
ProjectPersona project = (ProjectPersona)grid.DataContext;
if (project.SelectedTime != null)
c.ScrollItemIntoView(project.SelectedTime);
}
This approach does the initialization/checking of the row when it is first displayed, so it won't do all the rows up-front. If you can live with that, then perhaps this is the more elegant method.
I think I figured out how to do this. The problem was that the generated items were not added to the visual tree. After some searching, the best I could come up with is to call some protected methods of the VirtualizingStackPanel
in the ListBox
. While this isn't ideal, since it's only for testing I think I'm going to have to live with it.
This is what worked for me:
VirtualizingStackPanel itemsPanel = null;
FrameworkElementFactory factory = control.ItemsPanel.VisualTree;
if(null != factory)
{
// This method traverses the visual tree, searching for a control of
// the specified type and name.
itemsPanel = FindNamedDescendantOfType(control,
factory.Type, null) as VirtualizingStackPanel;
}
List<string> generatedItems = new List<string>();
IItemContainerGenerator generator = this.ItemsListBox.ItemContainerGenerator;
GeneratorPosition pos = generator.GeneratorPositionFromIndex(-1);
using(generator.StartAt(pos, GeneratorDirection.Forward))
{
bool isNewlyRealized;
for(int i = 0; i < this.ItemsListBox.Items.Count; i++)
{
isNewlyRealized = false;
UIElement cntr = generator.GenerateNext(out isNewlyRealized) as UIElement;
if(isNewlyRealized)
{
if(i >= itemsPanel.Children.Count)
{
itemsPanel.GetType().InvokeMember("AddInternalChild",
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMember,
Type.DefaultBinder, itemsPanel,
new object[] { cntr });
}
else
{
itemsPanel.GetType().InvokeMember("InsertInternalChild",
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMember,
Type.DefaultBinder, itemsPanel,
new object[] { i, cntr });
}
generator.PrepareItemContainer(cntr);
}
string itemText = GetControlText(cntr);
generatedItems.Add(itemText);
}
}