Simple SharePoint 2010 Cross-Site Blog Roller WebPart

enero 19, 2011
By

SharePoint es una gran plataforma y ciertamente la versión 2010 trajo consigo nuevas características de estas denominadas “Enterprise Class” que sin dudas marcan la evolución de este producto. Sin embargo, creo que se han descuidado las pequeñas cosas, aquellas fuera de lo “Empresarial” tales como por ejemplo la plantilla de Blog. Comparado con WordPress y Druplal, probablemente podamos afirmar que los blogs creados con la plantilla predeterminada de SharePoint son uno de los más limitados en características. Incluso es una lástima que de la versión 2007 a la 2010 éstos no hayan evolucionado a la par de otras características sorprendentes en SharePoint 2010.

Claro que siempre “podemos programarlo” y estoy convencido que utilizando a SharePoint 2010 como base, podemos desarrollar una plantilla de Blog tan completa como la de otros productos… lo que bueno, sería útil tener de manera predeterminada aquellas características que todo producto CMS Web 2.0 hoy en día tiene que obligatoriamente soportar.

Un problema simple: Anuncios en la página principal

SharePoint brinda de manera predeterminada dos mecanismos para la gestión de los anuncios en la página principal:

  • Utilizando una lista de Anuncios
  • Utilizando las características de publicación de la versión Enterprise de SharePoint.
  • Ambos mecanismos desde mi punto de vista están limitados en algún sentido y no son una solución completa de manera predeterminada. En el caso de la lista de anuncios, la limitante se encuentra en la interfaz de usuario para la gestión de los anuncios que es muy simple y la funcionalidad incompleta (por ejemplo, pudiera quererse que los usuarios realicen comentarios en los mismos). Como contraparte, las características de publicación de la versión Enterprise, brindan herramientas de edición poderosas que permiten la aprobación por flujo de trabajo, la edición múltiple, la creación de contenido rico y la pre-visualización de los cambios, pero no existe de manera predeterminada un WebPart que liste en la página principal los “Resúmenes” de estos artículos así como alguna miniatura de imágenes que puedan aparecen en el cuerpo del anuncio o muestre éstos uno detrás del otro habilitando la paginación. Y esto sin hablar de que el sistema de etiquetas solo está disponible en la versión Enterprise de SharePoint. En otras palabras, que útil sería tener un WordPress sobre SharePoint.

    Por supuesto, estoy comparando Naranjas y Manzanas porque SharePoint realmente está concebido como ECM para la Empresa (Enterprise Content Management) y no como CMS (Content Management System) aunque evidentemente SharePoint tiene características de CMS. La buena noticia es que la plataforma es tan robusta que podemos desarrollar lo que queramos con la calidad que nos propongamos.

    En SharePoint todo es un Elemento (Item) que tiene un Tipo de Contentido (Content Type) asociado. Los tipos de contenidos pueden ser Página, Anuncio, Tarea, Evento entre muchos otros. Como los elementos en SharePoint se pueden consultar, filtrar, ordenar y manejar tanto desde un servicio web como localmente a través del modelo de objeto, nada nos impide crear estas visualizaciones que de manera predeterminada no encontramos en SharePoint.

    Anuncios en la página principal utilizando un Blog como base

    En SharePoint existe una forma clásica y declarativa para cambiar el estilo de visualización de un listado de elementos: creando un XSLT que transforme el resultado XML de la consulta realizada en HTML que represente la visualización requerida. Existen varios WebParts (tanto incluidos en alguna edición de SharePoint como desarrollados por algunos MVP consultores de SharePoint) que permiten realizar esta tarea. Sin embargo, a pesar de que casi todo el mecanismo representación (Rendering) de SharePoint está basado en XSLT, no resulta natural su uso y es poco atractivo, para no decir muy repelente, para aquellos desarrolladores que se inician en la extensión de SharePoint.

    Por suerte con la versión 2010 se incluye la posibilidad del desarrollo visual de Elementos Web (WebParts) que si bien no es el santo grial (a mi modo de ver es simplemente la inclusión de los Smart Web Parts que se tenían en SharePoint 2007) resulta una forma mucho más sencilla de desarrollar un WebPart comparado con aprender a realizar un XSLT o generar HTML a partir de un código C#. Si bien no es MVC por lo menos se puede escribir el código de presentación en HTML (dentro de un fichero ASCX) y la lógica del Modelo/Controlador todo mezclado en un fichero CSharp (Code behind).

    Como caso de estudio, vamos a desarrollar un Elemento Web Visual que pueda conectarse a un blog hospedado en un sub-sitio (o a una lista de elementos como los Anuncios) y que tenga la siguiente funcionalidad:

  • Listar un número K (5 de manera predeterminada) de elementos extraídos desde un blog o de manera general de una lista de elementos.
  • Convertir el código HTML en texto planto y truncarlo a una cantidad de caracteres fija (digamos 300) para crear automáticamente un resumen.
  • Extraer automáticamente una imagen miniatura de la primera imagen que se encuentre en el contenido del elemento.
  • En la Figura 1 podemos apreciar dos instancias del elemento web finalizado, la primera conectándose a un blog hospedado en el sub-sitio /noticias y la segunda obteniendo los anuncios de la lista “Announcements” del sitio principal.

    image
    Figura 1. Blog Roller WebPart

    Para empezar, vamos a crear un Proyecto de Elemento Web Visual utilizando las plantillas para SharePoint 2010 que vienen con el Visual Studio 2010. Como se muestra en la figura 2, el proyecto es bastante simple: una clase BlogRollerWebPart.cs que contendrá escrito en C# la lógica del elemento web, un fichero BlogRollerWebPartUserControl.ascx que tendrá código ASP.NET (HTML con C# embebido) para la creación de la presentación y por último un manejador HTTP thumb.ashx que será el encargado de hacer dinámicamente las miniaturas. De manera adicional se ha definido una clase ContentItem (modelo fuertemente tipado) que sería un envoltorio (Wrapper) del SPListItem.

    image
    Figura 2. Proyecto de Visual Studio que contiene el Web Part Visual

    Desarrollando la capa de presentación en ASP.NET

    Empezando por la capa de presentación, el código ASP.NET del fichero ASCX que presenta los elementos quedaría de la siguiente manera:

    <style type="text/css">
           .border {display:block;
                    border-bottom:dotted 1px #CCC;
                    margin-top:-.4em
           }
    </style>
    <%
        foreach (ContentItem item in Model)
        {%>
            <table border="0" cellpadding="0" cellspacing="0">
                <tr>
                    <td colspan="2">
                        <a href="<%=item.DisplayURL%>">
                            <h3><%=item.Title%></h3>
                        </a>
                    </td>
                </tr>
                <tr>
                    <td>
                        <% if (!string.IsNullOrEmpty(item.ImageURL))
                           { %>
                        <img style="margin: 5px" src="<%=item.ImageURL%>"
                             alt="Thumb"/>
                        <% } %>
                    </td>
                    <td>
                        <p><%=item.Excerpt%></p>
    
                    </td>
                </tr>
                <tr>
                    <td colspan="2">
                        <p style="font-size: .8em">
                            <span>
                                <%=item.CommentsCount%> Comentario(s)
                            </span>
                            <span>| Publicado por: <i><%=item.Author%></i>
                            el <i><%=item.PublishDate.ToString()%></i>
                            </span>
                        </p>
                    </td>
                </tr>
            </table>
            <div class="border"></div>
    <%  } %>

    Note que básicamente se itera por una colección llamada Model que contiene instancias de ContentItem generando una tabla por cada elemento. Esta tabla contendrá un título con un vínculo, una imagen (si existe) en la parte izquierda, el contenido (Resumen) en la parte derecha y en la parte inferior un resumen de la cantidad de comentarios, el autor y la fecha de publicación. La idea de utilizar una tabla es para mantener compatibilidad con Internet Explorer 6 y navegadores viejos. Realmente lo recomendado es crear divs flotantes que representen este mismo esquema. En cualquier caso creo que es mucho mas fácil y natural programar una vista utilizando código ASP.NET que realizar un XSLT.

    Note que como regla inviolable el código que se escribe en esta vista tiene que estar orientado a presentar el contenido y nada más. La forma de calcular el resumen o contar los comentarios tiene que realizarse en un fichero aparte para mantener una clara separación entre la capa lógica y la presentación.

    Escribiendo la lógica del elemento web en C#

    La lógica del elemento web se encontrará en el fichero BlogRollerWebPart.cs (note que está en una clase fuera del control) y tendrá un esquema como el que se muestra a continuación:

    [ToolboxItemAttribute(false)]
    public class BlogRollerWebPart : WebPart
    {
        // Visual Studio might automatically update this path when you change the Visual Web Part project item.
        private const string _ascxPath = @"~/_CONTROLTEMPLATES/BlogRollerWebPart/BlogRollerWebPart/BlogRollerWebPartUserControl.ascx";
    
        protected override void CreateChildControls()
        {
            BlogRollerWebPartUserControl control = Page.LoadControl(_ascxPath) as BlogRollerWebPartUserControl;
            control.Model = GetData();
            Controls.Add(control);
        }
    
        // DEFINICIÓN DE PROPIEDADES 
        //
        string _siteUrl;
        [Personalizable(PersonalizationScope.Shared),
            WebBrowsable(true),
            WebDisplayName("Site URL"),
            Category("Custom Configuration")]
        public string SiteUrl
        {
            get { return _siteUrl; }
            set { _siteUrl = value; }
        }
    
        ...
    
        public BlogRollerWebPart()
        {
            // Inicialización de valores predeterminados
            _siteUrl = SPContext.Current.Site.Url;
            ...
        }
    
        List<ContentItem> GetData()
        {
            ...
        }
    }

    El método GetData() será el encargado de realizar las respectivas consultas CAML para obtener el modelo de datos que será de manera muy simple una lista que contiene los elementos consultados. En el método CreateChildControls() es cuando se carga el control ASCX y de manera adicional se le asigna el modelo al control como tal (debe crearse una propiedad Model de tipo IEnumerable<ContentItem> en el code behind del control).

    Además habrá que definir todas las propiedades que se deseen crear para que el usuario pueda personalizar el control (por ejemplo cambiar de subsitio, de fuente de datos simplemente definir la cantidad de elementos a mostrar).

    El cuerpo del método GetData() sería el siguiente:

    List<ContentItem> GetData()
    {
        ContentItem.ThumbWidth = ThumbWidth;
        ContentItem.ThumbHeight = ThumbHeight;
        ContentItem.MaxChars = MaxChars;
    
        var Model = new List<ContentItem>();
        using (var site = new SPSite(SiteUrl))
        using (var web = site.OpenWeb(WebUrl))
        {
            var listSource = web.Lists[PostsList];
            SPQuery query = new SPQuery();
            var sbQuery = new StringBuilder();
            if (!string.IsNullOrEmpty(ApproveText))
            {
                sbQuery.AppendFormat("<Where><Eq><FieldRef Name='_ModerationStatus' /><Value Type='ModStat'>{0}</Value></Eq></Where>", ApproveText);
            }
            sbQuery.AppendFormat("<OrderBy><FieldRef Name='{0}' Ascending='False' /></OrderBy>", PublishedDateField);
            query.Query = sbQuery.ToString();
    
            int count = 0;
            foreach (SPListItem item in listSource.GetItems(query))
            {
                var _commentsCount = 0;
                if (!string.IsNullOrEmpty(PostsCommentsList))
                {
                    var commentListSource = web.Lists[PostsCommentsList];
                    SPQuery _query = new SPQuery();
                    _query.Query = string.Format("<Where><Eq><FieldRef Name='PostID' /><Value Type='Counter'>{0}</Value></Eq></Where><OrderBy><FieldRef Name='Created' Ascending='False' /></OrderBy>", item.ID);
                    _commentsCount = commentListSource.GetItems(_query).Count;
                }
    
                Model.Add(new ContentItem(item, this)
                    {
                        CommentsCount = _commentsCount
                    });
    
                count++;
                if (count >= MaxPosts)
                    break;
            }
        }
        return Model;
    }

    Note que el comportamiento de este método es muy simple: ejecutar una consulta CAML para obtener los SPListItems y envolver a estos en un ContentItem. Con esto garantizamos acceder a cualquier contenido. Las variables SiteUrl, WebUrl, MaxChars, PostsList, etc son propiedades definidas anteriormente (omitidas en el código para ahorrar espacio) que permitirán que el usuario personalice el control. La consulta CAML usted la podrá extender tanto como desee agregando nuevas propiedades de configuración y entendiendo el String Builder sbQuery o incluso puede crear una única propiedad que sea “Consulta CAML” y se quita de arriba la creación de propiedades específicas.

    No obstante, hay tres características que no aparecen en este código sino en la implementación del modelo ContentItem:

    • Creación del Resumen
    • Obtención de la URL de la Imagen en el artículo

    El modelo ContentItem

    Como dije anteriormente la clase ContentItem es un envoltorio del SPListItem. Note en el código que el constructor del SPListItem requiere pasar como parámetros el SPListItem y la instancia del Web Part que está generando los datos (para poder acceder a las propiedades del Elemento Web):

    public class ContentItem
    {
        readonly Regex imgRegex =         new Regex(@"<img[^>]+src=[""']([^>""']+)[""'][^>]*>", RegexOptions.Compiled);
        readonly Regex textRegex =         new Regex(@"<[^>]*>", RegexOptions.Compiled);
    
        public static int MaxChars = 300;
        public static int ThumbWidth = 80;
        public static int ThumbHeight = 80;
    
        SPListItem _listItem;
        BlogRollerWebPart _webPart;
        public ContentItem(SPListItem listItem, BlogRollerWebPart webPart)
        {
            _listItem = listItem;
            _webPart = webPart;
        }
    
        public string Title
        {
            get
            {
                return _listItem.Title;
            }
        }
        public string Body
        {
            get
            {
                return _listItem[_webPart.BodyField].ToString();
            }
        }
        public string Excerpt
        {
            get
            {
                var _body = textRegex.Replace(Body, "").Trim();
                if (_body.Length > MaxChars)
                {
                    _body = _body.Substring(0, MaxChars).Trim();
                    var lastSpace = _body.LastIndexOf(" ");
                    if (lastSpace < 0) lastSpace = _body.Length;
                    return _body.Substring(0, lastSpace) + "...";
                }
                else
                    return _body;
            }
        }
        public string ImageURL
        {
            get
            {
                var match = imgRegex.Match(Body);
                return match.Success ?
                string.Format("/_layouts/BlogRollerWebPart/thumb.ashx?url={0}&width={1}&height={2}",
                    match.Groups[1].Value,
                    ThumbWidth,
                    ThumbHeight):
                null;
            }
        }
        public string DisplayURL
        {
            get
            {
                return _listItem.ParentList.Forms[0].ServerRelativeUrl+"?ID="+_listItem.ID;
            }
        }
        public int CommentsCount {get;set;}
        public string Author
        {
            get
            {
                return _listItem["Author"].ToString().Split(new[] { '#' })[1];
            }
        }
        public DateTime PublishDate
        {
            get
            {
                return (DateTime)_listItem[_webPart.PublishedDateField];
            }
        }
    
    }

    La propiedad Excerpt es la encargada de conformar el resumen. La idea con el resumen es que sea solo texto (texto plano) con un tamaño pequeño (150 o 300 caracteres). Para lograr esto el acercamiento más simple resulta remover todos los tags html para dejar solo el texto (esta quizás no sea la variante más eficiente pero sí la más rápida). Para ello se utiliza la expresión regular <[^>]*> y se remueve todo lo que machee con la misma. Luego se mide el texto y si se pasa de los X caracteres pues se trunca el mismo y se le agrega “…”.

    La propiedad DisplayURL es la encargada de construir la URL de acceso al elemento. Para esto se accede a la Lista que contiene el elemento y se le pide la URL para alguna de los formularios registrados. El formulario de mostrar el elemento generalmente es el primero que aparece. Note que por eso se utiliza _listItem.ParentList.Forms[0].ServerRelativeUrl. Pero esto solo devuelve la URL relativa a la página; tenemos nosostros que pasar por parámetros el ID del elemento concreto que se quiere visualizar (el patrón ID=ListItemID en el QueryString en las páginas de acción de las listas es muy utilizado en SharePoint).

    En el caso de obtener la URL de la primera imagen que aparezca en el contenido (propiedad ImageURL), también se utiliza una expresión regular, pero en esta caso para localizar los tags <img> y del primero que aparezca tomar el valor del atributo src.

    Para obtener dinámicamente una miniatura de dicha imagen es que se utiliza el manejador HTTP thumb.ashx. Éste se utiliza con los siguientes parámetros:

    thumb.ashx?url=/relative/url/image.png&width=80&height=80

    Este manejador obtiene la imagen que se le pase como parámetro, y obtiene una miniatura, manteniendo la proporción de la imagen original pero que cuadre dentro del ancho y alto que se pase como parámetro.

    El código del manejador sería el siguiente:

    public class Thumb : IHttpHandler
    {
        public bool IsReusable { get { return false; } }
    
        public void ProcessRequest(HttpContext ctx)
        {
            var _url = ctx.Request.QueryString["url"];
            var _thumbWidth = 0;
            int.TryParse(ctx.Request.QueryString["width"], out _thumbWidth);
            _thumbWidth = _thumbWidth == 0 ? 80 : _thumbWidth;
            var _thumbHeight = 80;
            int.TryParse(ctx.Request.QueryString["height"], out _thumbHeight);
            _thumbHeight = _thumbHeight == 0 ? 80 : _thumbHeight;
    
            using (var web = SPContext.Current.Site.OpenWeb())
            {
                try
                {
                    SPFile file = web.GetFile(_url);
                    Stream stream = null;
                    try
                    {
                        stream = file.OpenBinaryStream();
                    }
                    catch
                    {
                        SPSecurity.RunWithElevatedPrivileges(delegate
                        {
                            stream = new FileStream(HostingEnvironment.MapPath(_url), FileMode.Open);
                        });
                    }
    
                    System.Drawing.Image image = System.Drawing.Image.FromStream(stream);
                    float srcWidth = image.Width;
                    float srcHeight = image.Height;
                    int thumbHeight = (int)((srcHeight / srcWidth) * _thumbWidth);
                    var bmp = new Bitmap(_thumbWidth, thumbHeight);
    
                    System.Drawing.Graphics gr = System.Drawing.Graphics.FromImage(bmp);
                    gr.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
                    gr.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
                    gr.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High;
    
                    System.Drawing.Rectangle rectDestination = new System.Drawing.Rectangle(0, 0, _thumbWidth, thumbHeight);
                    gr.DrawImage(image, rectDestination, 0, 0, srcWidth, srcHeight, GraphicsUnit.Pixel);
    
                    using (var memory = new MemoryStream())
                    {
                        bmp.Save(memory, ImageFormat.Png);
                        ctx.Response.ContentType = "image/png";
                        ctx.Response.OutputStream.Write(memory.GetBuffer(), 0, (int)memory.Length);
                    }
    
                    bmp.Dispose();
                    image.Dispose();
                    stream.Dispose();
                }
                catch (Exception err)
                {
                    throw new HttpException(404, err.Message);
                    //ctx.Response.Write(err.Message);
                }
            }
        }
    }

    La idea que se sigue consiste en tratar de obtener el contenido del fichero que contiene la imagen, ya sea utilizando la API de SharePoint web.GetFile() o si este método falla, mapeando directamente el fichero utilizando HttpHosting.MapPath(). Una vez obtenido este contenido se aplica un algoritmo de reducción y finalmente el resultado se escribe como un png.

    Note que para acceder al HIVE de SharePoint y obtener el contenido de la imagen hay que elevar los permisos de ejecución, por lo que esto pudiera representar un problema de seguridad así que se recomienda verificar la URL antes de acceder (digamos, si no tiene extensión jpg o png lanzar acceso denegado). En principio este código lanzará excepción si el contenido al que se está accediendo no es una imagen, pero nunca está de más en temas de seguridad la verificación de la verificación.

    Utilizando el Elemento Web

    Básicamente esto es todo. Para probar el control descárguese desde este articulo el código fuente (con el paquete ya compilado incluido) y comience a probar.

    Para conectarse a un blog en español hospedado en un subsitio, por ejemplo /noticias la configuración de las propiedades del elemento web sería la siguiente:

    • Site URL: http://misitiodesharepoint
    • Relative Subsite URL: /noticias
    • Content List Name: Entradas De Blog
    • Published Date Field: PublishedDate
    • Body Field: Body
    • Comentts List Name: Comentarios
    • Approved Value: Aprobado

    En el caso de conectarse a la propia lista de anuncios, utilizar esta configuración:

    • Site URL: http://misitiodesharepoint
    • Relative Subsite URL: /
    • Content List Name: Announcements
    • Published Date Field: Created
    • Body Field: Body
    • Comentts List Name:
    • Approved Value:

    Con estas configuraciones deben obtener un resultado similar al de la Figura 1.

    Si por ejemplo, quiere utilizar el modelo de publicación (plantilla de sitio de noticias) que viene con la edición Enterprise de SharePoint 2010, conéctese a la biblioteca de páginas del sitio de noticias siguiendo esta idea; las páginas en SharePoint 2010 son elementos de SharePoint que también pueden consultarse a través de una consulta CAML y se guardan en una biblioteca de documentos que es una especialización de una lista.

    Conclusiones

    En este post hemos visto como utilizar el nuevo diseñador Visual de Elementos Web que viene con el Visual Studio 2010 para de una manera más simple desarrollar estos elementos web que tanto necesitamos y que no vienen en SharePoint de manera predeterminada. La manera en que se ha desarrollado el web part de ejemplo puede servirle como patrón para futuros desarrollos ya que se respetan los fundamentos básicos de separación (Modelo – Lógica – Presentación) tan necesario para la realización de un código organizado y legible.

    Creo que el lector podrá coincidir que si bien hay todavía carencias en SharePoint los pilares fundamentales de la plataforma son lo suficientemente robustos como punto de partida para la creación de los componentes que rellenen estos vacíos.

    DESCARGAR CÓDIGO FUENTE: BlogRollerWebPart.zip

    6 Responses to Simple SharePoint 2010 Cross-Site Blog Roller WebPart

    1. Esteban on marzo 22, 2011 at 12:54 pm

      Hola, muy bueno tu trabajo y gracias por compartirlo, yo recién estoy comenzando en el mundo de Sharepoint y la verdad ando medio perdido, esta WebPart me ha servido como referencia. Pero necesito hacerte unas preguntas y agradeceré tu pronta respuesta. Necesito hacer una Webparts con la misma funcionalidad que la que publicastes en este ejemplo pero la diferencia que necesito consumir el contenido desde una página en Blanco, o sea el tipo de contenido “página” y no encuentro la forma de hacerlo ni documentación de como acceder desde VS2010 cuando estoy creando la webparts para cada tipo de contenido.

      Muchas gracias y espero tu respuesta.

      Esteban

    2. romanevzki on julio 13, 2011 at 2:17 pm

      K tal, es un buen trabajo.. Me dejó una buena experiencia..

      Saludos..

    3. Milena on agosto 29, 2011 at 4:05 pm

      Hola Alejandro

      Quisiera saber si me puedes ayudar resolviendo un error que se esta presentando cuando intento cargar la foto en my site.

      Yo ya tengo instalado my site y sincronizado pero cuando le doy editar my perfil y voy a subir mi foto me saca un error.

      Este error dice: Archivo no encontrado

    4. NEX-5N on noviembre 16, 2011 at 11:18 am

      Gracias de antemano y buena suerte! :)

    5. Maryori Felizola on marzo 6, 2012 at 12:21 pm

      Hola, muy buenas tardes. Gracias por tu artículo y a lo mejor no hablo de lo mismo pero si de algo muy parecido.
      Estoy tratanto de modificar la plantilla de Sitio de proyecto adaptandola a la página que la empresa requiere. En mucho uso los webpaerts. Pero en la parte de navegación del project site, coloco unos vinculos con parametro por ejemplo: /pwa/nombredelproyecto/paginadelwebpart.aspx?page=ss
      y funciona correctamente. El probelma ocurre cuando gravo el sitio como plantilla, este parámetro desaparece.
      Si tienes respuesta a esta duda que tengo, mucho te sabría agradecer.

      • Alejandro Tamayo
        Alejandro Tamayo on marzo 6, 2012 at 4:54 pm

        Hola, me gustaría ayudar pero la pregunta no brinda detalles técnicos. ¿Es un Webpart? ¿Cómo pasas el parámetro? ¿Filtros?

    Deja un comentario

    Tu dirección de correo electrónico no será publicada. Los campos necesarios están marcados *

    *

    Acerca del autor...

    Alejandro Tamayo

    Web: http://www.linkedin.com/in/atamayocastillo
    Alejandro Tamayo
    Professor, Researcher, Developer, Consultant and technology enthusiast. Master of Science (MSc) in Computer Science and member of Weboo Research Group.Leer completo