Configurando Elmah para ASP.NET MVC y ASP.NET Web Api

Hace unos 3 años, cuando quería registrar los errores de mi aplicación, solía hacer uso de Log4Net, librería de Apache para .NET muy popular en aquel entonces. Ahora está más de moda utilizar Elmah para este cometido, debido a una serie de características que podéis revisar en su web :)

Cuando trabajamos únicamente con ASP.NET MVC la configuración es bastante sencilla: basta con instalar desde Nuget la librería y de manera automática incluye dentro del web.config los httpModules necesario para registrar los errores:

PM> Install-Package elmah

Sin embargo, si estamos trabajando con ASP.NET Web Api, podemos comprobar que esta configuración no le afecta y es necesario algo más.

ExceptionFilterAttribute

Cuando en ASP.NET MVC ocurre un error, la aplicación lanza un evento que es manejado por Application_Error ubicado en el Global.asax, pero no ocurre lo mismo en el caso de Web Api. Una buena práctica cuando trabajamos con ASP.NET Web Api es la implementación de un filtro para manejar las excepciones no controladas de la aplicación. Para ello, basta con crear una clase que herede de ExceptionFilterAttribute y asignar la misma a la configuración global en App_Start/WebApiConfig. En este caso, nos beneficiaremos de Elmah para registrar los errores:

using System.Web;
using System.Web.Http.Filters;
using Elmah;

namespace ElmahWebApi.Filters
{
    public class ExceptionFilter : ExceptionFilterAttribute
    {
        public override void OnException(HttpActionExecutedContext context)
        {
            ErrorLog.GetDefault(HttpContext.Current).Log(new Error(context.Exception, HttpContext.Current));
        }
    }
}

Sobrescribimos el método OnException y utilizamos Elmah para registrar el error. Acto seguido, asignamos una instancia de esta clase a la configuración global:

using System.Web.Http;
using ElmahWebApi.Filters;

namespace ElmahWebApi
{
    public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            config.Routes.MapHttpRoute(
                name: "DefaultApi",
                routeTemplate: "api/{controller}/{id}",
                defaults: new { id = RouteParameter.Optional }
            );

            // Uncomment the following line of code to enable query support for actions with an IQueryable or IQueryable<T> return type.
            // To avoid processing unexpected or malicious queries, use the validation settings on QueryableAttribute to validate incoming queries.
            // For more information, visit http://go.microsoft.com/fwlink/?LinkId=279712.
            //config.EnableQuerySupport();

            // To disable tracing in your application, please comment out or remove the following line of code
            // For more information, refer to: http://www.asp.net/web-api
         
            config.EnableSystemDiagnosticsTracing();
            config.Filters.Add(new ExceptionFilter());
        }
    }
}

Para comprobar que funciona correctamente con ambas tecnologías, he modificado el controlador por defecto de ASP.NET Web Api para que lance una excepción:

        // GET api/values
        public IEnumerable<string> Get()
        {
            throw new Exception("Testing Elmah from ASP.NET Web Api!");
            return new string[] { "value1", "value2" };
        }

y he creado una nueva acción en el controlador Home de ASP.NET MVC para que haga lo propio:

        public ActionResult TestingElmah()
        {
            throw new Exception("Testing Elmah from ASP.NET MVC!");
        }

Una vez lanzados ambos errores, para acceder al registro de excepciones de Elmah basta con navegar a /elmah.axd, donde tendremos un resultado similar al siguiente:

Elmah page aspnet mvc and aspnet web api

¡Buen fin de semana!

RedirectToAction en @Ajax.BeginForm

Durante varias revisiones de código, me he dado cuenta de que en ocasiones se está evitando el uso de formularios AJAX en ASP.NET MVC (cuando todos los formularios de la aplicación lo son) por el simple hecho de que no es posible realizar una redirección al ser, evidentemente, una recarga parcial de la página. En este post voy a hablar de una posible solución a este caso:

El controlador

En el lado del controlador, normalmente cuando queremos redirigir al usuario a otra acción lo hacemos de la siguiente manera:

using System.Web.Mvc;

namespace AjaxRedirection.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        public ActionResult GoToTheDarkSide()
        {
            return RedirectToAction("DarkSide");
        }

        public ActionResult DarkSide()
        {
            return View();
        }
    }
}

Obviamente, en el caso de una petición AJAX, esta redirección es necesaria gestionarla de otra forma, puesto que lo más que podemos obtener es que toda la página se incluya dentro del fragmento que hayamos elegido para la carga parcial :P.

Para poder manejar el escenario que esperamos, una posible solución sería la siguiente:

        [HttpPost]
        public ActionResult GoToTheDarkSideAjax()
        {
            Thread.Sleep(3000);
            return Json(new { NewUrl = @Url.Action("DarkSide") });
        }

Al ser un ejemplo muy muy simple :) he puesto un Thread.Sleep(3000); para que el servidor espere 3 segundos antes de indicar que necesita una redirección como resultado. Básicamente, lo que hacemos en este caso es devolver un objeto JSON con una propiedad llamada NewUrl que indicará al cliente a dónde debe dirigirse una vez terminada la petición.

Redirección en el cliente

Una vez que hemos hecho la petición AJAX, la forma para controlar si debemos redirigirnos o no podría ser la siguiente:

$(document).ajaxComplete(function (event, xhr, settings) {

    var jsonResult = JSON.parse(xhr.responseText);

    if (jsonResult != undefined && jsonResult.NewUrl)
        window.location = jsonResult.NewUrl;

});

En este ejemplo he asumido que este comportamiento debe manejarse a nivel global en la aplicación. Para ello he utilizado .ajaxComplete para que todas las peticiones ajax a través de JQuery sean controladas una vez hayan sido completadas. Cuando esto ocurre, comprobamos que dicha petición ha retornado un objeto JSON y que además contenga la propiedad NewUrl. En caso afirmativo lo único que tenemos que hacer es utilizar window.location para dirigirnos al lugar indicado por esta propiedad.

Adjunto el proyecto por si fuera de utilidad :)

¡Saludos!

Enviar imágenes desde HTML 5 canvas a servidor

Hace prácticamente un mes que no he tenido tiempo de pararme a escribir y creo que vuelvo con un tema que me parece muy interesante y potente :) A día de hoy, cuando tenemos que trabajar con imágenes, las cuales necesitaban un posterior tratamiento en cuanto a tamaño, filtros, etcétera, hacíamos uso de Flash o del lado de servidor para hacer este tratamiento. Esto suponía que si la imagen original ocupaba 2MB teníamos que hacer esa subida para poder tratarla a posteriori y desechar (o no) la imagen original.

En este post os quiero mostrar cómo es posible hacer uso de varias APIs de HTML 5 que nos van a hacer la vida más fácil en este aspecto.

¿Cuál es el objetivo?

El objetivo real siempre ha sido el mismo: almacenar en servidor el resultado final de una imagen que yo proporciono sin tener que subir nada más. Para conseguirlo, haremos el tratamiento que necesitemos a nuestra imagen en el lado del cliente y sólo cuando esté lista enviaremos la petición al servidor con el resultado final. Además podemos tener dos aproximaciones: que dicho envío se tenga que realizar de forma síncrona o asíncrona, por lo que veremos los dos :D

¿Por dónde empiezo?

En primer lugar, he creado el siguiente código HTML:

<input type="file" id="pic" name="pic" />
<canvas id="preview"></canvas>
<div id="actions">
    <form action="/Home/UploadImage" method="POST">
        <input type="text" id="imageName" name="imageName" />
        <input type="hidden" id="contentType" name="contentType" />
        <input type="hidden" id="imageData" name="imageData" />
        <input id="btnSave" type="submit" value="Save" />
    </form>

</div>
<div id="result"></div>
<script src="~/App/boot.js"></script>

El primero de los input lo vamos a utilizar para recuperar el archivo que queremos subir. A través del canvas mostraremos la imagen con los cambios realizados antes de enviar al servidor. Por último, en el caso de que necesitemos hacer una petición síncrona se ha creado un formulario con la acción donde se va a gestionar la información y una serie de campos que completaremos desde el lado de JavaScript.

Código JavaScript

En primer lugar, estoy haciendo uso de una pequeña función para reducir el código cada vez que solicito un elemento del DOM (manías mías :))

function id(elementId) {
    return document.getElementById(elementId);
}

Cuando la página esté lista voy a configurar los siguientes valores:

window.onload = function () {

    //constants
    var MAX_WIDHT = 200,
        MAX_HEIGHT = 200;

    var URL = window.webkitURL || window.URL;

    var inputFile = id('pic');

Con ellos sabré cuál es mi tamaño máximo para mi imagen, con el objetivo de redimensionarla, utilizaré window.URL para crear URLs de objetos y recuperaré el elemento input de tipo file al que agregaré el siguiente manejador:

    inputFile.addEventListener('change', function (event) {

        var file = event.target.files[0];

        //elements
        var canvas = id('preview'),
            ctx = canvas.getContext('2d'),
            url = URL.createObjectURL(file);

        var img = new Image();

        img.onload = function () {

            var width = img.width,
                height = img.height;

            //resize
            if (width > height) {
                if (width > MAX_WIDHT) {
                    height *= MAX_WIDHT / width;
                    width = MAX_WIDHT;
                }
                else {
                    if (height > MAX_HEIGHT) {
                        width *= MAX_HEIGHT / height;
                        height = MAX_HEIGHT;
                    }
                }
            }

            canvas.width = width;
            canvas.height = height;

            ctx.drawImage(img, 0, 0, width, height);

            //Form data (POST)

            console.log(file);

            //name
            var imageName = id('imageName');
            imageName.value = file.name;

            //contentType
            var contentType = id('contentType');
            contentType.value = file.type;

            //image data
            var imageData = id('imageData'),
                dataUrl = id('preview').toDataURL('image/png').replace('data:image/png;base64,', '');

            imageData.value = dataUrl;

            });

        };
        
        img.src = url;
        
    });

};

Cuando selecciono un archivo recupero el mismo a través de event.target.files[0], recupero el contexto del canvas y creo una URL de mi archivo con la que poder trabajar. A partir de este momento voy a utilizar un objeto Image para redimensionar su tamaño al máximo que he establecido. Una vez tenga las medidas, se las asociaré al elemento canvas para que se ajuste al mismo y llamaré al método drawImage con esos valores. Llegados a este punto tendré la vista preliminar de mi imagen :) Por último, para el método síncrono, basta con asociar los valores obtenidos a los elementos de tipo hidden que tengo en mi formulario, con el objetivo de que sean enviados cuando haga clic en el botón submit incluido en él. Si os fijáis, la imagen se manda en base 64 gracias al método toDataURL que nos ofrece canvas.

¿Cómo recupero esta información en servidor?

En este ejemplo estoy utilizando ASP.NET MVC en la parte de Back end. Por ello, el controlador Home tiene el siguiente aspecto:

using System;
using System.Web.Mvc;

namespace CanvasToServer.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult UploadImage(string imageName, string contentType, string imageData)
        {
            byte[] data = Convert.FromBase64String(imageData);

            return File(data, contentType, imageName);
        }
    }
}

En la acción UploadImage tengo como argumentos los valores con el mismo nombre que los hidden asociados en mi formulario HTML. Como conozco cuál es el formato en que llega la información, basta con convertir de base 64 a un array de bytes y con él realizar las operaciones oportunas. En mi caso, lo único que hago es devolver de nuevo al cliente la imagen que acabo de subir transformada en forma de archivo, con el objetivo de poder comprobar que se ha generado correctamente.

Subida de la imagen en canvas a través de AJAX

En el caso de que necesite hacer la petición a través de AJAX, basta con reemplazar el siguiente código por el que le sigue:

            //Form data (POST)

            //console.log(file);

            ////name
            //var imageName = id('imageName');
            //imageName.value = file.name;

            ////contentType
            //var contentType = id('contentType');
            //contentType.value = file.type;

            ////image data
            //var imageData = id('imageData'),
            var dataUrl = id('preview').toDataURL('image/png').replace('data:image/png;base64,', '');

            //imageData.value = dataUrl;


            //(AJAX)
            id('btnSave').addEventListener('click', function (e) {

                e.preventDefault();

                console.log("AJAX way");
                var formData = new FormData();

                formData.append('imageName', file.name);
                formData.append('contentType', file.type);
                formData.append('imageData', dataUrl);

                $.ajax({
                    type: 'POST',
                    url: '/Home/UploadImage',
                    data: formData,
                    processData: false,
                    contentType: false,
                    success: function (result) {
                        var imgElement = document.createElement('img');
                        imgElement.src = 'data:image/png;base64,' + result;

                        id('result').appendChild(imgElement);
                    }
                });

            });
En este caso tampoco sería necesario el formulario y sus hidden, ya que usaremos FormData para enviar la información.

En la segunda opción, manejamos el evento click del botón submit y creamos un objeto de tipo FormData. Este objeto se trata de una mejora de XMLHttpRequest Level 2 donde podemos crear una colección de elementos clave valor que representarán los campos y valores de un formulario. Utiliza el mismo formato que si usáramos “multipart/form-data”. Más información.
La información es la misma que almacenábamos en los campos hidden del formulario. En la petición AJAX utilizamos la misma para el campo data, indicamos la URL de la acción que procesará la petición y por último debemos indicar las opciones processData: false y contentType: false. Si la petición resulta satisfactoria, lo que nos va a devolver es la misma imagen que hemos subido y la inyectaremos en el DOM (únicamente nos sirve como test, ya que no tiene mucho sentido ;)).

En el lado del servidor he modificado la acción para que sepa diferenciar las peticiones AJAX y devolver en este caso lo mismo que le llegó por parámetro:

using System;
using System.Web.Mvc;

namespace CanvasToServer.Controllers
{
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            return View();
        }

        [HttpPost]
        public ActionResult UploadImage(string imageName, string contentType, string imageData)
        {
            byte[] data = Convert.FromBase64String(imageData);

            if (Request.IsAjaxRequest())
                return Content(imageData); //useless - only for testing

            return File(data, contentType, imageName);
        }
    }
}

Espero que haya sido de utilidad :)
Proyecto de ejemplo.

¡Saludos!