2009-03-10 15 views
6

Desidero verificare che gli elementi nel mio ListBox siano visualizzati correttamente nell'interfaccia utente. Ho capito che un modo per farlo è quello di passare attraverso tutti i bambini dello ListBox nell'albero visivo, ottenere il loro testo e quindi confrontarlo con quello che mi aspetto che il testo sia.Forzare WPF per creare gli elementi in un ItemsControl

Il problema con questo approccio è che internamente ListBox utilizza uno VirtualizingStackPanel per visualizzare i suoi elementi, quindi vengono creati solo gli elementi visibili. Alla fine mi sono imbattuto nella classe ItemContainerGenerator, che sembra debba costringere WPF a creare i controlli nella struttura visiva per l'elemento specificato. Sfortunatamente, ciò sta causando alcuni strani effetti collaterali per me. Ecco il mio codice per generare tutti gli elementi nella 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); 
    } 
} 

(posso fornire il codice per GetItemText() se vuoi, ma semplicemente attraversa la struttura ad albero visuale fino a quando un TextBlock è trovato mi rendo conto ci sono altri modi per avere un testo in un articolo, ma lo aggiusterò una volta che avrò funzionato correttamente.)

Nella mia app, ItemsListBox contiene 20 articoli, con i primi 12 elementi inizialmente visibili. Il testo per i primi 14 elementi è corretto (probabilmente perché i loro controlli sono già stati generati). Tuttavia, per gli articoli 15-20, non ottengo alcun testo. Inoltre, se si scorre verso il basso dello ItemsListBox, anche il testo degli elementi 15-20 è vuoto. Quindi sembra che io stia interferendo con il normale meccanismo di WPF per la generazione dei controlli.

Cosa sto sbagliando? C'è un modo diverso/migliore di forzare gli oggetti in un ItemsControl da aggiungere all'albero visivo?

Aggiornamento: Penso di aver scoperto perché si sta verificando, anche se non so come risolverlo. Suppongo che la chiamata a PrepareItemContainer() generi tutti i controlli necessari per visualizzare l'elemento e quindi aggiungere il contenitore all'albero visivo nella posizione corretta. Si scopre che non sta facendo nessuna di queste cose. Il contenitore non viene aggiunto allo ItemsControl finché non si scorre verso il basso per visualizzarlo e in quel momento viene creato solo il contenitore stesso (ad esempio ListBoxItem) - i suoi figli non vengono creati (ci dovrebbero essere alcuni controlli aggiunti qui, uno dei quali dovrebbe essere il TextBlock che visualizzerà il testo dell'articolo).

Se attraverso l'albero visivo del controllo che ho passato a PrepareItemContainer() i risultati sono gli stessi. In entrambi i casi viene creato solo lo ListBoxItem e nessuno dei suoi figli viene creato.

Non sono riuscito a trovare un buon modo per aggiungere ListBoxItem all'albero visivo. Ho trovato il VirtualizingStackPanel nell'albero visivo, ma chiamando i suoi risultati Children.Add() in un InvalidOperationException (non è possibile aggiungere elementi direttamente allo ItemPanel, poiché genera elementi per il suo ItemsControl). Proprio come un test, ho provato a chiamare il suo AddVisualChild() usando Reflection (dato che è protetto), ma non ha funzionato.

+0

Non capisco. Perché vuoi farlo? Prove? –

+0

Sì, solo a scopo di test. – Andy

+0

È possibile impostare la proprietà ['VirtualizingStackPanel.IsVirtualizing'] (http://msdn.microsoft.com/en-us/library/system.windows.controls.virtualizingstackpanel.isvirtualizing.aspx) associata a' false' in 'ItemsListBox 'prima di aggiungere gli articoli. Quando lo imposti a 'true' di nuovo, le cose inizieranno a virtualizzare quando scorri. –

risposta

1

Credo di aver capito come fare questo. Il problema era che gli oggetti generati non venivano aggiunti all'albero visivo.Dopo alcune ricerche, la cosa migliore che potrei inventare è chiamare alcuni metodi protetti dello VirtualizingStackPanel nello ListBox. Anche se questo non è l'ideale, dal momento che è solo per i test, penso che dovrò conviverci.

Questo è ciò che ha funzionato per 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); 
    } 
} 
3

Proprio rapida ricerca, se il ListBox usa VirtualizingStackPanel - forse sarà sufficiente per sostituirlo con StackPanel come

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

Anche se questo lo risolve, preferirei non modificare il ListBox stesso se posso evitarlo, dal momento che sarebbe meglio usare un VirtualizingStackPanel nell'app reale. – Andy

+0

Ho provato questo con un Combobox e per qualche motivo non ha funzionato. –

3

si può andare su questo nel modo sbagliato. Quello che ho fatto è collegare l'evento Loaded [del contenuto di] il mio DataTemplate:

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

... e poi elaborare il riga appena visualizzato nel gestore di eventi:

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

Questo approccio esegue l'inizializzazione/controllo della riga quando viene visualizzata per la prima volta, quindi non farà tutte le righe in primo piano. Se riesci a conviverci, forse questo è il metodo più elegante.

0

Per chiunque altro si chiedesse di questo, nel caso di Andy, forse scambiare il VirtualizingStackPanel con un normale StackPanel sarebbe la soluzione migliore qui.

La ragione per cui PrepareItemContainer su ItemContainerGenerator non funziona è che un elemento deve essere nell'albero visivo affinché PrepareItemContainer funzioni. Con VirtualizingStackPanel, l'elemento non verrà impostato come figlio visivo del pannello fino a quando VirtualizingStackPanel non stabilirà/sta per essere visualizzato sullo schermo.

Un'altra soluzione (quella che uso) è creare il proprio VirtualizingPanel, in modo da poter controllare quando gli oggetti vengono aggiunti all'albero visivo.

1

La soluzione di Andy è un'ottima idea, ma è incompleta. Ad esempio, i primi 5 contenitori vengono creati e nel pannello. La lista ha 300 elementi>. Chiedo l'ultimo contenitore, con questa logica, ADD. Quindi richiedo l'ultimo indice - 1 contenitore, con questo log ADD! Questo è il problema. L'ordine dei bambini all'interno del pannello non è valido.

una soluzione per questo:

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

Nel mio caso, ho scoperto che chiamando UpdateLayout() sul ItemsControl (ListBox, ListView, etc.) ha iniziato la sua ItemContainerGenerator, in modo tale che lo stato del generatore cambiato da "NotStarted "a" GeneratingContainers "e i contenitori null non venivano più restituiti da ItemContainerGenerator.ContainerFromItem e/o ItemContainerGenerator.ContainerFromIndex.

Ad esempio:

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