Workaround para Windows Phone ListBox/LongListSelector y ScrollIntoView() cuando los elementos tienen diferente tamaño

En Windows Phone 7.5 hay un bug bastante asqueroso que sólo se presenta cuando quieres mostrar un elemento en un ListBox o en un LongListSelector llamando a ScrollIntoView() y los elementos contenidos tienen diferentes tamaños. El bug también está presente en Windows Phone 8, pero sólo en el componente ListBox. LongListSelector está construido de otra forma y, según Microsoft, ya no presenta el problema.

Como es habitual en la empresa, y aparte de ser un problema conocido desde hace tiempo, no se va a solucionar. Personalmente podría entender que no se haga en las versiones actuales ya que requeriría que luego los fabricantes reconstruyeran sus propias plataformas, pero me resulta completamente inaceptable que siga estando en el nuevo Windows Phone 8, y encima conociéndolo de antemano.

En fin, es lo que hay, y encima tampoco existe ningún workaround oficialmente sancionado para resolver el problema. En otras palabras, si tienes un programa en el que te ocurre eso, básicamente te jodes y bailas.

Pero no, aquí está RFOG que te va a sacar las castañas del fuego. Como sabéis llevo unos meses currando en Yuilop para Windows Phone, y también debéis saber que me toca mucho las narices que te digan eso no se puede hacer y similares. Y por supuesto, a tu jefe tampoco le agrada mucho que le digas: es un bug y no tiene solución.

Para descargo propio he de deciros que la solución es un poco chorra y que sólo había que pensar un poco, pero tenéis que reconocerme que debo de darme algo de bombo y platillo.

***

Bueno, ahora la parte técnica.

En el ejemplo que anexo más abajo, cuando se pulsa el botón de Insert… se genera un elemento de tres posibles mediante Code Behind. Los tres tienen como elemento base un Grid, pero el interior es diferente y por tanto su altura. Dos de ellos contienen una imagen y el tercero, un texto.

En cada elemento creado, se llama a ScrollIntoView() sobre el citado, para que se visualice adecuadamente.

Conforme vamos creando elementos, llega un momento en el que uno de ellos se queda cortado a medias como en la captura:

clip_image001

El comportamiento adecuado debería ser que se mostrara completo, mostrando la parte inferior. Ese es el problema: cuando los elementos tienen diferente altura, termina ocurriendo esto mismo. Y según Microsoft, no se va a solucionar. Las apuestas suben cuando hay campos de edición y cambios de orientación: con total seguridad te quedará el elemento cortado.

El código que genera los elementos es:

        private void Button_Tap(object sender, GestureEventArgs e)
        {
            switch (s_rnd.Next(0,3))
            {
                case 0:
                    var itemText = new ListBoxItemText("Hola mundo");
                    ListBox.Items.Add(itemText); 
                    ListBox.ScrollIntoView(itemText);
                    break;
                case 1:
                    var itemImgBien = new ListBoxItemImage(StaticResources.ImageBien);
                    ListBox.Items.Add(itemImgBien);
                    ListBox.ScrollIntoView(itemImgBien);
                    break;
                case 2:
                    var itemImgMal = new ListBoxItemImage(StaticResources.ImageMal);
                    ListBox.Items.Add(itemImgMal);
                    ListBox.ScrollIntoView(itemImgMal);
                    break;
            }
        }

Fijaos en el detalle. Dependiendo de un número aleatorio creamos uno de tres. Eso debería funcionar bien, pero como podéis comprobar vosotros en vuestra casa, no lo hace en el momento en el que empiezan a aparecer elementos con el texto y con una de las dos imágenes.

***

Como ya os he dicho, la solución es bastante sencilla. Os pego el código:

        private void ButtonNoBug_Tap(object sender, GestureEventArgs e)
        {
            switch (s_rnd.Next(0, 3))
            {
                case 0:
                    var itemText = new ListBoxItemText("Hola mundo");
                    ListBox.Items.Add(itemText);
                    ForceScrollListBox();
                    break;
                case 1:
                    var itemImgBien = new ListBoxItemImage(StaticResources.ImageBien);
                    ListBox.Items.Add(itemImgBien);
                    ForceScrollListBox();
                    break;
                case 2:
                    var itemImgMal = new ListBoxItemImage(StaticResources.ImageMal);
                    ListBox.Items.Add(itemImgMal);
                    ForceScrollListBox();
                    break;
            }
        }

Y el truco del almendruco:

        private void ForceScrollListBox()
        {
            var fake = new Grid();
            ListBox.Items.Add(fake);
            ListBox.ScrollIntoView(fake);
        }

Sí, señores. El truco consiste en crear un nuevo elemento del mismo tipo base pero completamente vacío, añadirlo y llamar a ScrollIntoView() sobre él. El uso de memoria es mínimo porque el componente no debe ocupar muchos bytes ya que está vacío, y tampoco se visualiza, pero en general el sistema empuja hacia arriba al elemento que queremos mostrar.

No obstante, creo haber detectado que no siempre funciona, al menos en el emulador, pero de momento la gente de QA de Yuliop todavía no me ha protestado. [Aplausos de fondo].

En el ejemplo tenéis tres botones. En el primero hacemos inserciones con el bug, en el segundo sin él y con el tercero corregimos si tenemos un elemento a medio mostrar o nos vamos abajo del todo si estamos a medias.

Finalmente, el código fuente de todo esto está aquí:

https://github.com/rfog/InsertInListboxproblem_Solution

Sed buenos y hasta otra.