Заставить WPF создавать элементы в ItemsControl

StackOverflow https://stackoverflow.com/questions/630124

  •  08-07-2019
  •  | 
  •  

Вопрос

Я хочу убедиться, что элементы в моем 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 (невозможно добавить элементы непосредственно в < code> ItemPanel , поскольку он генерирует элементы для своего ItemsControl ). В качестве теста я попытался вызвать его AddVisualChild () с помощью Reflection (поскольку он защищен), но это тоже не сработало.

Это было полезно?

Решение 3

Думаю, я понял, как это сделать. Проблема заключалась в том, что сгенерированные элементы не были добавлены в визуальное дерево. После некоторого поиска лучшее, что я могу придумать, - это вызвать некоторые защищенные методы 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);
    }
}

Другие советы

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

<ListBox.ItemsPanel>
  <ItemsPanelTemplate>
      <StackPanel/>
  <ItemsPanelTemplate>
<ListBox.ItemsPanel>

Возможно, вы поступили неправильно. Я подключил событие 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);
}

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

Решение от Энди - очень хорошая идея, но оно неполное. Например, первые 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;
            }
        }
    }

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

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

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

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

Например:

    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);
    }
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top