viernes, 30 de mayo de 2014

Less, @imports (inline) y BOM

Trabajando con Less me he encontrado con un pequeño problema que quiero compartir en el blog.

El caso es que tengo varios ficheros .less así como también varios ficheros .css. Aunque podría utilizar los Bundle de ASP.NET.MVC para unirlos a todos en un sólo fichero (el CSS resultante de preprocesar los ficheros .less más los .css estáticos), me gustaría poco a poco ir utilizando más instrucciones Less para ir conociendo mejor la herramienta.

Incluir otros ficheros .less dentro de un fichero .less es muy sencillo, simplemente debemos importarlo y el fichero será preprocesado. Por ejemplo:

Principal.less

.principal {

  background-color: red;

}

@import "Secundario.less";

Secundario.less

.secundario {

  background-color: yellow;

}

Principal.css

.principal {

  background-color: red;

}

.secundario {

  background-color: yellow;

}

Sin embargo, si queremos incluir un fichero .css dentro de un .less tendremos que afinar un poco más en la instrucción @imports. Imaginemos que tenemos dos fichero .css cuyo contenido queremos incluir en Principal.less.

Estatico1.css

.estatico1 {

    background-color: blue;

}

Estatico2.css

.estatico2 {

    background-color: brown;

}

Principal.less

@import "Estatico1.css";
@import "Estatico2.css";

.principal {

  background-color: red;

}

@import "Secundario.less";

El resultado de Principal.css es el siguiente:

@import "Estatico1.css";

@import "Estatico2.css";

.principal {

  background-color: red;

}

.secundario {

  background-color: yellow;

}

Claramente no era el resultado esperado, nosotros queríamos incluir los ficheros Estatico1.css y Estatico2.css en la salida, no agregar la instrucción @import en CSS. En cualquier caso esto no es un problema, la documentación es clara al respecto:

“If the file has a .css extension it will be treated as CSS and the @import statement left as-is”

Con un pequeño cambio podemos incluir su contenido en la salida:

Principal.less

@import (inline) "Estatico1.css";

@import (inline) "Estatico2.css";

.principal {

  background-color: red;

}

@import "Secundario.less";

Y su salida:

.estatico1 {

  background-color: blue;

}

.estatico2 {

  background-color: brown;

}

.principal {

  background-color: red;

}

.secundario {

  background-color: yellow;

}

En este punto parece estar todo resuelto, pero… ¡Cuidado con el BOM!

Si por algún extraño motivo la codificación del texto del segundo o siguiente fichero CSS a incluir “inline” es UTF-8 con marca de BOM, entonces tendrás un problema. Fíjate que digo el segundo o siguiente, el primero da igual. Para verlo en directo guardaré el fichero Estatico2.css como UTF-8 con BOM y después veremos que pasa:

image

Aunque aparentemente el resultado final es el mismo, vemos que hay un problema en cliente:

image

Ese puntito rojo es malvado, es el BOM del fichero Estatico2.css que hace que de ahí para abajo no se aplica ninguna regla CSS :(

¿Cómo solucionar esto?

En mi caso he optado por no utilizar la instrucción @import (inline) porque no podemos garantizar que todos los ficheros importados tengan una codificación distinta de UTF-8 con BOM. Hoy por hoy descargamos ficheros .css de múltiples lugares (Nuget, Github, Bower, etc.) y no me podemos (al menos a mí no me apetece) estar vigilando su codificación. Además, teniendo los Bundle de ASP.NET MVC el tema está resuelto porque (y no me preguntes por qué pero lo agradezco), cuando ASP.NET genera el bundle SÍ elimina esa marca de BOM :)

Además, tener UTF-8 con BOM no es tan excepcional como parece, sobre todo si trabajamos con Visual Studio que agrega mágicamente el BOM a cualquier fichero UTF-8 editado http://vlasovstudio.com/fix-file-encoding/

Cabe mencionar que esto me ha pasado utilizando Web Essentials 2013 2.1
“Web Essentials uses the node-less compiler and it always uses the latest version”.

Un saludo!

domingo, 25 de mayo de 2014

Herramienta de diff y merge en Git

Una vez configurado el editor de texto predeterminado para git que se usará para solicitar el mensaje en un commit, ahora es el momento de configurar la herramienta de comparación (git difftool) y mezcla (git mergetool) de ficheros.

En mi caso he optado por KDiff3, pero la lista de herramientas para estas tareas es muy amplia. El propio SourceTree viene preparado para trabajar con las siguientes (y simplemente seleccionado la herramienta de un desplegable en Tools > Options > Diff):

Si configuramos la herramienta desde SourceTree, esta configuración no valdrá para Git Bash y será exclusiva de SourceTree. De este modo, si queremos utilizar alguna herramienta visual desde Git Bash tendremos que configurarlo por nosotros mismos.

La configuración será una u otra en función de si la ruta al fichero kdiff3.exe está o no incluida en la variable de entorno PATH.

Si kdiff3.exe está en PATH, nos bastará con ejecutar los siguientes comandos de git:

git config --global diff.tool kdiff3

git config --global merge.tool kdiff3

Si kdiff3.exe no está en PATH, la configuración es un poco más laboriosa:

git config --global diff.tool kdiff3

git config --global difftool.kdiff3.path "C:/Program Files (x86)/KDiff3/kdiff3.exe"

git config --global merge.tool kdiff3

git config --global merge.kdiff3.path "C:/Program Files (x86)/KDiff3/kdiff3.exe"

Además también será necesario configurar que la herramienta configurada no cree archivos de backup

git config --global mergetool.keepBackup false

Ni que el propio KDiff3 cree igualmente ficheros .orig de backup, más info en http://stackoverflow.com/a/1251871

En cualquier caso, los ficheros .orig parecen un serio candidato a ser ignorados con .gitignore

Un saludo!

sábado, 24 de mayo de 2014

Sublime Text 2 como editor predeterminado en Git

Si trabajas con git en Windows seguramente también trabajarás con algún cliente gráfico del estilo SourceTree o similar. Sin embargo, es muy habitual (y de hecho te da puntos de carisma) trabajar desde la línea de comandos con Git Bash.

Como ya sabrás, al hacer un commit en git es obligatorio un comentario. Por ello, el típico comando commit sería algo así:

git commit -m “Your comment goes here…”

No obstante, es posible no suministrar el comentario en el comando de git y escribirlo con nuestro editor de texto preferido, en mi caso Sublime Text. En este caso es necesario configurar git para establecer el editor de texto predeterminado. En el caso de ST lo haríamos con el siguiente comando:

git config --global core.editor "'c:/program files/sublime text 2/sublime_text.exe' -n -w"

El parámetro -n indica a ST que abra una nueva ventana y el parámetro -w hace que Git Bash pueda esperar al cierre de ST para confirmar o abortar el commit.

Por otro lado, si quieres configurar el texto predeterminado del comentario puedes crear un fichero de texto cualquiera con un contenido cualquiera y asignarlo con el comando:

git config --global commit.template "ruta a tu plantilla de comentario"

Recuerda que las rutas en las variables de configuración de git utilizan / en vez \

Como valioso tip (o como cutre-tip, como tú quieras llamarlo) tanto para confirmar como para abortar el commit, siempre cierra la nueva ventana de ST que se abrió para escribir el comentario con Close Window o su atajo Ctrl + Shift + W.

Actualización 20/05/2015

Después de trabajar un tiempo con git y aprender algo más :) he visto otros comandos que utilizarán el editor predeterminado, por ejemplo git config --global --edit. El problema está en que entonces el parámetro –w restará más que sumará (no siempre se quiere esperar al editor). De este modo he vuelto a los orígenes y he desistido de utilizar ST como editor predeterminado, me parece más sencillo fluir con VIM (editor predeterminado de Git Bash) y dejarme de líos para algo que aporta tan poco. 

Un saludo!

viernes, 16 de mayo de 2014

TempData basado en MongoDB en ASP.NET MVC

TempData es un forma útil de compartir datos en ASP.NET MVC entre distintas requests.La implementación por defecto que incorpora el framework está basada en Session y esto podría suponer problemas en escenarios cloud donde sería un cuello de botella si pretendemos escalar horizontalmente nuestra aplicación.

Haciendo honor a la promesa de que todo en ASP.NET MVC es flexible y configurable, podemos sustituir la implementación por defecto de TempData con nuestra propia solución.

Inicialmente trabajé con una implementación basada en cookies. La idea y el 85% del código está tomado del artículo de José María Aguilar en su post TempData sin variables de sesión en MVC.

El código del proveedor es el siguiente (lo pongo aquí porque incorpora algunas diferencias a raíz de los comentarios que se hicieron en el anterior post citado):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Web;
using System.Web.Mvc;
 
namespace WebApplication1
{
    public class CookieTempDataProvider : ITempDataProvider
    {
        private const string CookieName = "TempData";
 
        public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
        {
            var cookie = controllerContext.HttpContext.Request.Cookies.Get(CookieName);
            if (cookie == null)
            {
                return null;
            }
            var bytes = Convert.FromBase64String(HttpUtility.UrlDecode(cookie.Value));
            var value = Encoding.Unicode.GetString(bytes);
            return Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(value);
        }
 
        public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
        {
            if (values != null && values.Any())
            {
                var serializedData = Newtonsoft.Json.JsonConvert.SerializeObject(values);
                var bytes = Encoding.Unicode.GetBytes(serializedData);
                var value = HttpUtility.UrlEncode(Convert.ToBase64String(bytes));
                var cookie = new HttpCookie(CookieName, value) { HttpOnly = true };
                controllerContext.HttpContext.Response.Cookies.Add(cookie);
            }
            else
            {
                var cookie = controllerContext.HttpContext.Request.Cookies[CookieName];
                if (cookie != null)
                {
                    cookie.Expires = DateTime.Now.AddDays(-1);
                    controllerContext.HttpContext.Response.Cookies.Set(cookie);
                }
            }
        }
    }
}
La implementación basada en cookies está bien y funciona, pero a medida que utilizo más TempData me da cierto reparo guardar determinados datos sensibles en cookies. Es cierto que podríamos fácilmente encriptar la cookie, pero también es cierto que la cookie tiene un tamaño máximo, un usuario podría no aceptarla, etc. Para solucionar esto (y sobre todo por marear un poco) me he decidido a crear un proveedor de TempData basado en MongoDB.

¿Qué no sabes que es MongoDB? Pues aquí tienes un tutorial magnífico de Rubén Fernández que te vendrá que ni al pelo.

El proveedor de TempData sobre MongoDB es el siguiente:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.Builders;
 
namespace Mss2.Presentation.WebClient.Infrastructure
{

    public class MongoDBTempDataDocument

    {

        public MongoDBTempDataDocument()

        {

            CreatedDate = DateTime.UtcNow;

        }

 

        public DateTime CreatedDate { get; set; }

        public ObjectId Id { get; set; }

        public string Key { get; set; }

        public Guid UniqueId { get; set; }

 

        [BsonSerializer(typeof(MongoDBCustomSerializer))]

        public object Value { get; set; }

    }

 
    public class MongoDBTempDataProvider : ITempDataProvider
    {
        private readonly string _collection;
        private readonly string _connectionString;
 
        public MongoDBTempDataProvider(string connectionString, string collection)
        {
            _connectionString = connectionString;
            _collection = collection;
        }
 
        public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
        {
            var uniqueId = GetUniqueId(controllerContext);
            if (uniqueId == null)
            {
                return null;
            }
            var collection = GetCollection();
            var query = Query<TempDataDocument>.EQ(p => p.UniqueId, uniqueId);

            SortByBuilder sort = SortBy.Ascending("CreatedDate");

            MongoCursor<MongoDBTempDataDocument> cursor = collection.Find(query).SetSortOrder(sort);

            var cursor = collection.Find(query);
            if (!cursor.Any())
            {
                return null;
            }
            var tempData = new Dictionary<string, object>();

            cursor.ToList().ForEach(item =>

            {

                if (tempData.ContainsKey(item.Key))

                {

                    tempData[item.Key] = item.Value;

                }

                else

                {

                    tempData.Add(item.Key, item.Value);

                }

            });

            return tempData;
        }
 
        public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
        {
            var uniqueId = GetUniqueId(controllerContext);
            if (uniqueId == null)
            {
                return;
            }
            if (values.Any(p => p.Value.GetType().Name.Contains("AnonymousType")))
            {
                throw new ArgumentException("You cannot save a instance of anonymous type");
            }
            var collection = GetCollection();
            var query = Query<TempDataDocument>.EQ(p => p.UniqueId, uniqueId);
            collection.Remove(query);
            if (values != null && values.Any())
            {
                collection.InsertBatch(values.Select(p => new TempDataDocument()
                {
                    UniqueId = (Guid)uniqueId,
                    Key = p.Key,
                    Value = p.Value
                }));
            }
        }
 
        private MongoCollection<TempDataDocument> GetCollection()
        {
            var url = new MongoUrl(_connectionString);
            var client = new MongoClient(url);
            var server = client.GetServer();
            var database = server.GetDatabase(url.DatabaseName);
            var collection = database.GetCollection<TempDataDocument>(_collection);
            return collection;
        }
 
        private static Guid? GetUniqueId(ControllerContext controllerContext)
        {
            var cookie = controllerContext.HttpContext.Request.Cookies["UniqueId"];
            if (cookie != null)
            {
                return new Guid(cookie.Value);
            }
            return null;
        }
    }
}
Lo más reseñable es que el deserializador de MongoDB de serie falla al deserializar tipos anónimos, es por ello que se controla durante la inserción si el tipo es anónimo para lanzar una excepción, más información aquí.

Después de publicar este mismo post, el bueno de Luis Ruiz Pavón consiguió una solución al problema de no poder deserializar objetos anónimos y mejoró notoriamente el código, gracias!!
El código está en el post TempData basado en MongoDB (II) donde aparece ya todo esto resuelto y funcionado.

Después y para conseguir un valor único por usuario, yo he optado por guardar un Guid en una cookie durante el evento Application_BeginRequest, pero estoy abierto a cualquier otra recomendación para conseguir un valor único por usuario.

        private void SetUniqueIdCookie()
        {
            var cookie = Request.Cookies["UniqueId"];
            if (cookie == null)
            {
                var uniqueId = Guid.NewGuid();
                cookie = new HttpCookie("UniqueId")
                {
                    HttpOnly = true,
                    Value = uniqueId.ToString()
                };
                HttpContext.Current.Response.Cookies.Add(cookie);
            }
        }
 
        protected void Application_BeginRequest(object sender, EventArgs e)
        {
            SetUniqueIdCookie();
        }
Por cierto, aunque en el post no lo pongo por dejar el código lo más sencillo posible, en la implementación de producción tengo encriptada la cookie de valor único, así como la cookie completa que guarda TempData si opto por la implementación de TempData basada en cookies.

Y tampoco querría dejar de comentar que NO utilizo LINQ to Mongo ¿Por qué? Lee el post MongoDb y c#. Dos titanes en lucha constante de Pedro Hurtado y lo sabrás, básicamente la implementación de LINQ del driver de Mongo tiene algunas fugas importantes, toma nota!

Para ponerlo en funcionamiento, bastaría con agregar un par de settings en nuestro fichero web.config:
  <add key="MongoDB_ConnectionString" value="mongodb://192.168.1.23/aspnet"/>
  <add key="MongoDB_Collection" value="tempData"/>

Y registrar el proveedor (en mi caso estoy usando Unity como IoC container)
container.RegisterType<ITempDataProvider, MongoDBTempDataProvider>(
    new InjectionConstructor(
        ConfigurationManager.AppSettings["MongoDB_ConnectionString"],
        ConfigurationManager.AppSettings["MongoDB_Collection"]));
Un saludo!