views:

1146

answers:

4

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 ListBox in the visual tree, get their text, and then compare that with what I expect the text to be.

The problem with this approach is that internally ListBox uses a VirtualizingStackPanel to display its items, so only the items that are visible are created. I eventually came across the ItemContainerGenerator class, which looks like it should force WPF to create the controls in the visual tree for the specified item. Unfortunately, that is causing some weird side affects for me. Here is my code to generate all of the items in the ListBox:

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;
        DependencyObject cntr = generator.GenerateNext(out isNewlyRealized);
        if(isNewlyRealized)
        {
            generator.PrepareItemContainer(cntr);
        }

        string itemText = GetControlText(cntr);
        generatedItems.Add(itemText);
    }
}

(I can provide the code for GetItemText() if you'd like, but it just traverses the visual tree until a TextBlock is found. I realize that ther are other ways to have text in an item, but I'll fix that up once I get item generation working properly.)

In my app, ItemsListBox contains 20 items, with the first 12 items initially visible. The text for the first 14 items is correct (likely because their controls have already been generated). However, for items 15-20, I don't get any text at all. In addition, if I scroll to the bottom of the ItemsListBox, the text of items 15-20 is also blank. So it seems like I'm interfering with WPF's normal mechanism for generating controls some how.

What am I doing wrong? Is there a different/better way of forcing the items in an ItemsControl to be added to the visual tree?

Update: I think that I have found why this is occurring, although I do not know how to fix it. My assumption that the call to PrepareItemContainer() would generate any necessary controls to display the item, and then add the container to the visual tree in the correct location. It turns out that it is not doing either of these things. The container isn't added to the ItemsControl until I scroll down to view it, and at that time only the container itself (i.e. ListBoxItem) is created - its children are not created (there should be a few controls added here, one of which should be the TextBlock that will display the text of the item).

If I traverse the visual tree of the control that I passed to PrepareItemContainer() the results are the same. In both cases only the ListBoxItem is created, and none of its children are created.

I could not find a good way to add the ListBoxItem to the visual tree. I found the VirtualizingStackPanel in the visual tree, but calling its Children.Add() results in an InvalidOperationException (cannot add items directly to the ItemPanel, since it generates items for its ItemsControl). Just as a test, I tried calling its AddVisualChild() using Reflection (since it is protected), but that didn't work, either.

A: 

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>
While that does fix it, I'd rather not change the ListBox itself if I can avoid it, since it would be better to use a VirtualizingStackPanel in the actual app.
Andy
A: 

You could set the VirtualizingStackPanel.IsVirtualizing attached property to false on ItemsListBox before adding the items. When you set it to true again things will start virtualizing when you scroll.

Robert Macnee
+1  A: 

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);
    }
}
Andy
A: 

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.

David Turner