Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 8

Mejorando la experiencia de usuario - Parte 1

Aplicación terminada? no. Funcionalidad terminada? mmm digamos que si.

La aplicación ya hace lo mínimo funcional pero hay que hacerle varios ajustes y mejoras para brindar al usuario una experiencia más agradable.

Dentro del conjunto de cosas a mejorar respecto a UX tenemos:

  • Hacer que aparezca un articulo seleccionado por defecto
  • Mejorar la apariencia del ListView
    • Disminuir el tamaño de los títulos del ListView
    • Disminuir el ancho del ListView
    • Evitar que los resúmenes de los artículos en el ListView crezcan de manera descontrolada
    • Colocar una imagen dummy en el Listview cuando no existan imágenes en el artículo
    • Colocar la imagen adecuada cuando la única imagen del RSS es el aggbug
    • Colocar una imagen dummy en el Listview cuando cuando la imagen hallada en el artículo sea demasiado pequeña
    • Mientras cargan los datos del feed da la impresión de que la App no esta haciendo nada

Hacer que aparezca un articulo seleccionado por defecto

Para lograr esta tarea debemos orquestar nuestra View y el ViewModel, en el ViewModel agregar una nueva propiedad que nos indique cual es el primer artículo encontrado y en la View hacer Binding para que el ListView tome ese valor como valor seleccionado.

Desde Visual Studio abrimos ViewModel/RssMainViewModel.cs y agregamos una nueva propiedad para cumplir nuestro objetivo,utilizando desde luego la funcionalidad de notificación de cambios.

 private Article _FirstOrDefaultArticle;
public Article FirstOrDefaultArticle
{
    get { return _FirstOrDefaultArticle; }
    set
    {
        SetProperty(ref _FirstOrDefaultArticle, value);
    }
}

Ahora en el método Initialize adicionamos esta línea para llevar rastro del primer artículo creado:

 public async Task Initialize()
{
    Articles = await RSSHelper.GetArticleListFromFeedAsync(this.FeedUrlString);
    FirstOrDefaultArticle = Articles.FirstOrDefault();
}

Desde Visual Studio abrimos el archivo View/RssMainView.xaml y en el ListView lvwBlogPosts establecemos la propiedad SelecttedItem para que haga Binding con la propiedad creada anteriormente:

 <ListView x:Name="lvwBlogPosts" Grid.ColumnSpan="2" Grid.Row="1" 
        Style="{StaticResource Lista-Posts-Style}" 
        ItemsSource="{Binding Articles}"
        ItemTemplate="{StaticResource Post-List-ItemTemplate}"
        SelectedItem="{Binding FirstOrDefaultArticle}"
        >
</ListView>

Ejecutamos nuestra App y ahora apenas cargan los artículos se actualiza el contenido cargado en el WebView.

Ahora hay que mejorar la apariencia del ListView, hay mucho por hacer y eso que hasta ahora es experiencia de usuario y no hemos ido aún a la parte gráfica.

Disminuir el tamaño de los títulos del ListView

Desde Visual Studio damos clic derecho sobre el archivo View/RssMainView.xaml y seleccionamos la opción abrir desde Blend.

Ya en Blend damos clic derecho sobre lvwBlogPosts y seleccionamos las siguientes opciones

SNAG-0001

Esto nos deja en posición de editar el template, en el panel llamado "Objects and Timeline" usualmente ubicado en la parte izquierda de la pantalla, seleccionamos el TextBlock que muestra el título el cual debe ser el que se muestra a continuación, pero cerciórate en tu caso:

SNAG-0002

 

Damos clic derecho sobre el TextBlock siguiendo la siguiente secuencia

SNAG-0004

Y en el panel de propiedades establecemos el tamaño de fuente en 18px, atención las unidades son px y no pt. Modificamos los márgenes para que todos queden en 5 con excepción del izquierdo que debe permanecer en 0, finalmente el ancho la propiedad Width la establecemos en 255.

Disminuir el ancho del ListView

Para disminuirlo debemos cambiar el tamaño de la columna, recuerda que todos los cambios los podemos hacer a nivel del editor de código o a nivel gráfico preferiblemente haciendo uso de Blend.

Desde Blend seleccionamos el Grid principal de la aplicación, en la parte superior se muestran los marcadores de tamaño de las columnas y los utilizamos para establecer la columna 1 (comenzando desde 0) en 350, desde código puedes hacerlo buscando estos tag

 <Grid.ColumnDefinitions>
    <ColumnDefinition Width="100"/>
    <ColumnDefinition Width="350"/>
    <ColumnDefinition/>
</Grid.ColumnDefinitions>

Como te darás cuenta debemos corregir algunas cosas del título, regresando a RssMainView.xaml y estando posicionado el Titulo del blog, en el panel de propiedades vas al margen y le das Reset.

 

SNAG-0005

Otra cosa que debemos modificar como consecuencia de este cambio en el ancho de columna es el titulo del articulo, justo arriba del WebView, lo seleccionamos y hacemos reset del margen.

Evitar que los resúmenes de los artículos en el ListView crezcan de manera descontrolada

El alto de cada resumen del artículo esta delimitado por la cantidad de caracteres existentes en el resumen, pero en ocasiones esto no es suficiente.

Desde Blend editamos nuevamente el ItemTemplate del ListView lvwBlogPosts y estando allí seleccionamos el TextBlock que contiene el resumen.

Editamos el Style del TextBlock y establecemos la propiedad MaxHeight en 130. Esta propiedad esta justo abajo de la propiedad Height y se hace visible desplegando con la flecha inferior de ese panel.

Colocar una imagen dummy en el Listview cuando no existan imágenes en el artículo

Algunos artículos no tienen imágenes, esto es perfectamente normal, en el caso de mi blog es porque seguramente hablan de solo código, en otros puede ser porque es una nota sencilla, un dialogo etc. Hay muchos motivos e independientemente del motivo lo cierto es que los miembros de la lista sin imagen se ven ... tristes.

Abrimos Appx.xaml y creamos el nuevo recurso, preferiblemente al inicio antes de declarar los demás estilos y templates y le asignamos el valor de la imagen que vamos a utilizar cuando un artículo determinado no tenga imagen:

 <x:String x:Key="void-img-uri" x:Name="void-img-uri">ms-appx:///Assets/img/just-code.png</x:String>

Ahora abrimos el archivo Util/RSSHelper.cs, localizamos la definición de la constante TEMPURI y a continuación agregamos un campo static readonly llamado DefaultImageUri, le asignaremos un recurso definido en Appx.xaml quedando como se ve a continuación:

 /// <summary>Default Uri</summary>
private static readonly string DefaultImageUri = (string) App.Current.Resources["void-img-uri"];

Ahora modificamos el código para que referencie este campo.

 private static readonly string DefaultImageUri = (string) App.Current.Resources["void-img-uri"];

/// <summary>
/// Extracts the first image Url from a html string
/// </summary>
/// <param name="htmlString">A string containing html code</param>
/// <returns>a string with the Url or first image in the htmlString parameter</returns>
/// <remarks>This method uses regular expressions,so using System.Text.RegularExpressions; must be addeed</remarks>
public static string ExtractFirstHtmlImage(string htmlString)
{
    string respuesta = DefaultImageUri;
    try
    {
        var rgx = new Regex(
            STR_IMGTAG_SRC_EXP,
                        RegexOptions.IgnoreCase | RegexOptions.Multiline);

        var match = rgx.Match(htmlString);

        respuesta = match.Groups["Url"].Value;

        if (respuesta == "")
            respuesta = DefaultImageUri;
    }
    catch { respuesta = DefaultImageUri; }

    return respuesta;
}

Colocar la imagen adecuada cuando la única imagen del RSS es el aggbug

Sucede que algunos motores de RSS agregan al final de cada entrada una imagen de 1x1 de color transparente para qué?

Sucede que los lectores RSS tradicionalesNo soportan Javascript, es decir no ejecutan ninguno de los scripts de la página, y resulta que estos script son utilizados en ocasiones para medir las visitas de la página y otros datos relevantes. Así que no hay muchos mecanismos de medición válidos para determinar cuando una entrada de un feed RSS ha sido visitada, menos aún cuando muchas entradas pueden aparecer en un solo request al RSS.

Una forma sencilla que se idearon en su momento para medir el numero de veces que una entrada RSS es desplegada es agregar al final de cada entrada un tag <img> referenciando una imagen de 1x1px de color transparente, llamada aggbug. De tal forma que cada llamado al aggbug que reciba el servidor implica que un articulo ha sido mostrado, por eso es común encontrar en los feed RSS un llamado a alguna URL con el texto aggbug a la cual le pasan como parámetro el PostId.

Sabiendo esto, en algunas entradas RSS en la que no existan imágenes siempre se encontrará al final una referencia a la imagen aggbug por lo que aparentemente:

  • El post no tiene imágenes
  • No se muestra ninguna imagen
  • No funciona el código creado para cuando no se encuentra imagen alguna

Pero ya sabemos que es lo que realmente sucede, así que incluimos esa validación al buscar las imágenes del HTML.

 /// <summary>Default Uri</summary>
private const string TEMPURI = "https://tempuri.org";
/// <summary>Agg bur string used in some RSS to stablish a request boundary per post</summary>
private const string AGGBUG = "aggbug";
/// <summary>Default image used when no images or invalid images are found</summary>
private static readonly string DefaultImageUri = (string)App.Current.Resources["void-img-uri"];

/// <summary>
/// Extracts the first image Url from a html string
/// </summary>
/// <param name="htmlString">A string containing html code</param>
/// <returns>a string with the Url or first image in the htmlString parameter</returns>
/// <remarks>This method uses regular expressions,so using System.Text.RegularExpressions; must be addeed</remarks>
public static string ExtractFirstHtmlImage(string htmlString)
{
    string respuesta = DefaultImageUri;
    try
    {
        var rgx = new Regex(
            STR_IMGTAG_SRC_EXP,
                        RegexOptions.IgnoreCase | RegexOptions.Multiline);

        var match = rgx.Match(htmlString);

        respuesta = match.Groups["Url"].Value;

        if (string.IsNullOrWhiteSpace(respuesta) || respuesta.Contains(AGGBUG))
            respuesta = DefaultImageUri;
    }
    catch { respuesta = DefaultImageUri; }

    return respuesta;
}

Colocar una imagen dummy en el Listview cuando la imagen hallada en el artículo sea demasiado pequeña

Similar al paso anterior ahora hay que hacer esta comparación, sin embargo si queremos saber el tamaño de la imagen la única forma de saberlo es creando un objeto Bitmap con la URL recibida, para luego si averiguar el tamaño. Esto puede tener performance penalties desde luego, pero dado que no es algo que haremos frecuentemente ni miles de veces por segundo, es algo que vamos a pasar por alto.

Lo que debemos hacer es esperar a que la imagen sea cargada para así poder saber su tamaño real, la carga de la imagen es un proceso asíncrono es decir cuando ponemos a cargar la imagen la siguiente instrucción se ejecuta de inmediato aunque la imagen no se haya cargado todavía. Si revisamos las dimensiones de la imagen y esta aún no ha sido cargada el tamaño que obtenemos es 0.

La única forma de saber en que preciso momento una imagen ha sido cargada y al mismo tiempo evitar crear más Bitmaps de los necesarios es haciendo uso del evento ImageOpened de un control Image, este último en todo caso debe crear un Bitmap así que no haría falta crear Bitmaps adicionales para resolver el problema, sin embargo....

El control Image al que debemos controlarle el evento ImageOpened esta dentro del template del ListView definido en el archivo Appx.xaml, si tratamos de adicionarle un manejador al evento opened Visual Studio nos indica que no es posible asignar manejadores de eventos en Appx,xaml, y aquí surge un dilema triple (trilema?)

  1. Hemos dejado el template en Appx.xaml y no en el archivo de nuestra View por un tema de orden, pero en Appx.xaml no se pueden poner eventos
  2. Podemos pensar en dejar el template en un archivo de recursos independiente que nada tenga que ver con Appx.xaml, pero un archivo de recursos independientes por definición no tiene code behind así que... tampoco podemos crearle allí el manejador del evento
  3. Dejarlo en el archivo del View sería un poco desordenado pero desde allí si se le pueden crear manejadores de eventos a los objetos contenidos en un template

Resultado: funcionalidad mata buenas prácticas. Debemos mover el template al archivo del View y desde allí si asignarle un manejador de eventos al evento Opened del control Image.

Desde Visual Studio abrimos el archivo Appx.xaml y buscamos el DataTemplate llamado Post-List-ItemTemplate, lo seleccionamos todo y lo cortamos. Seguidamente abrimos el archivo Views/RssMainView.xaml y en la sección de recursos pegamos le código que hemos cortado anteriormente quedando así:

 <DataTemplate x:Key="Post-List-ItemTemplate">
    <StackPanel Orientation="Horizontal">
        <Image Style="{StaticResource Image-Post-List}" 
                Source="{Binding ImgUri}"  />
        <StackPanel>
            <TextBlock TextWrapping="Wrap" 
                    Text="{Binding Title}" 
                    Style="{StaticResource Title-PostList-Style}"/>
            <TextBlock TextWrapping="Wrap" 
                        Text="{Binding Summary}" 
                        Style="{StaticResource Summary-PostList-Style}"/>
        </StackPanel>
    </StackPanel>
</DataTemplate>

A la imagen que se encuentra al inicio se le debe agregar un manejador al evento ImageOpened la cual asignaremos al ViewModel tal como se ve a continuación:

 <DataTemplate x:Key="Post-List-ItemTemplate">
    <StackPanel Orientation="Horizontal">
        <Image ImageOpened="ViewModel.ImageOpenedHandler" 
                Style="{StaticResource Image-Post-List}" 
                Source="{Binding ImgUri}"  />
        <StackPanel>
            <TextBlock TextWrapping="Wrap" 
                    Text="{Binding Title}" 
                    Style="{StaticResource Title-PostList-Style}"/>
            <TextBlock TextWrapping="Wrap" 
                        Text="{Binding Summary}" 
                        Style="{StaticResource Summary-PostList-Style}"/>
        </StackPanel>
    </StackPanel>
</DataTemplate>

Ahora desde Visual Studio abrimos el archivo ViewModel/RssMainViewModel.cs aquí crearemos el manejador para el evento Opened tal como se ve a continuación:

 private const int MIN_LEN = 15;

public void ImageOpenedHandler(object sender, RoutedEventArgs e)
{
    var tmp = (Image)sender;
    var bmp = (BitmapImage)tmp.Source;

    if (bmp.PixelHeight <= MIN_LEN || bmp.PixelWidth <= MIN_LEN)
    {
        var dummyImgUri = (string)App.Current.Resources["void-img-uri"];
        tmp.Source = new BitmapImage(new Uri(dummyImgUri));
    }
}

Básicamente revisamos que el tamaño de la imagen en alguno de los dos ejes no sea inferior que un límite, en este caso 15, si el límite es superado asignamos la imagen establecida por defecto.

Terminado, esto ya es tema solucionado! al ejecutar se vera diferente de acuerdo a la imagen seleccionada por cada uno de ustedes, en mi caso luce de la siguiente forma:

SNAG-0006

 

Mientras cargan los datos del feed da la impresión de que la App no esta haciendo nada

Hay que colocar un indicador de trabajo en segundo plano, este debe mostrarse desde el comienzo hasta cuando el ListView este completamente cargado.

En Views/RssMainView.xaml agregamos ( usando el IDE o el mecanismo que desees ) un control de tipo ProgressRing con las siguientes características:

  • Nombre: rngLoading
  • Ancho y Alto: 200
  • Centrado Vertical y Horizontalmente
  • Fila 1 y Columna 0 del Grid principal
  • Ocupando dos columnas

Espero que lo hagas utilizando estilos tal y como te he enseñado en esta serie de artículos.

Adicionalmente establecemos la propiedad IsActive en true.

Al ejecutar la aplicación podemos ver como aparece el anillo indicando que esta en un proceso de carga, ahora es necesario integrarlo con el ViewModel donde de paso incluiremos lógica para volverlo inactivo una vez el proceso de caga finalice.

Cambiamos el valor de la propiedad IsActive para hacerle Binding con la propiedad IsLoading del ViewModel.

 <ProgressRing x:Name="rngLoading" Grid.Row="1" Grid.ColumnSpan="2" 
                Style="{StaticResource rngLoading-Style}" 
                IsActive="{Binding IsLoading}"/>

En ViewModel/RssMainViewModel.cs adicionamos una propiedad notificable IsLoading inicializada en true.

 private bool _IsLoading = true;
public bool IsLoading
{
    get { return _IsLoading; }
    set
    {
        SetProperty(ref _IsLoading, value);
    }
}

Justo al final del método Initialize adicionamos código para establecer IsLoading en false.

Eso es todo, al finalizar la aplicación muestra un indicador de carga y una vez la información es cargada este desaparece.

Conclusión

Arduo trabajo, sin embargo estos detalles son los que hacen de nuestra aplicación algo verdaderamente útil así que el esfuerzo vale la pena ;)

Más adelante en esta serie de post veremos la segunda parte de las mejoras en la experiencia de usuario, donde incluiré cosas como la validación de la conexión a internet.

Mientras tanto en el próximo artículo procederemos a mejorar la interfaz gráfica puliendo algunos detalles, estoy seguro que será de su agrado.

No dejaré fuentes completos por esta vez, cualquier duda pueden preguntar.

 

Índice General

Para cumplir con el alcance establecido he decidido fraccionar el proyecto en las siguientes partes:

 

  1. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 1
    • Introducción al tutorial  
    • Let's begin
    • Indice General
  2. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 2
    • Preparando la solución
  3. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 3
    • Modelo de Datos
  4. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 4
    • Consumiendo el RSS por medio de SyndicationClient
    • CreateContent
    • CreateSummary
    • Find1stImageFromHtml
  5. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 5
    • Inicializando la Aplicación e implementado el View-Model
    • Cómo y desde donde llamar a Initialize
    • Asociando el DataContext del View
  6. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 6
    • Construyendo la UI - Parte 1
    • Esquema principal de la App
    • Creando elementos básicos
    • El titulo
    • Aplicar propiedades utilizando estilos
    • El icono
    • El artículo actual
    • La Lista de Artículos
  7. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 7
    • Vinculando la View con el ViewModel
    • El artículo actual
  8. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 8
    • Mejorando la experiencia de usuario - Parte 1
    • Hacer que aparezca un articulo seleccionado por defecto
    • Disminuir el tamaño de los títulos del ListView
    • Disminuir el ancho del ListView
    • Evitar que los resúmenes de los artículos en el ListView crezcan de manera descontrolada
    • Colocar una imagen dummy en el Listview cuando no existan imágenes en el artículo
    • Colocar la imagen adecuada cuando la única imagen del RSS es el aggbug
    • Colocar una imagen dummy en el Listview cuando la imagen hallada en el artículo sea demasiado pequeña
    • Mientras cargan los datos del feed da la impresión de que la App no esta haciendo nada
    • Conclusión
  9. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 9
    • Mejorar la apariencia de ListView
    • Mejorar la apariencia del ProgressRing
    • Mejorar la apariencia del WebView
    • Soporte para Snapped View
    • Imagen de Fondo
  10. Windows 8:Cómo crear un app lector de blogs (RSS)-Parte 10
    • Mejorando la experiencia de usuario - Parte 2
    • Detección de conexión a internet
    • Adición de la política de privacidad
    • Tareas adicionales
    • FIN DEL TUTORIAL