Pregunta

Deseo verificar que los elementos de mi ListBox se muestren correctamente en la IU. Pensé que una forma de hacerlo es pasar por todos los elementos secundarios del ListBox en el árbol visual, obtener su texto y luego compararlo con lo que espero que sea el texto.

El problema con este enfoque es que internamente ListBox usa un VirtualizingStackPanel para mostrar sus elementos, por lo que solo se crean los elementos que están visibles. Finalmente encontré la clase ItemContainerGenerator , que parece que debería obligar a WPF a crear los controles en el árbol visual para el elemento especificado. Desafortunadamente, eso está causando algunos efectos secundarios extraños para mí. Aquí está mi código para generar todos los elementos en el 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);
    }
}

(Puedo proporcionar el código para GetItemText () si lo desea, pero solo atraviesa el árbol visual hasta que se encuentre un TextBlock . Me doy cuenta de que hay hay otras formas de tener texto en un elemento, pero lo arreglaré una vez que logre que la generación de elementos funcione correctamente.

En mi aplicación, ItemsListBox contiene 20 elementos, con los primeros 12 elementos inicialmente visibles. El texto para los primeros 14 elementos es correcto (probablemente porque sus controles ya se han generado). Sin embargo, para los ítems 15-20, no recibo ningún texto. Además, si me desplazo hasta la parte inferior de ItemsListBox , el texto de los elementos 15-20 también está en blanco. Entonces parece que estoy interfiriendo con el mecanismo normal de WPF para generar controles de alguna manera.

¿Qué estoy haciendo mal? ¿Hay una forma diferente / mejor de forzar que los elementos en un ItemsControl se agreguen al árbol visual?

Actualización : Creo que he descubierto por qué ocurre esto, aunque no sé cómo solucionarlo. Supongo que la llamada a PrepareItemContainer () generaría los controles necesarios para mostrar el elemento y luego agregaría el contenedor al árbol visual en la ubicación correcta. Resulta que no está haciendo ninguna de estas cosas. El contenedor no se agrega al ItemsControl hasta que me desplazo hacia abajo para verlo, y en ese momento solo se crea el contenedor en sí (es decir, ListBoxItem ), sus hijos no son creado (debe haber algunos controles agregados aquí, uno de los cuales debe ser el TextBlock que mostrará el texto del elemento).

Si recorro el árbol visual del control que pasé a PrepareItemContainer () , los resultados son los mismos. En ambos casos, solo se crea el ListBoxItem y ninguno de sus elementos secundarios se crea.

No pude encontrar una buena manera de agregar el ListBoxItem al árbol visual. Encontré el VirtualizingStackPanel en el árbol visual, pero llamar a su Children.Add () da como resultado un InvalidOperationException (no puede agregar elementos directamente a la < code> ItemPanel , ya que genera elementos para su ItemsControl ). Solo como prueba, intenté llamar a su AddVisualChild () usando Reflection (ya que está protegido), pero tampoco funcionó.

¿Fue útil?

Solución 3

Creo que descubrí cómo hacer esto. El problema fue que los elementos generados no se agregaron al árbol visual. Después de algunas búsquedas, lo mejor que puedo encontrar es llamar a algunos métodos protegidos del VirtualizingStackPanel en el ListBox . Si bien esto no es ideal, ya que es solo para pruebas, creo que voy a tener que vivir con eso.

Esto es lo que funcionó para mí:

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

Otros consejos

Solo una mirada rápida, si ListBox usa VirtualizingStackPanel, tal vez sea suficiente para sustituirlo por StackPanel como

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

Puede que esté haciendo esto de la manera incorrecta. Lo que hice fue conectar el evento Cargado de [el contenido de] mi DataTemplate:

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

... y luego procese la fila recién mostrada en el controlador de eventos:

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

Este enfoque realiza la inicialización / comprobación de la fila cuando se muestra por primera vez, por lo que no hará todas las filas por adelantado. Si puedes vivir con eso, entonces quizás este sea el método más elegante.

La solución de Andy es una muy buena idea, pero está incompleta. Por ejemplo, se crean los primeros 5 contenedores y en el panel. La lista tiene 300 > artículos. Solicito el último contenedor, con esta lógica, ADD. Luego solicito el último índice - 1 contenedor, con este logis ¡AGREGAR! Ese es el problema. El orden de los niños dentro del panel no es válido.

Una solución para esto:

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

Para cualquiera que se pregunte acerca de esto, en el caso de Andy, quizás cambiar la VirtualizingStackPanel con un StackPanel normal sería la mejor solución aquí.

La razón por la que llamar a PrepareItemContainer en ItemContainerGenerator no funciona es que un elemento debe estar en el árbol visual para que PrepareItemContainer funcione. Con un VirtualizingStackPanel, el elemento no se establecerá como un elemento secundario visual del panel hasta que VirtualizingStackPanel determine que está / está a punto de estar en la pantalla.

Otra solución (la que uso) es crear su propio VirtualizingPanel, para que pueda controlar cuándo se agregan elementos al árbol visual.

En mi caso, descubrí que llamar a UpdateLayout () en el ItemsControl ( ListBox , ListView , etc.) inició su ItemContainerGenerator , de modo que el estado del generador cambió de " NotStarted " ItemContainerGenerator.ContainerFromItem y / o ItemContainerGenerator.ContainerFromIndex ya no devolvían los contenedores " GeneratingContainers " ;, y null .

Por ejemplo:

    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);
    }
Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top