Принуждение WPF к созданию элементов в ItemsControl - PullRequest
6 голосов
/ 10 марта 2009

Я хочу убедиться, что элементы в моем ListBox правильно отображаются в пользовательском интерфейсе. Я решил, что один из способов сделать это - просмотреть все дочерние элементы ListBox в визуальном дереве, получить их текст, а затем сравнить его с ожидаемым текстом.

Проблема этого подхода заключается в том, что внутренне ListBox использует VirtualizingStackPanel для отображения своих элементов, поэтому создаются только видимые элементы. В конце концов я наткнулся на класс ItemContainerGenerator, который, похоже, должен заставить WPF создавать элементы управления в визуальном дереве для указанного элемента. К сожалению, это вызывает некоторые странные побочные эффекты для меня. Вот мой код для генерации всех элементов в 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);
    }
}

(Я могу предоставить код для GetItemText(), если хотите, но он просто перебирает визуальное дерево, пока не будет найден TextBlock. Я понимаю, что есть и другие способы добавить текст в элемент, но я Я исправлю это, как только у меня получится сгенерировать элементы.)

В моем приложении ItemsListBox содержит 20 элементов, причем первые 12 элементов изначально видны. Текст для первых 14 элементов является правильным (вероятно, потому что их элементы управления уже были созданы). Однако для пунктов 15-20 я не получаю никакого текста вообще. Кроме того, если я прокручиваю до нижней части ItemsListBox, текст пунктов 15-20 также будет пустым. Похоже, я вмешиваюсь в обычный механизм WPF для генерации элементов управления, как-то.

Что я делаю не так? Есть ли другой / лучший способ заставить элементы из ItemsControl быть добавленными в визуальное дерево?

Обновление : Я думаю, что я нашел, почему это происходит, хотя я не знаю, как это исправить. Я предполагаю, что вызов PrepareItemContainer() сгенерирует все необходимые элементы управления для отображения элемента, а затем добавит контейнер в визуальное дерево в правильном месте. Оказывается, он не делает ни одну из этих вещей. Контейнер не добавляется в ItemsControl до тех пор, пока я не прокручиваю его вниз, и в это время создается только сам контейнер (т.е. ListBoxItem) - его дочерние элементы не создаются (здесь должно быть добавлено несколько элементов управления). , один из которых должен быть TextBlock, который будет отображать текст элемента).

Если я перейду к визуальному дереву элемента управления, который я передал PrepareItemContainer(), результаты будут такими же. В обоих случаях создается только ListBoxItem, и ни один из его дочерних элементов не создается.

Я не смог найти хороший способ добавить ListBoxItem к визуальному дереву. Я нашел VirtualizingStackPanel в визуальном дереве, но вызов его Children.Add() приводит к InvalidOperationException (невозможно добавить элементы непосредственно в ItemPanel, так как он генерирует элементы для ItemsControl). В качестве теста я попытался вызвать его AddVisualChild() с помощью Reflection (поскольку он защищен), но это тоже не сработало.

Ответы [ 6 ]

3 голосов
/ 20 января 2010

Возможно, вы поступили неправильно. Я подключил событие Loaded к [содержимому] моего DataTemplate:

<DataTemplate DataType="{x:Type local:ProjectPersona}">
  <Grid Loaded="Row_Loaded">
    <!-- ... -->
  </Grid>
</DataTemplate>

... и затем обработать вновь отображаемую строку в обработчике событий:

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

Этот подход выполняет инициализацию / проверку строки при ее первом отображении, поэтому он не будет выполнять все строки заранее. Если вы можете жить с этим, то, возможно, это более элегантный метод.

3 голосов
/ 10 марта 2009

Просто быстро, если ListBox использует VirtualizingStackPanel - может быть, этого будет достаточно, чтобы заменить его StackPanel как

<ListBox.ItemsPanel>
  <ItemsPanelTemplate>
      <StackPanel/>
  <ItemsPanelTemplate>
<ListBox.ItemsPanel>
1 голос
/ 04 декабря 2013

Решение от Энди - очень хорошая идея, но оно неполное. Например, первые 5 контейнеров созданы и в панели. В списке 300> предметов. Я запрашиваю последний контейнер с этой логикой ADD. Затем я запрашиваю последний индекс - 1 контейнер, с этим logis ADD! Это проблема. Порядок «Дети» внутри панели недействителен.

Решение для этого:

    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;
            }
        }
    }
1 голос
/ 13 марта 2009

Думаю, я понял, как это сделать. Проблема заключалась в том, что сгенерированные элементы не были добавлены в визуальное дерево. После некоторого поиска лучшее, что я мог придумать, - это вызвать некоторые защищенные методы VirtualizingStackPanel в ListBox. Хотя это не идеально, так как только для тестирования, я думаю, мне придется с этим смириться.

Вот что у меня сработало:

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);
    }
}
0 голосов
/ 15 мая 2015

В моем случае я обнаружил, что вызов UpdateLayout() на ItemsControl (ListBox, ListView и т. Д.) Запустил ItemContainerGenerator, так что состояние генератора изменилось с "NotStarted" на " Контейнеры GeneratingContainers "и null больше не возвращались ItemContainerGenerator.ContainerFromItem и / или ItemContainerGenerator.ContainerFromIndex.

Например:

    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);
    }
0 голосов
/ 08 июня 2011

Для всех, кто интересуется этим, в случае с Энди, возможно, лучшим решением будет замена VirtualizingStackPanel на обычную StackPanel.

Причина, по которой метод PrepareItemContainer для ItemContainerGenerator не работает, заключается в том, что элемент должен находиться в визуальном дереве, чтобы PrepareItemContainer работал. С VirtualizingStackPanel элемент не будет установлен как визуальный дочерний элемент панели, пока VirtualizingStackPanel не определит, что он находится на экране.

Другое решение (которое я использую) заключается в создании вашей собственной VirtualizingPanel, чтобы вы могли контролировать, когда элементы добавляются в визуальное дерево.

...