2009-03-10 9 views
6

Je souhaite vérifier que les éléments de mon ListBox s'affichent correctement dans l'interface utilisateur. J'ai pensé qu'une façon de faire ceci est de passer en revue tous les enfants du ListBox dans l'arbre visuel, d'obtenir leur texte, puis de comparer cela avec ce que j'attends du texte.Forcer WPF à créer les éléments dans ItemsControl

Le problème avec cette approche est qu'en interne ListBox utilise un VirtualizingStackPanel pour afficher ses éléments, de sorte que seuls les éléments visibles sont créés. J'ai finalement rencontré la classe ItemContainerGenerator, qui semble devoir forcer WPF à créer les contrôles dans l'arborescence visuelle pour l'élément spécifié. Malheureusement, cela cause des effets secondaires bizarres pour moi. Voici mon code pour générer tous les éléments dans le 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 traverse tout l'arbre visuel jusqu'à ce qu'un TextBlock se trouve je réalise Il existe d'autres façons d'avoir du texte dans un élément, mais je le réparerai une fois que la génération d'éléments fonctionnera correctement.)

Dans mon application, ItemsListBox contient 20 éléments, les 12 premiers éléments étant visibles au départ. Le texte pour les 14 premiers éléments est correct (probablement parce que leurs contrôles ont déjà été générés). Cependant, pour les articles 15 à 20, je n'ai aucun texte. En outre, si je fais défiler vers le bas du ItemsListBox, le texte des articles 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 une manière différente/meilleure de forcer les éléments d'un ItemsControl à être ajoutés à l'arborescence visuelle?

Mise à jour: Je pense que j'ai trouvé pourquoi cela se produit, même si je ne sais pas comment le réparer. Ma supposition que l'appel à PrepareItemContainer() générerait les contrôles nécessaires pour afficher l'élément, puis ajouter 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é au ItemsControl jusqu'à ce que je défile vers le bas pour le voir, et à ce moment seul le conteneur lui-même (ListBoxItem) est créé - ses enfants ne sont pas créés (il devrait y avoir quelques contrôles ajoutés ici, dont devrait être le TextBlock qui affichera le texte de l'article).

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

Je ne pouvais pas trouver un bon moyen d'ajouter le ListBoxItem à l'arbre visuel. J'ai trouvé le VirtualizingStackPanel dans l'arbre visuel, mais en appelant ses Children.Add() les résultats dans un InvalidOperationException (ne peut pas ajouter des articles directement au ItemPanel, puisqu'il génère des articles pour son ItemsControl). Tout comme un test, j'ai essayé d'appeler son AddVisualChild() en utilisant la réflexion (car il est protégé), mais cela n'a pas fonctionné non plus.

+0

Je ne comprends pas. Pourquoi voulez-vous faire cela? Essai? –

+0

Oui - juste à des fins de test. – Andy

+0

Vous pouvez définir la propriété attachée ['VirtualizingStackPanel.IsVirtualizing'] (http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel.isvirtualizing.aspx) à' false' sur 'ItemsListBox 'avant d'ajouter les éléments. Lorsque vous définissez à true, les choses vont commencer à virtualiser lorsque vous faites défiler. –

Répondre

1

Je pense que je me suis dit comment faire. 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 meilleur que je pourrais trouver est d'appeler quelques méthodes protégées du VirtualizingStackPanel dans le ListBox. Bien que ce ne soit pas idéal, puisque c'est seulement pour les tests, je pense que je vais devoir vivre avec.

C'est 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); 
    } 
} 
3

Juste rapide à la recherche, si le ListBox utilise VirtualizingStackPanel - ce sera peut-être suffisant pour le remplacer par StackPanel comme

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

Bien que cela corrige, je préfère ne pas changer le ListBox lui-même si je peux l'éviter, car il serait préférable d'utiliser un VirtualizingStackPanel dans l'application réelle. – Andy

+0

J'ai essayé ceci avec un Combobox et pour une raison quelconque cela n'a pas fonctionné. –

3

Vous allez peut-être sur ce dans le mauvais sens. Ce que je ne l'est crochet événement Loaded de [le contenu] mon DataTemplate:

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

... et traiter ensuite la ligne nouvellement 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 fait l'initialisation/vérification de la ligne quand elle est d'abord affichée, donc elle ne fera pas toutes les lignes à l'avance. Si vous pouvez vivre avec cela, alors c'est peut-être la méthode la plus élégante.

0

Pour quiconque s'interrogeant à ce sujet, dans le cas d'Andy, il serait peut-être préférable de remplacer le VirtualizingStackPanel par un StackPanel normal.

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

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

1

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 contient 300 éléments. Je demande le dernier conteneur, avec cette logique, AJOUTER. Puis je demande le dernier index - 1 container, 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; 
      } 
     } 
    } 
-1

Dans mon cas, je trouve que l'appel UpdateLayout() sur le ItemsControl (ListBox, ListView, etc.) a commencé son ItemContainerGenerator, de sorte que l'état du générateur a changé de « Non démarré "à" GeneratingContainers ", et null conteneurs ne sont plus retourné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); 
    } 
Questions connexes