Forcing WPF to create the items in an ItemsControl

柔情痞子 提交于 2019-12-01 02:23:52

问题


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.


回答1:


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>



回答2:


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.




回答3:


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



回答4:


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



回答5:


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.




回答6:


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


来源:https://stackoverflow.com/questions/630124/forcing-wpf-to-create-the-items-in-an-itemscontrol

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!