ASP.NET MVC – ¿Por qué usar los helpers para formularios?

Hace nada mi compi Javier Torrecilla (Little Tower para los amigos) ha escrito un post sobre los helpers de ASP.NET MVC.

En este post quiero centrarme en por qué debes usar los helpers para formularios de ASP.NET MVC. La respuesta “por qué están ahí” no vale. Hay muchas cosas que están por ahí y no deberían usarse salvo casos muy concretos (incluso cosas del .NET Framework).

Iremos como los New Kids on the Block, es decir paso a paso. Por supuesto si ya sabes todo lo que te aportan los helpers entonces este post no te aportará mucho, pero eres bienvenido a seguir leyendo por supuesto (y a comentar, claro) 🙂

Pregunta: Como recibo en un controlador los datos mandados en un <form>?

Esa es una pregunta típica que se hace cualquiera que empieza con ASP.NET MVC. Si vienes de ASP antiguo (el clásico, el de Interdev) o de otra tecnología como PHP, pues igual empiezas a buscar alguna propiedad llamada Form en la Request o algo así. ¡No busques! No lo hagas porque la encontrarás y entonces la usarás y te perderás uno de los elementos más poderosos de ASP.NET MVC: el model binder.

Para responder a esta pregunta vamos a usar esta vista:

Da de alta una cerveza:

 

<form method="POST">

    Nombre:

    <input type="text" name="Name" />

    <br />

    Categoría:

    <input type="text" name="Category" />

    <br />

    <input type="submit" value="Enviar!"/>

</form>

Todo HTML, que de moment no usamos Helpers 🙂

Veamos como no acceder a los valores de un formulario. Si vienes de PHP o Interdev probablemente habrás llegado a teclear algo como esto:

public ActionResult Index()

{

    if (Request.Form["Name"] != null && Request.Form["Category"] != null)

    {

        // Procesar alta

        return null;

    }

    else

    {

        return View();

    }

}

Parece lógico ¿no? Buscamos si los datos de formulario existen, y si exiten es que venimos del POST y los procesamos. En caso contrario es que nos han hecho un GET por lo que devolvemos la vista.

Vale… olvida este código. Así no se hacen las cosas en ASP.NET MVC. Primero la lógica del GET (mostrar una vista) y del POST (procesar una alta) están mezclados y eso no es buena señal (¿conoces el SRP)? Por suerte ASP.NET MVC nos permite separar la lógica del GET de la del POST definiendo dos métodos y decorando el que gestiona el POST con el atributo [HttpPost].

Tener la lógica separada sería un poco mejor y de hecho hay por Internet código parecido al siguiente:

public ActionResult Index()

{

    return View();

}

 

[HttpPost]

public ActionResult Index(FormCollection form)

{

    if (form["Name"] != null && form["Category"] != null)

    {

        // Procesar alta

    }

    return null;

}

La clase FormCollection que aparece es una clase propia de ASP.NET MVC que tiene la misma información que Request.Form. Este código funciona y de hecho verás código así por Internet (se ve de todo en este mundo) pero la realidad es que hay muy pocas razones para usar FormCollection hoy en día. ASP.NET MVC tiene un concepto mucho más poderoso, un amigo al que debes conocer y entender: el model binder.

Ya he hablado en este blog sobre el model binder, así que ahora solo introduciré la regla más básica: Si tu acción tiene un parámetro cuyo nombre es idéntico al de un atributo name de un campo de un formulario, el model binder enlazará el valor del campo al parámetro automáticamente.

Es decir, el código anterior nos puede quedar como:

[HttpPost]

public ActionResult Index(string name, string category)

{

    // procesar alta

    return null;

}

Esa es la regla básica del model binder. Estoy seguro que esto te parece precioso pero a la vez es posible que una inquietud atormente tu nueva felicidad: si tengo un formulario con 20 campos… se deben declarar 20 parámetros en la acción?

Por suerte el equipo de ASP.NET MVC se dio cuenta de ello y así surge la segunda (y última que veremos en este post) regla del model binder. Ojo, que ahí va: Si tu accíón recibe como parámetro, un objeto de una clase que tiene una propiedad cuyo nombre es igual al de un atributo name de un campo del formulario, el model binder enlazará automáticamente el valor de este campo a la propiedad correspondiente del objeto recibido como parámetro. Quizá te parezca un poco rebuscado, pero lo que significa es que puedo tener una clase como la que sigue:

public class BeerModel

{

    public string Name { get; set; }

    public string Category { get; set; }

}

Y ahora en mi controlador recibir los datos simplemente usando un parámetro de tipo BeerModel:

[HttpPost]

public ActionResult Index(BeerModel beer)

{

    // procesar alta

    return null;

}

El model binder se encarga de crear el objeto beer y de rellenarlo con los parámetros del formulario. ¿Genial, no?

Pregunta: Vale, pero esto que tiene que ver con los helpers?

De momento nada, pero ahora veamos que ocurre si no los ponemos. Imagina que alguien me da de alta una cerveza pero hace submit del formulario sin entrar el nombre que es obligatorio. Evidentemente yo tengo que mostrarle de nuevo la vista de dar de alta una cerveza. Una forma no muy correcta de hacerlo es la siguiente:

[HttpPost]

public ActionResult Index(BeerModel beer)

{

    if (string.IsNullOrEmpty(beer.Name))

    {

        return View(beer);

    }

    // procesar alta

    return null;

}

Si el nombre de la cerveza está vacío devuelvo la vista como resultado, pero le paso la propia cerveza. ¿Para qué? Pues porque ahora tengo que meter código para recuperar el estado de los controles. Es decir, debo rellenar el atributo value de los dos <input> con los valores que me vienen de beer, ya que si no el usuario recibiría… ¡una vista completamente vacía!. Si vienes de Webforms este es un buen punto para empezar a odiar ASP.NET MVC, pero si lo sobrepasas luego, estoy seguro, lo empezarás a amar irremediablemente.

En resumen nuestra vista ahora queda algo como así:

@model MvcApplication1.Models.BeerModel

@{

    ViewBag.Title = "Index";

    var nameValue = Model != null ? Model.Name : null;

    var categoryValue = Model != null ? Model.Category : null;

}

Da de alta una cerveza:

 

<form method="POST">

    Nombre:

    <input type="text" name="Name" value="@nameValue"/>

    <br />

    Categoría:

    <input type="text" name="Category" value="@categoryValue"/>

    <br />

    <input type="submit" value="Enviar!"/>

</form>

Aquí me estoy aprovechando de una característica de Razor que es que si igualamos un atributo de un elemento HTML a null, entonces Razor ni nos renderiza el atributo. Es decir si nameValue o categoryValue vale null, el atributo value ni aparece en el HTML generado.

Vale… ahora imagínate un formulario con 20 campos y ya puedes empezar a llorar.

Y esta es la primera razón por la que usar helpers. Si modificamos la vista para usar los helpers:

@model MvcApplication1.Models.BeerModel

@{

    ViewBag.Title = "Index";

}

Da de alta una cerveza:

 

<form method="POST">

    Nombre:

    @Html.TextBoxFor(m=>m.Name)

    <br />

    Categoría:

    @Html.TextBoxFor(m=>m.Category)

    <br />

    <input type="submit" value="Enviar!"/>

</form>

Automáticamente los controles del formulario recuperan su estado automáticamente. ¡Hey! El que venía de Webforms… sigues ahí? 😉

Pregunta: Y como muestro que campo es el que ha producido el error?

Jejejeee… muy buena pregunta. Hemos redirigido de nuevo al usuario porque se ha dejado de introducir el nombre de la cerveza, pero estaría bien que le informáramos de ello. Antes he dicho que el código del controlador no era muy correcto. Y no lo es porque no usa un objeto de ASP.NET MVC llamado ModelState. El ModelState guarda entre otras cosas todos los errores que se producen al validar los datos por parte del controlador.

Así que lo suyo es introducir el error en el ModelState cuando lo detectamos:

[HttpPost]

public ActionResult Index(BeerModel beer)

{

    if (string.IsNullOrEmpty(beer.Name))

    {

        ModelState.AddModelError("Name", "¿Donde has visto una cerveza sin nombre?");

        return View(beer);

    }

    // procesar alta

    return null;

}

El método AddModelError añade un error a la propiedad indicada (el segundo parámetro es el mensaje de error). Ahora si ejecutamos el proyecto y envías el formulario dejando el campo nombre vacío, el código HTML generado de vuelta será algo como:

<form method="POST">

    Nombre:

    <input class="input-validation-error" id="Name" name="Name" type="text" value="" />

    <br />

    Categoría:

    <input id="Category" name="Category" type="text" value="sdsd" />

    <br />

    <input type="submit" value="Enviar!"/>

</form>

Fíjate que el <input> generado para tener el nombre de la cerveza ahora incluye una clase llamada “input-validation-error”. Esta clase la añade el helper cuando ve que hay un error asociado en el ModelState.

Luego ya por CSS es cosa tuya personalizar esta clase para que haga algo útil (p. ej. mostrar el campo en rojo). Lo importante es que el helper la añade automáticamente sin que tu tengas que hacer nada.

Ahora imagínate sin los helpers… para un formulario con 20 campos, tener que consultar para cada uno si existe un error en el ModelState… buf, sería para morirse.

Y es por esto, básicamente, que debemos utilizar los helpers!

Un saludo!

PD: Ojo, hay mejores maneras de validar la entrada de datos en ASP.NET MVC. Basta decir que el código de validación no debería estar en el controlador y que ASP.NET MVC ofrece dos mecanismos built-in que son DataAnnotations y la interfaz IValidatableObject. No hemos visto nada de esto porque no es el objetivo de este post.

ASP.NET MVC: Autocompletado con enums

La verdad es que el tema de los enums y ASP.NET MVC da para hablar bastante (yo mismo hice un post hace ni mucho). Pero hace algunos días mi buen amigo y a veces rival, Marc Rubiño publicó en su blog un interesante artículo sobre como crear combos que mostrasen valores de enums.

En este post voy a mostrar una técnica parecida, pero a través de las data list, un concepto nuevo de HTML5 que como pasa muchas veces está recibiendo menos atención de la que merece. Y es que las data list nos dan una manera fácil y sencilla de tener cajas de texto que se autocompleten.

Qué son las data lists de HTML5?

Bueno, pues como ya debes deducir de su nombre las data list no es nada más que la posibilidad de definir un concepto nuevo, que es esto: una lista de datos.

Una lista de datos se usa como fuente de datos para un control que pueda tener una y lo bueno es que el viejo y a veces injustamente denostado <input type=”text”> entran en esta categoría. Honestamente no entiendo como el <select> no tiene soporte para data lists. Quiero suponer que hay alguna explicación, pero la verdad no la encuentro. Bueno, da igual… ¡al tajo!

La definición de una data list es muy simple:

<datalist id="sexo">

    <option value="N">Nada</option>

    <option value="P">Poco</option>

    <option value="M">Mucho</option>

    <option value="D">Demasiado</option>

</datalist>

Ahora en HTML5 podemos usar el atributo list de la etiqueta <input> para asociar a un <input> (p. ej. una caja de texto) una lista de valores:

<input type="text" id="txtSexo" list="sexo" placeholder="Sexo" autocomplete="on" />

Básicamente tan solo se trata de usar el atributo list con el valor del id de la data list a usar.

El atributo id es requerido por Chrome (si no se aplica al <input> Chrome ignora el atributo list). Este es el resultado en Chrome, Opera e IE10:

image

Podemos ver las diferencias entre navegadores:

  • Chrome nos muestra el value a la izquierda y la descripción a la derecha
  • Opera tan solo nos muestra el value
  • IE10 tan solo nos muestra la descripción

Sin duda el mejor mecanismo, en mi opinión, es el de Chrome, luego el de Opera y por último el de IE10. No he podido probar FF ni Safari por no tenerlos a mano en el momento de escribir el post.

Igual te sorprende que prefiera que el desplegable muestre N, P, M y D como hace Opera en lugar de Nada, Poco,… que muestra IE10. Eso es porque el valor que se debe teclear en el textbox es el de value. El valor de value es el que se almacena en el textbox, y por lo tanto es el que debe teclearse (a diferencia de la <select> que permite mostrar un valor y mandar otro el <input type=”file” /> no tiene esta opción).

Ten presente siempre que estamos usando un <input type=”text” /> por lo que el usuario puede entrar lo que quiera. No es como una <select> donde debe elegir un elemento de los que le indiquemos. Así pues los elementos de la data list son ¡más una sugerencia que una obligación!

Un ejemplo.

Veamos como podríamos usar esto con enums y MVC…

Lo primero es definirnos un enum:

    public enum Beers

    {

        Estrella_Damm,

        Voll_Damm,

        Moritz,

        Epidor,

        Mahou,

        Cruzcampo

    }

Ahora vamos a extender la clase HtmlHelper con un método de extensión nuevo que llamaremos TextBoxEnumerableFor:

    public static class HtmlHelperEnumExtensions

    {

        public static IHtmlString TextBoxEnumerableFor<TModel, TProperty>(this HtmlHelper<TModel> helper, Type enumerationType, Expression<Func<TModel, TProperty>> expression)

        {

            var httpContext = helper.ViewContext.HttpContext;

            var html = new StringBuilder();

            if (httpContext.Items[enumerationType.FullName] == null)

            {

 

                httpContext.Items.Add(enumerationType.FullName, GenerateDataList(enumerationType, html));

            }

            var textbox = helper.TextBoxFor(expression, new {list = enumerationType.FullName});

            html.AppendLine(textbox.ToString());

            return MvcHtmlString.Create(html.ToString());

        }

 

        private static string GenerateDataList(Type enumerationType, StringBuilder html)

        {

            var id = enumerationType.FullName;

            html.AppendFormat(@"<datalist id=""{0}"">", id);

            html.AppendLine();

            foreach (var item in Enum.GetNames(enumerationType))

            {

                html.AppendFormat(@"<option>{0}</option>", item.Replace(‘_’, ‘ ‘));

                html.AppendLine();

            }

            html.AppendLine("</datalist>");

 

            return id;

        }

    }

Aspectos a comentar de este código:

  1. Uso HttpContext.Items para “hacer un seguimiento” de si ya se ha creado una datalist. Esto es para no definir en el mismo HTML dos veces la misma datalist si se generan dos (o más) cajas de texto vinculadas al mismo enum.
  2. Sustituyo los guiones bajos (_) por un espacio. Eso debería tenerse en cuenta luego al recibir los datos.

Este código es muy sencillo y realmente debería sobrecargarse el método para dar más opciones (p. ej. especificar atributos html adicionales que ahora no es posible). Pero como ejemplo, creo que sirve.

Su uso es muy sencillo:

@using MvcApplication1

@using MvcApplication1.Models

@model MvcApplication1.Models.BeerModel

 

@{

    ViewBag.Title = "Index";

}

Entra el nombre de tu cerveza preferida aquí:

 

@Html.TextBoxEnumerableFor(typeof(Beers), m=>m.BeerName)

BeerName es una propiedad (string) del modelo BeerModel. Y este es el HTML generado:

Entra el nombre de tu cerveza preferida aquí:

<datalist id="MvcApplication1.Models.Beers">
<option>Estrella Damm</option>
<option>Voll Damm</option>
<option>Moritz</option>
<option>Epidor</option>
<option>Mahou</option>
<option>Cruzcampo</option>
</datalist>
<input id="BeerName" 
    list="MvcApplication1.Models.Beers" 
    name="BeerName" 
    type="text" value="" />

No generamos atributo value, en este caso dicho atributo toma el valor del propio texto del option.

Y un pantallazo de como se ve en Opera:

image

Puedes ver como se usa los elementos del enum para mostrar una sugerencia de autocompletado del textbox. Pero recuerda: el usuario puede entrar el valor que quiera.

Como digo, el código se puede mejorar mucho, pero como idea y ejemplo creo que es suficiente.

Un saludo!