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

Este es el cuarto artículo de la serie dedicada a crear una aplicación lectora de RSS, en esta parte crearemos la funcionalidad que consume los datos desde un RSS y con esa información llenamos los datos de nuestro modelo, creado en la parte 3.

Consumiendo el RSS por medio de SyndicationClient

Desde Visual Studio damos clic derecho sobre la carpeta Util y creamos una nueva clase llamada RSSHelper cuyo objetivo es abstraer toda la lógica necesaria para consumir el feed.

Esta clase debemos marcarla como estática y pública

 using RSSJuanK4Blog.Model;
using System.Threading.Tasks;

namespace RSSJuanK4Blog.Util
{
    public static class RSSHelper
    {

    }
}

Dentro de esta clase definimos un método estático que devuelva una lista de artículos, este método debe trabajar de manera asíncrona por lo que hacemos uso de async, lo cual de paso implica que devuelva un Task.

 using RSSJuanK4Blog.Model;
using System.Threading.Tasks;

namespace RSSJuanK4Blog.Util
{
    public static class RSSHelper
    {
        public static async Task< ArticleList > GetArticleListFromFeedAsync(string feedUrl)
        {

            return null;
        }
    }
}

Ahora implementaremos el método, creamos un objeto SyndicationClient y por medio de este traemos los datos del feed, por medio del feed traemos los artículos expuestos en el RSS, los cuales se exponen como ítems. Es importante notar que hay que agregar algunos namespace para poder utilizar las clases Uri y SyndicationClient:

 using RSSJuanK4Blog.Model;
using System;
using System.Threading.Tasks;
using Windows.Web.Syndication;

namespace RSSJuanK4Blog.Util
{
    public static class RSSHelper
    {
        public static async Task< ArticleList > GetArticleListFromFeedAsync(string feedUrl)
        {
            var syncClient = new SyndicationClient();
            var feed = await syncClient.RetrieveFeedAsync(new Uri(feedUrl));

            foreach (var art in feed.Items)
            {

            }

            return null;
        }
    }
}

Dentro del método creamos una lista de artículos y la llenamos dentro del foreach creando un nuevo articulo por cada ítem hallado. Este es el código para que sea fácil de entender, noten que estamos llamando algunos métodos que no hemos creado, pero los crearemos luego ;)

 public static async Task< ArticleList > GetArticleListFromFeedAsync(string feedUrl)
{
    var syncClient = new SyndicationClient();
    var feed = await syncClient.RetrieveFeedAsync(new Uri(feedUrl));
    var lista = new ArticleList();

    foreach (var art in feed.Items)
    {
        //estos métodos aún no los hemos creado, pero lo haremos más adelante
        string htmlContent = CreateContent(art.NodeValue);
        string summary = CreateSummary(art.Summary,htmlContent);
        Uri     imgUri = Find1stImageFromHtml(htmlContent);
        //Crear el articulo
        var newArt = new Article()
                    {
                        Title   = art.Title.Text,
                        Content = htmlContent,
                        Summary = summary,
                        ImgUri =  imgUri
                    };
        //Adicionarlo a la lista
        lista.Add(newArt);
    }

    return lista;
}

Esta es la misma funcionalidad del código anterior pero un poco más corta:

 public static async Task< ArticleList > GetArticleListFromFeedAsync(string feedUrl)
{
    var syncClient = new SyndicationClient();
    var lista = new ArticleList();

    var feed = await syncClient.RetrieveFeedAsync(new Uri(feedUrl));
    foreach (var art in feed.Items)
    {
        var content = CreateContent(art.NodeValue);
        lista.Add(new Article()
                    {
                        Title = art.Title.Text,
                        Content = content,
                        Summary = CreateSummary(art.Summary, content),
                        ImgUri = Find1stImageFromHtml(content)
                    });
    }

    return lista;
}

Ahora creamos los esqueletos de cada una de las funciones que hemos creado, y seguidamente las implementaremos.

 public static async Task< ArticleList >private static string CreateContent(string content)
{
    throw new NotImplementedException();
}

private static string CreateSummary(ISyndicationText syndicationText, string htmlContent)
{
    throw new NotImplementedException();
}

private static Uri Find1stImageFromHtml(string htmlContent)
{
    throw new NotImplementedException();
}

CreateContent

En este método debemos validar que el contenido reportado por el RSS sea válido, esto implica cosas como validar que no sea nulo (puede pasar, créanme), y dado que vamos a mostrar la información en formato html es conveniente extraer del contenido del articulo solo el contenido html ya que usualmente no es lo único que contiene.

Adicionalmente dejaremos abierta la puerta para adicionarle información al inicio y final de ese html lo cual puede ser útil en el futuro.

Usualmente en los feed RSS hay información adicional antes del contenido html, así que este método debe detectar dónde comienza el html y estar incluso preparado cuando en el feed no hay nada de código html.

 private static string CreateContent(SyndicationContent content)
{
    string htmlString = string.Empty;
    string preHtml = string.Empty;
    string proHtml = string.Empty;

    if (content != null)
    {
        //Hallar el primer tag html
        var htmlBeginIndex = content.IndexOf('<');
        //Recortar la cadena desde el primer tag html hallado
        if(htmlBeginIndex != -1)
            htmlString = content.Substring(htmlBeginIndex);
    }

    return preHtml + htmlString + proHtml;
}

CreateSummary

En este método extrae un resumen del contenido del RSS, muchos RSS ya reportan esta información automáticamente, lo cual es muy útil, pero muchos otros no lo hacen.

Cuando el resumen no sea reportado vamos a crearlo, y esto lo haremos con ayuda del contenido hallado con el método anterior. Bien sea que reporten o no el resumen una cosa a tener en cuenta es que este resumen puede tener tamaños variados, por lo cual conviene establecer un tamaño máximo para el resumen.

Para efectos de este tutorial el resumen no sera mostrado como html sino como texto plano, para convertir html en texto plano usamos un helper incluido en WinRT llamado HtmlUtilities, para poderlo usar debemos adicionar el namespace Windows.Data.Html.

 using Windows.Data.Html;

Así que primero validamos que exista un resumen, sino existe o no es informado procedemos a crearlo con base en el contenido, una vez tenemos el resumen convertimos el html en texto plano y finalmente recortamos el resumen para que no supere un valor máximo establecido. Dicho valor máximo lo hemos establecido por medio de una constante.

 const int MAX_ABSTRACT_LEN = 300;
private static string CreateSummary(ISyndicationText syndicationText,
    string htmlContent)
{
    string summaryText = String.Empty;

    if (syndicationText != null && !string.IsNullOrWhiteSpace(syndicationText.Text))
        summaryText = HtmlUtilities.ConvertToText(syndicationText.Text);
    else
        summaryText = HtmlUtilities.ConvertToText(htmlContent);

    summaryText = summaryText.Substring(0, Math.Min(summaryText.Length, MAX_ABSTRACT_LEN));

    return summaryText;
}

Del código anterior para muchos puede ser un poco misterioso este fragmento

 Math.Min(summaryText.Length, MAX_ABSTRACT_LEN));

Este método de la clase Math selecciona el menor de dos números lo cual es conveniente porque si tratamos de recortar una cadena por ejemplo a 300 caracteres pero la cadena no alcanza a tener esa longitud entonces reventaría una excepción, esto lo podemos controlar con un if y un par de validaciones, pero este código nos ahorra varias líneas adicionales.

Find1stImageFromHtml

De los métodos que estamos creando este posiblemente sea el único que se puede considerar complejo, la idea es analizar una cadena html para buscar los tag img y de estos extraer el atributo src, valla tarea... pero con ayuda de expresiones regulares se puede hacer fácilmente, expresiones regulares bien da para todo un curso de 40 horas, pero nos basta con saber que este código lo hace por nosotros. Lo anexo desde mi Github donde comparto este código para toda persona que le interese:

Lo único que debemos hacer es volverlo estático. Para que funcione adecuadamente hay que agregar el namespace:

 using System.Text.RegularExpressions;

Ahora que ya esta adicionado hacemos uso de el invocándolo desde nuestro método y convirtiendo el string en un Uri:

 private static Uri Find1stImageFromHtml(string htmlContent)
{
    return new Uri(ExtractFirstHtmlImage(htmlContent));
}

Hemos concluido el artículo, antes de despedirme y recordarles revisar el próximo artículo les dejo el código completo de lo que se ha implementado:

 using RSSJuanK4Blog.Model;
using System;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Windows.Data.Html;
using Windows.Web.Syndication;

namespace RSSJuanK4Blog.Util
{
    public static class RSSHelper
    {
        public static async Task< ArticleList > GetArticleListFromFeedAsync(string feedUrl)
        {
            var syncClient = new SyndicationClient();
            var lista = new ArticleList();

            var feed = await syncClient.RetrieveFeedAsync(new Uri(feedUrl));
            foreach (var art in feed.Items)
            {
                var content = CreateContent(art.NodeValue);
                lista.Add(new Article()
                            {
                                Title = art.Title.Text,
                                Content = content,
                                Summary = CreateSummary(art.Summary, content),
                                ImgUri = Find1stImageFromHtml(content)
                            });
            }

            return lista;
        }

        private static string CreateContent(string content)
        {
            string htmlString = string.Empty;
            string preHtml = string.Empty;
            string proHtml = string.Empty;

            if (content != null)
            {
                //Hallar el primer tag html
                var htmlBeginIndex = content.IndexOf('<');
                //Recortar la cadena desde el primer tag html hallado
                if (htmlBeginIndex != -1)
                    htmlString = content.Substring(htmlBeginIndex);
            }

            return preHtml + htmlString + proHtml;
        }

        const int MAX_ABSTRACT_LEN = 300;
        private static string CreateSummary(ISyndicationText syndicationText,
            string htmlContent)
        {
            string summaryText = String.Empty;

            if (syndicationText != null && !string.IsNullOrWhiteSpace(syndicationText.Text))
                summaryText = HtmlUtilities.ConvertToText(syndicationText.Text);
            else
                summaryText = HtmlUtilities.ConvertToText(htmlContent);

            summaryText = summaryText.Substring(0, Math.Min(summaryText.Length, MAX_ABSTRACT_LEN));

            return summaryText;
        }

        private static Uri Find1stImageFromHtml(string htmlContent)
        {
            return new Uri(ExtractFirstHtmlImage(htmlContent));
        }

/// <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 string ExtractFirstHtmlImage(string htmlString)
{
    string respuesta = TEMPURI;
    try
    {
        var rgx = new Regex(
            @"<img\b[^>]*?\b(?(1)src|src)\s*=\s*(?:""(?<URL>(?:\\""|[^""])*)""|'(?<URL>(?:\\'|[^'])*)')",
                        RegexOptions.IgnoreCase | RegexOptions.Multiline);

        var match = rgx.Match(htmlString);

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

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

    return respuesta;
}
  }
}

Hasta la próxima

Í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