Question

Je souhaite vérifier que les éléments de mon ListBox sont correctement affichés dans l'interface utilisateur. J'ai pensé qu'une façon de faire ceci est de passer en revue tous les enfants du ListBox de l'arborescence visuelle, de récupérer leur texte et de le comparer ensuite avec ce que j'attends du texte.

Le problème avec cette approche est que ListBox utilise en interne un VirtualizingStackPanel pour afficher ses éléments. Ainsi, seuls les éléments visibles sont créés. J'ai finalement rencontré la classe ItemContainerGenerator , qui devrait forcer WPF à créer les contrôles dans l'arborescence visuelle de l'élément spécifié. Malheureusement, cela cause des effets étranges sur moi. Voici mon code pour générer tous les éléments de la 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);
    }
}

(Je peux fournir le code pour GetItemText () si vous le souhaitez, mais il ne fait que traverser l'arborescence visuelle jusqu'à ce qu'un TextBlock soit trouvé. Il existe d’autres moyens d’ajouter du texte à un élément, mais je vais y remédier dès que la génération d’éléments fonctionnera correctement.)

Dans mon application, ItemsListBox contient 20 éléments, les 12 premiers éléments étant initialement visibles. Le texte des 14 premiers éléments est correct (probablement parce que leurs contrôles ont déjà été générés). Cependant, pour les éléments 15 à 20, je ne reçois aucun texte du tout. De plus, si je fais défiler vers le bas du ItemsListBox , le texte des éléments 15-20 est également vide. Il semble donc que j'interfère avec le mécanisme normal de WPF pour générer des contrôles, d'une certaine manière.

Qu'est-ce que je fais mal? Existe-t-il un moyen meilleur / différent de forcer les éléments d’un ItemsControl à être ajoutés à l’arbre visuel?

Mise à jour : je pense avoir trouvé pourquoi, mais je ne sais pas comment résoudre ce problème. Mon hypothèse selon laquelle l'appel à PrepareItemContainer () générerait les contrôles nécessaires pour afficher l'élément, puis ajouterait le conteneur à l'arborescence visuelle à l'emplacement correct. Il s'avère qu'il ne fait aucune de ces choses. Le conteneur n'est pas ajouté à ItemsControl tant que je n'ai pas fait défiler l'écran pour le visualiser. À ce moment, seul le conteneur lui-même (c'est-à-dire ListBoxItem ) est créé - ses enfants ne le sont pas. créé (il devrait y avoir quelques contrôles ajoutés ici, l'un d'eux devrait être le TextBlock qui affichera le texte de l'élément).

Si je traverse l'arborescence visuelle du contrôle que j'ai transmis à PrepareItemContainer () , les résultats sont identiques. Dans les deux cas, seul le ListBoxItem est créé et aucun de ses enfants n'est créé.

Je ne trouvais pas le moyen d'ajouter le ListBoxItem à l'arborescence visuelle. J'ai trouvé le VirtualizingStackPanel dans l'arborescence visuelle, mais l'appel de son Children.Add () donne lieu à une InvalidOperationException (impossible d'ajouter des éléments directement au < code> ItemPanel , car il génère des éléments pour son ItemsControl ). À titre de test, j’ai essayé d’appeler son AddVisualChild () à l’aide de Reflection (car il est protégé), mais cela n’a pas fonctionné non plus.

Était-ce utile?

La solution 3

Je pense avoir compris comment faire cela. Le problème était que les éléments générés n'étaient pas ajoutés à l'arborescence visuelle. Après quelques recherches, le mieux que je puisse trouver est d’appeler des méthodes protégées du VirtualizingStackPanel dans le ListBox . Ce n’est pas idéal, c’est uniquement pour les tests, je pense que je vais devoir vivre avec.

Voici ce qui a fonctionné pour moi:

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

Autres conseils

En un clin d’œil, si le ListBox utilise VirtualizingStackPanel, il suffira peut-être de le remplacer par StackPanel comme

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

Vous pouvez vous y prendre de la mauvaise façon. Ce que j'ai fait est de relier l'événement Loaded de [le contenu de] mon DataTemplate:

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

... puis traitez la nouvelle ligne affichée dans le gestionnaire d'événements:

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

Cette approche effectue l’initialisation / vérification de la ligne lors de son affichage initial. Elle ne traitera donc pas toutes les lignes à l’avance. Si vous pouvez vivre avec cela, alors c'est peut-être la méthode la plus élégante.

La solution d’Andy est une très bonne idée, mais elle est incomplète. Par exemple, les 5 premiers conteneurs sont créés et dans le panneau. La liste en contient 300 > articles. Je demande le dernier conteneur, avec cette logique, ADD. Ensuite, je demande le dernier index - 1 conteneur, avec ce logis ADD! C'est le problème. L'ordre des enfants à l'intérieur du panneau n'est pas valide.

Une solution pour cela:

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

Pour ceux qui s’interrogent à ce sujet, dans le cas d’Andy, échanger le VirtualizingStackPanel avec un StackPanel normal serait peut-être la meilleure solution ici.

La raison pour laquelle l'appel de PrepareItemContainer sur ItemContainerGenerator ne fonctionne pas est qu'un élément doit figurer dans l'arborescence de synthèse pour que PrepareItemContainer fonctionne. Avec VirtualizingStackPanel, l'élément ne sera pas défini comme un enfant visuel du panneau tant que VirtualizingStackPanel n'aura pas déterminé qu'il est / est sur le point d'être affiché à l'écran.

Une autre solution (celle que j'utilise) consiste à créer votre propre VirtualizingPanel, afin que vous puissiez contrôler le moment où des éléments sont ajoutés à l'arborescence visuelle.

Dans mon cas, j'ai constaté que l'appel de UpdateLayout () sur ItemsControl ( ListBox , ListView , etc.) a démarré son ItemContainerGenerator , de sorte que le statut du générateur soit passé de " NotStarted " Les conteneurs "GeneratingContainers", et null ne sont plus renvoyés par ItemContainerGenerator.ContainerFromItem et / ou ItemContainerGenerator.ContainerFromIndex .

Par exemple:

    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);
    }
Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top