ASP.NET MVC: Enlazar una propiedad a jQuery UI Slider

¡Hola! Un compañero me ha preguntado si era posible enlazar una propiedad (de tipo int) a un control slider de jQuery UI. La verdad es que sí que es posible y vamos a ver en este post una posible solución que de hecho es extrapolable a otras situaciones parecidas que podáis tener.

Templated helpers al rescate

En ASP.NET MVC2 introdujeron el concepto de templated helpers un mecanismo para construir la interfaz de usuario a partir del tipo de datos del modelo. Simplificando un poco, si colocamos en la carpeta DisplayTemplates y EditorTemplates una vista parcial ASP.NET MVC usará esta vista automáticamente cada vez que se use el método Html.DisplayFor o Html.EditorFor respectivamente.

Si tenemos un Modelo de tipo X que tiene una propiedad, llamémosle Foo, cuyo tipo sea BarType, si hacemos:

  1. @Html.EditorFor(x=>x.Foo)

ASP.NET MVC buscará la vista EditorTemplates/BarType (el nombre de la vista es el tipo de la propiedad usada en EditorFor).

Como esta regla del nombre de tipo puede ser demasiado genérica, también es posible usar el atributo [UIHint] indicando el nombre del template (la vista parcial) a usar para editar o mostrar los datos:

  1. public class FooModel
  2. {
  3.     [UIHint("FooEdit")]
  4.     public BarType Foo { get; set; }
  5. }

Ahora la llamada a Html.EditorFor, buscará la vista llamada FooEdit en EditorTamplates.

Vamos pues a crear el template para editar y visualizar una propiedad de tipo int usando el slider de jQuery UI.

Para ello creamos una vista parcial en Views/Shared/EditorTemplates y le damos el nombre que queramos, en mi caso slider.cshtml. Para crear un slider basta con tener un <div> y luego llamar al método slider() (doy por supuesto que jQuery UI se ha descargado y está referenciada). Así que empezaremos con este código:

  1. <script type="text/javascript">
  2.     $(document).ready(function () {
  3.         $("#slider").slider();
  4.     });
  5. </script>
  6. <div id="slider">
  7. </div>

Con esto creamos el slider pero dado que estamos en modo edición, necesitamos alguna manera para guardar el valor que el usuario seleccione. Una forma rápida de hacerlo es tener un hidden que mantenga en todo momento el valor que el usuario seleccione en el slider. Y aquí nos surge la primera duda: que valor ha de tener el atributo name para que luego ASP.NET MVC sea capaz de reconocerlo y enlazarlo a la propiedad del viewmodel? El problema es que el valor del atributo name depende del nombre de la propiedad que estamos enlazando así que hemos de recuperar este nombre… Por suerte podemos saber este nombre, usando la propiedad HtmlFieldPrefix de la propiedad TemplateInfo del ViewData. Es decir, podemos generar el campo hidden así:

  1. <input type="hidden" name="@ViewData.TemplateInfo.HtmlFieldPrefix"/>

Finalmente sólo nos queda suscrbirnos al evento change del slider y actualizar el campo hidden. Para ello deberemos añadir un id (he usado slider_hidden) al campo hidden y usar el siguiente código para crear el slider:

  1. <script type="text/javascript">
  2.     $(document).ready(function () {
  3.         var options = {
  4.             change: function (event, ui) {
  5.                 $("#slider_hidden").val(ui.value);
  6.             }     
  7.         };       
  8.         $("#slider").slider(options);
  9.     });
  10. </script>

Con eso ya podemos crear un ViewModel con una propiedad int, decorada con [UIHint(“silder”)] y observar como aparece el slider para editar la propiedad. P.ej. dado el siguiente ViewModel:

  1. public class Ratio
  2. {
  3.     public string Texto { get; set; } 
  4.     [UIHint("slider")]
  5.     public int Rating2 { get; set; }
  6. }

Una vista para editar un objeto Ratio sería tan sencilla como:

  1. @using MvcSliderBinding.Models
  2. @model Ratio
  3. @{
  4.     ViewBag.Title = "title";   
  5. }
  6. @using (Html.BeginForm())
  7. {
  8.     @Html.LabelFor(x => x.Texto)
  9.     @Html.EditorFor(x => x.Texto)
  10.     <br />
  11.     @Html.LabelFor(x => x.Rating)
  12.     @Html.EditorFor(x => x.Rating)
  13.     
  14.     <input type="submit" />
  15. }

Al usar @Html.EditorFor(x=>x.Rating), al ser Rating una propiedad decorada con UIHint(“slider”) se va a usar el editor template que hemos creado antes.

Ya tenemos enlazado una propiedad con el slider de jQuery! Ahora vamos a pulir detalles…

Preparándolo para que pueda haber más de un slider

El template de edición que hemos creado NO admite ser repetido en una misma vista, ya que usa ids estáticos para el <div> que será el slider y el hidden que contiene el valor. Si tuviéramos un viewmodel que tiene dos propiedades y quisiéramos usar dos sliders no nos funcionaría bien. Para solucionarlo nos basta con asegurar que los IDs son siempre distintos. Hay varias maneras, una es usar como id un prefijo y el valor que nos da HtmlFieldPrefix, ya que ese se supone único. Otro es usar un GUID, como se muestra a continuación:

  1. @model Nullable<int>
  2. @{
  3.     var suffix = Guid.NewGuid().ToString(); 
  4. }
  5. <script type="text/javascript">
  6.     $(document).ready(function () {
  7.         var options = {
  8.             value: @(Model.HasValue ? Model.Value : min),
  9.             change: function (event, ui) {
  10.                 $("#@suffix").val(ui.value);
  11.             }
  12.         };  
  13.         $("#slider_@(suffix)").slider(options);
  14.     });
  15. </script>
  16. <div id="slider_@(suffix)">
  17. </div>

Guardamos en suffix el GUID creado y lo añadimos al hidden y al div. Un detalle más que aprovecho para enseñaros es establecer el valor inicial del slider al valor que tenga la propiedad (que está en Model). Pero, si estamos creando el objeto el valor de Model será null. Es por ello que debo declarar que el modelo de la vista es de tipo Nullable<int>, en lugar de int, para poder aceptar esos valores nulos.

Accediendo a la información del viewmodel

Una cosa muy interesante al usar template helpers, es que nuestro template helper puede acceder a información del viewmodel que define la propiedad. Es decir, dentro del template helper, yo puedo saber cual es la propiedad que estoy editando (en mi caso es Rating) y tengo información sobre la clase que define dicha propiedad (en mi caso Ratio). A esos datos se puede acceder a través de ViewData.ModelMetadata:

image

Podeis usar las propiedades de ViewData.ModelMetadata para realizar ciertas tareas, como analizar el ViewModel en busca de atributos que os puedan ayudar a definir como renderizar el template… lo que se os ocurra.

Un ejemplo de esto, imaginad que tenemos una propiedad decorada con el atributo [Range], para indicar que acepta un rango de valores. Pues desde aquí podríais consultar dicho atributo Range (teneis acceso al Type del ViewModel y sabeis el nombre de la propiedad) y configurar el slider para que sólo acepte entradas en este rango.

Es lo que he hecho yo, salvo que en lugar de usar reflection para acceder al atributo Range, lo que he hecho es obtener los validadores que hay asociados a la propiedad y generar código acorde a ellos. Para ello uso el método GetVaidators() de ModelMetadata:

  1. @model Nullable<int>
  2. @{
  3.     var suffix = Guid.NewGuid().ToString();
  4.  
  5.     bool hasRange = false;
  6.     var rangeVal = ViewData.ModelMetadata.GetValidators(ViewContext.Controller.ControllerContext).OfType<RangeAttributeAdapter>().FirstOrDefault();
  7.     ModelClientValidationRangeRule rule = null;
  8.     var min = 0;
  9.     var max = -1;
  10.     if (rangeVal != null)
  11.     {
  12.         rule = rangeVal.GetClientValidationRules().OfType<ModelClientValidationRangeRule>().FirstOrDefault();
  13.         if (rule != null)
  14.         {
  15.             min = Convert.ToInt32(rule.ValidationParameters["min"]);
  16.             max = Convert.ToInt32(rule.ValidationParameters["max"]);
  17.         }
  18.     }
  19.  
  20. }

Llamo a GetValidatos y busco el validador de tipo RangeAttributeAdapter. Eso es lo mismo que buscar si existe un atributo [Range], salvo que es más genérico (aunque no entraremos en detalles, simplemente comentar que DataAnnotations es una manera de añadir validadores, pero pueden haber más). Si existe el validador de rango, obtengo su configuración, en concreto sus valores mínimo y máximo, y me los guardo. Con esto ahora tengo información para generar un slider que sólo acepte valores en este rango:

  1. <script type="text/javascript">
  2.     $(document).ready(function () {
  3.         var options = {
  4.          @if (rule!=null)
  5.          {
  6.             @:min: @min,
  7.             @:max: @max,
  8.          }
  9.             value: @(Model.HasValue ? Model.Value : min),
  10.             change: function (event, ui) {
  11.                 $("#@suffix").val(ui.value);
  12.             }
  13.         };       
  14.         $("#slider_@(suffix)").slider(options);
  15.     });

Listos! Ahora tenemos un editor que además respeta el validador de rango que tenga la propiedad.

Y así podríamos ir perfilando este template de edición con todo lo que necesitáramos para adaptarlo a nuestras necesidades… ¡Simple, sencillo y potentísimo!

Os dejo un proyecto VS2010 con la implementación del template de edición y uno de visualización para que podáis verlo en acción: https://skydrive.live.com/?cid=6521c259e9b1bec6&sc=documents&uc=1&id=6521C259E9B1BEC6%21167#

Espero que os haya sido útil! Un saludo!

ObservableCollection<T>, INotifyPropertyChanged y WinRT

NOTA: Este post está basado en la versión Developers Preview de Windows 8, que salió en Septiembre del 2011. Versiones posteriores pueden dejar (y con suerte dejarán) este artículo obsoleto.

Un post cortito: Si desarrollas aplicaciones Metro para Windows 8 usando C# y XAML no uses ObservableCollection<T>. Simple y llanamente no funciona.

En su lugar debe usarse IObservableVector<T> interfaz de la cual podéis encontrar una implementación aquí: http://code.msdn.microsoft.com/Data-Binding-7b1d67b5/sourcecode?fileId=44725&pathId=1428387049. Esa implementación proporciona además un método ToObservableVector para convertir una INotifyCollectionChanged (es decir una ObservableCollection<T>) en un IObservableVector<T>.

Relacionado con el tema: ojo con implementar INotifyPropertyChanged. En concreto, ojo con cual implementas pues resulta que ahora hay dos! Por un lado está el INotifyPropertyChanged de toda la vida (System.ComponentModel.INotifyPropertyChanged) y por otro uno nuevo que es el que usa WinRT: Windows.UI.Xaml.Data.INotifyPropertyChanged. Ese último es el que tenéis que utilizar en vuestros ViewModels.

Desconozco el porque de estos cambios (no usar INotifyCollectionChanged ni el INotifyPropertyChanged de toda la vida), aunque supongo que tienen que ver en no usar colecciones ni eventos propios de .NET y tenerlo todo controlado dentro del API de WinRT. También supongo que en siguientes versiones de Windows 8 eso se arreglará.

Así que si no queréis, como yo, perder un buen rato preguntándoos porque no se actualiza una ListBox… ya sabéis! 😉

Un saludo!

PD: Algunos enlaces que he encontrado buscando acerca de esto:

  1. ObservableCollection no funciona: http://social.msdn.microsoft.com/Forums/en-AU/winappswithcsharp/thread/054913c2-6ad4-4b54-a349-c7ae846d4f8e
  2. Selecciona el INotifyPropertyChanged correcto en WinRT: http://blog.galasoft.ch/archive/2011/09/25/quick-tip-select-the-correct-inotifypropertychanged-in-windows-8.aspx –> Según menciona aquí esto parece que ya está corregido y que la nueva versión de Win8 ya no tendrá ese error.

Aplicaciones "de una sola página” con HTML5 y ASP.NET MVC

Muy buenas!

Cada vez más nos encontramos con aplicaciones web que funcionan “en una sola página”, es decir que se carga la página inicial y luego todas las nuevas peticiones son via AJAX. Esas aplicaciones funcionan perfectamente hasta que el usuario le daba a atràs o a F5 para refrescar la página: en este momento se pierde el estado de la navegación.

Hasta ahora no había una manera estándard y sencilla para lidiar con esto, pero HTML5 ya está aquí y incluye una nueva API de historial que nos va ayudar con estos casos. Aunque hay más, voy a centrarme en este post en dos elementos de dicha API:

  • El evento popstate. Este evento de window se lanza cuando se navega a una dirección del historial. Bueno, sí, la definición es un poco ambigua pero básicamente traducido es que se lanza cuando se pulsa el botón de atrás en el navegador.
  • El método pushState del objeto history. Este método permite meter una entrada en el historial. Esa entrada consta de un objeto con datos arbitrarios y una url. Cuando llamemos a pushState la barra de direcciones se modificará para mostrar la nueva url, pero el navegador no navegará hacia allí. En su lugar habrá añadido una entrada ficticia en el historial con los datos que nosotros le hayamos indicado. Cuando se pulse atrás en el navegador, se lanzará el evento popstate y en él podremos recuperar esos datos y simular lo que sea que tenga que simularse para darle la sensación al usuario de que el botón de atrás funciona.

Sí, parece un poco lioso, pero en este post veremos como realizar una aplicación ASP.NET MVC que:

  1. Muestre una lista de productos y enlaces a los detalles
  2. Al pulsar en un detalle se muestre una página del producto
  3. Todo el refresco sea via ajax
  4. Al pulsar F5 el usuario se queda donde está. Es decir, si estaba viendo los detalles del producto 2 continuará viéndolos.
  5. El funcionamiento de back y forward será el esperado.
  6. En la barra de direcciones se mostrará la URL real que se está visitando, aunque esta se haya cargado via ajax.

En resumen, nuestra aplicación se va a comportar exactamente como se espera de una aplicación que no es ajax salvo que… usará ajax con todas las ventajas (menos refresco y mayor velocidad) que conlleva. Y lo mejor… no nos costará mucho hacerlo. ¡Viva HTML5!

1. La estrategia general

Tenemos que definir la estrategia general: por un lado mientras naveguemos por la aplicación todo serán peticiones ajax, pero si el usuario le da a F5, entonces ya no tendremos una petición ajax. En su lugar tendremos una petición estándard que tendremos que gestionar. Por lo tanto nuestros controladores deberán estar capacitados para servir la misma información les venga la petición por Ajax, o les venga de forma tradicional.

Para solventar esto, nos basta con meter todo el contenido en una vista parcial y devolver la vista parcial cuando la petición sea via Ajax. Cuando la petición sea tradicional entonces devolveremos una vista normal que lo único que hará será renderizar la vista parcial.

P.ej. Este es el código de la vista Home/Index.cshtml:

@{
ViewBag.Title = "Indice";
}

@{Html.RenderPartial("Index_partial");}

Simplemente renderizar Index_partial que es donde habrá el contenido. Luego nos basta un método en el controlador como el siguiente:

public ActionResult Index()
{
return Request.IsAjaxRequest() ?
(ActionResult)PartialView("Index_partial") :
(ActionResult)View();
}

Esa estrategia la repetiremos en todas las acciones de los controladores.

Bien, vayamos ahora a por las vistas. Este es el código de la vista Index_partial:

<div id="source">
<h2>Index</h2>
<a href="@Url.Action("List", "Products")" data-ajax="true" >Ver productos</a>
<br />
</div>

Tan sólo un enlace, con el atributo data-ajax=”true” y un div llamado source. El atributo data-ajax a true es importante porque es el que usaremos para interceptar este enlace y cargarlo via ajax. Por otro lado el div source también es importante porque es donde vamos a poner el contenido que nos venga de la petición ajax: es decir machacaremos todo nuestro contenido con el que nos venga de la petición ajax.

Con eso, ya tenemos la estrategia montada: Vamos a tener una vista “normal” y una parcial por cada acción y las vistas parciales serán las que realmente muestren el contenido.

2. Interceptar las llamadas a los links

Para esto vamos a valernos del atributo data-ajax que hemos creado antes. Este atributo es un atributo que me he inventado yo. En HTML5 te puedes inventar los atributos que te da la gana, siempre que empiecen por data-. Es una forma de estandarizar lo que antes hacíamos con clases CSS o bien inventándonos atributos. Pero a diferencia de usar una clase CSS (que usa algo pensado para aspecto para comportamiento) o inventarse un atributo cualquiera (que hace que el código deje de ser HTML válido) usar un atributo data-* no tiene ninguna contra-indicación. Así funcionan muchas de las características de HTML5: se han cogido muchas cosas que se hacían antes y se ha buscado una manera estándard de hacerlas!

Bueno, al tajo, vamos a crearnos un archivo myscripts.js al que vamos a meter algunas funciones, empezando por lo siguiente:

function bindLinks() {
$("a[data-ajax]").each(function () {
$(this).click(function (evt) {
evt.preventDefault();
ajaxload($(this).attr("href"), true);
});
});
}

La función bindLinks recorre todos los tags <a> que tengan el atributo data-ajax y les asigna una función gestora al evento click. Esa función gestora lo que hace es evitar que actúe el click estándard (por lo que NO navegaremos a través del link) y luego llama a ajaxload que es un método que veremos a continuación pasándole el valor del atributo href del tag <a> pulsado.

Ya tenemos todos los clicks de los enlaces interceptados. Para que esto se ejecute nos basta con asegurar que al cargar la página se llame a bindLinks. Para esto en la página de Layout añadimos:

<script type="text/javascript">
$(document).ready(function () {
bindLinks();
});
</script>

Finalmente la definición de la función ajaxload es casi trivial, ya que lo único que hace es llamar a load() de jQuery para cargar los datos y meterlos dentro del div source que hemos mencionado antes.

function ajaxload(url, add) {
$("#source").load(url, function () {
bindLinks();
});
}

El único detalle es que luego llama de nuevo a bindLinks() para volver a interceptar los clicks de los nuevos tags <a> que hayan aparecido!

3. Simular la modificación de la barra de direcciones

Vale, tenemos un enlace, que nos debería dirigir a una cierta URL, pongamos /Productos/Ver/10 pero en lugar de seguir el enlace, lo estamos cargando via ajax. Si queremos que el F5 funcione correctamente, debemos “modificar” la barra de direcciones, para que aparezca la URL “real”. Esto, que antes no se podía hacer, ahora es posible con la nueva History API de HTML5.

Cada vez que carguemos un enlace via Ajax añadiremos una entrada en el historial. Para ello usaremos el método pushState del objeto history. A este método se le pasan tres parámetros:

  1. Un objeto de estado. Es un objeto javascript cualquiera.
  2. Un título
  3. Una URL

El objeto de estado es una de las partes más importantes: cuando el usuario pulse el botón de atrás vamos a recibir el evento popstate y en este evento tendremos el objeto de estado. En este objeto pues nosotros podemos poner toda aquella información necesaria para que luego, en el evento popstate, podamos “deshacer” los cambios y darle la ilusión al usuario de que el botón de atrás funciona bien.

La URL que pasemos es la URL que se mostrará en la barra de direcciones. Pero sólo se mostrará, el navegador no navegará hacia allí. De nuevo es para engañar al usuario y hacerle creer que realmente ha ido a otra URL cuando en realidad simplemente hemos modificado el contenido de la página usando javascript (ajax).

Para modificar la barra de direcciones nos basta un pequeño añadido a la función ajaxload que teníamos antes:

function ajaxload(url, add) {
history.pushState({ uri: url }, '', url);
$("#source").load(url, function () {
bindLinks();
});
}

Cada vez que cargamos una vista via Ajax, añadimos una entrada en el historial. El primer parámetro es el objeto de estado. En este caso simplemente colocamos la URL “a la que navegamos” (simplemente porque no necesitaremos nada más). El tercer parámetro es la URL que mostrará el navegador en la barra de direcciones. Pero recordad: el navegador NO irá a esa URL (nosotros estamos cargando su contenido por ajax).

4. Soporte para back

Al modificar la barra de direcciones hemos dado soporte a F5. Porque al pulsar F5 el navegador refrescará la última entrada del historial que ahora contiene la URL que hemos puesto antes. Y recordad que nosotros antes hemos preparado los controladores para responder por Ajax y para responder a peticiones normales: por lo tanto en este punto tenemos una aplicación que se refresca totalmente por ajax… pero con soporte para F5. ¿No os parece genial?

Pues ahora vamos a añadir el soporte para el botón de atrás. Para ello nos vamos a basar en el evento popstate que se lanza cuando el navegador debe navegar a otra entrada del historial (básicamente cuando se pulsa el botón de back). En este evento recibimos el objeto de estado de la posición de historial que se está eliminando.

Vamos a modificar la página de Layout para añadir un manejador al evento popstate:

<script type="text/javascript">
$(document).ready(function () {
$(window).bind('popstate', function (evt) {
doPopstate(evt.originalEvent.state);
});
bindLinks();
});
</script>

Usamos el método bind() de jQuery para enlazar un manejador al evento popstate del objeto window y llamar a doPopstate, pasándole la propiedad state del evento (que es el objeto de estado).

La función doPopstate es nuestra y tiene el siguiente código:

function doPopstate(data) {
if (data != null) {
ajaxload(data.uri, false);
}
}

Si tenemos objeto de estado (teoricamente debemos tenerlo SIEMPRE, pero Chrome p.ej. al cargar una página por primera vez lanza un popstate sin objeto de estado, mientras que Firefox no lo lanza. Personalmente creo que es comportamiento de Firefox el correcto), nos limitamos a cargar via ajax la URL de este objeto de estado. Dado que están apilados será la URL anterior a la que estamos.

En este punto hemos tenido que añadir un parámetro a ajaxload (el booleano), para evitar que ajaxload nos añada una entrada de historial si estamos yendo hacia atrás. El nuevo código de ajaxload queda así:

function ajaxload(url, add) {
if (add) {
history.pushState({ uri: url }, '', url);
}
$("#source").load(url, function () {
bindLinks();
});
}

Por supuesto también modificamos la llamada a ajaxload dentro del manejador del evento de click de los enlaces en bindLinks(), para pasarle un true. Y con eso… hemos terminado.

Para entender exactamente que pasa he modificado ligeramente ajaxload y doPopstate para que hagan un log de lo que ocurre en un <div> de la página de Layout. Y eso es un poco más o menos lo que ocurre…

imageimage

imageimage

  1. Lanzamos la aplicación
  2. Pulsamos “Ver productos”
  3. Pulsamos “Detalle del producto 2”
  4. Pulsamos “Inicio”

Fijaos como en todo este tiempo el reloj de “Tiempo Actual” es siempre el mismo (estamos cargando via Ajax) y como la barra de direcciones va cambiando. También podéis ver el log en la parte inferior. Continuemos…

imageimage

imageimage

  1. Pulsamos “Ver productos”
  2. Pulsamos “Detalles del producto 1”
  3. Pulsamos BACK –> Fijaos en este punto en la aparición del evento popState y como recuperamos el evento del historial anterior.
  4. Pulsamos BACK de nuevo –> Otro evento popState y recuperamos la posición anterior.

De nuevo en todo este tiempo el reloj de “Tiempo actual” se ha movido: todo son refrescos Ajax. Sigamos…

imageimage

  1. Pulsamos “Ver Productos”
  2. Pulsamos F5 –> En este punto se lanza una petición “real”. Podeis ver como el reloj de “Tiempo actual” se ha modificado y el log ha desaparecido (normal se ha cargado toda la Layout de nuevo). Pero nos hemos quedado en la página donde estábamos (el listado de productos).

Bueno… os dejo el proyecto en VS2010 para que juguéis con él e investiguéis un poco como funciona el History API de HTML5.

Importante: El proyecto lo he probado con:

  1. Firefox 7.0 y funciona correctamente
  2. Chrome 15.0.874.83 y funciona correctamente
  3. Internet Explorer 9 y no funciona (no tiene soporte para History API)
  4. Opera 11.5 y funciona correctamente

Editado: Edito para informar que @wasat me ha comentado que Internet Explorer 10 soporta también History API. Para más info: http://msdn.microsoft.com/en-us/ie/hh272905.aspx#_HTML5History Gracias Jose por la información!!!!

Os dejo el enlace con el proyecto para que veais como está hecho y podais jugar con él: https://skydrive.live.com/?cid=6521c259e9b1bec6&sc=documents&uc=1&id=6521C259E9B1BEC6%21167#

Nota: También os dejo el enlace de History.js, que es un plugin de jQuery para soportar History API de forma consistente en todos los navegadores y que es mejor usar antes que meterse a hacerlo a mano: http://plugins.jquery.com/project/history-js No he mencionado el plugin antes porque el objetivo del post no era mostraros el plugin sino la nueva History API de HTML5.

Un saludo!!!!

Usar Recaptcha en ASP.NET MVC (desde cero)

Buenas! En este post vamos a ver como usar Recaptcha en ASP.NET MVC. Pero, antes que nada permitidme una aclaración: Si estás buscando integrar rápidamente Recaptcha en tu proyecto que sepas que puedes usar MvcRecaptcha o también el helper que viene en MVC3. Pero vamos a ver como hacerlo desde cero. ¿Por que? Pues simplemente porque me parece un buen ejemplo didáctico. Pero insisto: ya hay soluciones hechas, eso es sólo para ver como podríamos hacerlo desde cero

Añadir el captcha en una vista es sumamente sencillo: basta con incluir un tag <script> y dejar que él haga todo. También se puede crear usando javascript (lo que es útil si se quiere crear el captcha sólo si se cumplen ciertas condiciones en tiempo de ejecución), pero no vamos a verlo aquí (todos los detalles están en http://code.google.com/intl/ca/apis/recaptcha/docs/display.html en el apartado de “Ajax API”).

Para añadir recaptcha en nuestra página basta simplemente con añadir el siguiente código script:

<script type="text/javascript"
src="http://www.google.com/recaptcha/api/challenge?k=CLAVE_PUBLICA">
</script>

Este tag <script> renderizará el captcha en la posición donde se incluya.

Vamos a crearnos un helper que nos genere este tag script. El código es trivial:

public static class RecaptchaExtensions
{
public static IHtmlString Recaptcha(this HtmlHelper @this)
{
return Recaptcha(@this, "RecaptchaPublicKey");
}
public static IHtmlString Recaptcha(this HtmlHelper @this, string publicKeyId)
{
var publicKey = ConfigurationManager.AppSettings[publicKeyId];
return DoRecaptcha(@this, publicKey);
}

private static IHtmlString DoRecaptcha(this HtmlHelper @this, string publicKey)
{
var tagBuilder = new TagBuilder("script");
tagBuilder.Attributes.Add("type", "text/javascript");
tagBuilder.Attributes.Add("src", string.Concat("http://www.google.com/recaptcha/api/challenge?k=", publicKey));

return MvcHtmlString.Create(tagBuilder.ToString(TagRenderMode.Normal));
}
}

El método que realmente realiza el trabajo es el método privado DoRecaptcha, que usa un objeto TagBuilder para construir el tag <script>. Fijaos en que el valor de retorno de las funciones del helper es IHtmlString.

La función Recaptcha del helper recibe un parámetro que es el nombre del <appSetting> donde hay la clave pública de Recaptcha (hay una versión sin parámetreos que usa el <appSetting> cuya clave sea RecaptchaPublicKey.

Usar el helper es muy sencillo:

<div>
Necesitamos asegurarnos que eres humano. Actualmente sólo aceptamos
<i>Humanos estándar</i>:
@Html.Recaptcha()
</div>

Perfecto! Estamos listos para lo realmente interesante: Comprobar que el resultado que entra el usuario es válido.

Para ello, si consultamos la página donde se describe el proceso de verificación veremos que necesitamos 4 valores:

  1. La IP del cliente
  2. La clave privada de Recaptcha
  3. Dos valores adicionales, llamados challenge y response que nos envía recaptcha (son campos añadidos al formulario). Los nombres de los dos campos son recaptcha_challenge_field y recaptcha_response_field.

Bueno, para validar que el usuario ha dado de alta el captcha, lo podríamos hacer de muchas maneras, pero yo he escogido un filtro de acción. Eso me va a permitir decorar la acción del controlador de la siguiente manera:

[HttpPost]
[Recaptcha(Name="Captcha")]
public ActionResult Register(RegisterModel model)
{
//...
}

Si la validación con Recaptcha es errónea el flitro dejará un error en ModelState con la clave indicada en el parámetro Name (aquí el mensaje es fijo, pero por supuesto podría ser variable). El filtro lo configuraremos para que se ejecute antes de la acción, por lo que, dentro del método Register podremos usar ModelState.IsValid para preguntar si todo está correcto (incluyendo el captcha).

El uso de un filtro de acción es interesante porque elimina toda esa lógica de comprobación de la acción del controlador.

Bueno, si revisamos de nuevo la documentación de Recaptcha, vemos que debemos usar los 4 valores mencionados anteriormente y realizar un POST a la dirección http://www.google.com/recaptcha/api/verify. La respuesta de este POST nos indicará si la validación ha sido correcta (la primera línea valdrá true) o ha sido incorrecta (valdrá false). ¡Y ya está!

Para crear el filtro, derivamos de la clase ActionFilterAttribute y redefinimos el método OnActionExecuting, para que se ejecute justo ANTES de la acción del controlador:

public class RecaptchaAttribute : ActionFilterAttribute
{
public string Name { get; set; }

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.RequestContext.HttpContext.Request;
var challenge = request.Form["recaptcha_challenge_field"];
var response = request.Form["recaptcha_response_field"];
const string postUrl = "http://www.google.com/recaptcha/api/verify";
var result = PerformPost(request.UserHostAddress, challenge, response, postUrl);
if (!result)
{
filterContext.Controller.ViewData.ModelState.AddModelError
(Name ?? string.Empty, "Recaptcha incorrecto");
}
}

}

Este es el código básico: Recogemos los dos campos recaptcha_challenge_field y recaptcha_response_field, realizamos el POST y si el resultado NO es correcto añadimos un error usando el método AddModelError de ModelState.

El método PerformPost sería tal y como sigue:

private bool PerformPost(string remoteip, string challenge, string response, string postUrl)
{
var request = WebRequest.Create(postUrl);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
var stream = request.GetRequestStream();
var privateKey = ConfigurationManager.AppSettings["RecaptchaPrivateKey"];
using (var sw = new StreamWriter(stream))
{
const string data = "privatekey={0}&remoteip={1}&challenge={2}&response={3}";
sw.Write(data, privateKey, remoteip, challenge, response);
}
var recaptchaResponse = request.GetResponse();
string recaptchaData = null;
var recaptchaStream = recaptchaResponse.GetResponseStream();
if (recaptchaStream != null)
{
using (var sr = new StreamReader(recaptchaStream))
{
recaptchaData = sr.ReadToEnd();
}
return ParseResponse(recaptchaData);
}
else return false;
}

Usamos la clase WebRequest para realizar una petición POST con los campos indicados. Fijaos en la definición de la variable data que contiene las variables en el formato típico de post: nombre=valor&nombre=valor&… Luego simplemente volcamos esa variable en el stream de la request del objeto WebRequest.

Finalmente recogemos la respuesta, la guardamos toda en una cadena y la parseamos con el método ParseResponse que es tal y como sigue:

private static bool ParseResponse(string recaptchaData)
{
var reader = new StringReader(recaptchaData);
var first = reader.ReadLine();
var result = false;
if (first != null)
{
first = first.ToLowerInvariant();
bool.TryParse(first, out result);
}

return result;
}

Más simple imposible: leemos la primera línea y miramos si es true o false. Esa primera línea nos indica si ha ido bien o mal la validación del captcha.

Y listos! Por supuesto en la vista podemos usar Html.ValidationMessage para añadir el mensaje de error en caso de que la validación del captcha sea incorrecta:

@Html.ValidationMessage("Captcha")

El lugar donde coloquemos este llamada a Htm.ValidationMessage es donde aparecerá el mensaje de error en caso de que la validación del captcha sea incorrecta. Por supuesto el parámetro de ValidationMessage es la misma cadena que el valor del atributo Name del ActionFilter (en mi caso Captcha).

Nos falta ver el código de la acción del controlador, pero no tiene ningún secreto:

[HttpPost]
[Recaptcha(Name="Captcha")]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
// Creamos el usuario y lo autenticamos
}
// Si llegamos aquí hay algun error (puede ser el captcha
// puede ser cualquier otro).
return View(model);
}

Una prueba rápida nos permite ver que efectivamente si el usuario falla el captcha aparece el mensaje de error:

image

Y eso es todo!

En este post hemos visto como usar un ActionFilter para integrar la validación de Recaptcha en nuestro site de forma sencilla y fácil.

Insisto en lo que os he dicho al principio: hay soluciones ya hechas para integrar Recaptcha, pero a veces está bien ver las cosas desde cero, saber como funcionan e intentar ver como afrontarlas, no? Porque si siempre nos lo dan todo masticado… que gracia tiene?

Un saludo! 😀