ASP.NET Mvc Delete Link usando DELETE method

Realmente he llegado tarde (seguro que estaba haciendo alguna que otra mala cosa) al MVC de ASP.Net, lo digo porque aunque había jugueteado un poco no me había tenido que poner a hacer cosas un poco serias.

Bueno, la verdad es que estoy encantado ya voy haciendo mis pinitos poco a poco … una de las cosas con las que estaba más a disgusto ha sido el tema de hacer las eliminaciones vía POST (cosas de los que hemos jugado con Ruby)

Sthephen Walter (autor de ASP.NET MVC Framework Unleashed) tiene un post en su blog con una muy buena descripción del problema en su blog.  Resumo.

  • Realizar un borrado mediante HTTP GET es un error de seguridad como la copa de un pino.
  • Realizar un borrado usando Ajax y HTTP DELETE es dependiente del JavaScript.
  • Realizar un borrado usando HTTP POST, según Sthephen es la manera más idónea para no ser dependiente de JavaScript.
  • Problema, un link no puede hacer un HTTP POST
  • Tenemos que usar una imagen o un botó
  • Propone una solución para realizar la eliminación mediante AJAX y java script (demasiado dependiente)

Mi problema (y seguro que el de otros) – Requisitos

  • Quiero realizar un borrado
  • Quiero hacerlo usando un link
  • Quiero que sea seguro
  • Quiero que use HTTP DELETE
  • Quiero poder validar “¿Esta Ud. Seguro?”
  • Quiero poder personalizar el mensaje
  • Quiero que tras borrar pueda dirigirme a una página concreta
  • Quiero controlar los errores
  • Quiero que sea versátil y no tener que escribir JavaScript cada vez

Extendiendo jQuery

Ya está más que dicho, pero jQuery es grande, muy grande. Y una de las cosas que más me gustan es la facilidad con que puede extenderse, básicamente hay dos formas de hacerlo, una añadiendo nuevos métodos que son aplicables a los elementos (jQuery.fn.extend) y otra a través de la cual se pueden añadir nuevos métodos a  jQuery (jQuery.extend).

En este caso, vamos a extender jQuery añadiéndole un nuevo método, un método llamado mvcDelete que podremos usar desde el onclick de un link.

He preferido la este modo de extensión para poder escribir un sencillo HtmlHelper que me ayude a no tener que escribir prácticamente nada de JavaScript cada vez que quiero poner un link para eliminar un elemento.

<%= Html.DeleteLink(“Delete”, new { action=”Delete”, controller=”Page”, id=Model.Id, redirectToController=”Page”}, null) %>

   1:  jQuery.extend({
   2:          mvcDelete: function(options) {
   3:              var defaults = {
   4:                  controller: "",
   5:                  action: "delete",
   6:                  onComplete: function(xhr, status) { },
   7:                  onSuccess: function(data) { },
   8:                  onError: function(xhr, status) { alert("Error"); },
   9:                  message: "Are you sure you wish to delete this item?",
  10:                  confirmAlert: true,
  11:                  id: 0,
  12:                  redirectToController: "",
  13:                  redirectToAction: "index"
  14:              }
  15:              var ajaxError = false;
  16:              var opt = $.extend(defaults, options);
  17:              var deleteRequest = function() {
  18:                  var deleteUrl = '/' + opt.controller + '/' + opt.action + '/' + opt.id;
  19:                  $.ajax({
  20:                      type: "DELETE",
  21:                      url: deleteUrl,
  22:                      async: false,
  23:                      success: opt.onSuccess(data),
  24:                      complete: opt.onComplete(xhr,status),
  25:                      error: function(xhr, status) { opt.onError(xhr, status); ajaxError = true; }                    
  26:                  });
  27:              };
  28:   
  29:              if (opt.controller.length > 0 && opt.id > 0) {
  30:                  if (opt.confirmAlert) {
  31:                      if (confirm(opt.message))
  32:                          deleteRequest();
  33:                      else
  34:                          return false;
  35:                  } else
  36:                      deleteRequest();
  37:   
  38:                  if (!ajaxError && opt.redirectToController.length > 0) {
  39:                      var redirect = '/' + opt.redirectToController + '/' + opt.redirectToAction;
  40:                      window.location.replace(redirect);
  41:                  }
  42:              }
  43:              return false;
  44:          }
  45:   
  46:      });

En donde vamos a poder pasar:

controller -> controlador de la acción de borrado (requerido)
action -> acción de borrado, por defecto delete
id -> elemento a borrar (requerido)
onComplete -> una función a realizar tras completarse el borrado
onError -> una función a realizar si se producen errores
confirmAlert -> si se mostrará o no el mensaje de advertencia
message -> el mensaje de seguridad
redirectToController -> el controlador de salida (por defecto el mismo que de entrada)
redirectToAction -> la acción de la salida por defecto (Index)

Después el helper que se encargará de realizar el link será

   1:  public static string DeleteLink(this HtmlHelper helper,
   2:                                  string text,
   3:                                  object deleteOptions,
   4:                                  object linkHtmlAttributes)
   5:  {
   6:      var linkTagBuilder = new TagBuilder("a");
   7:   
   8:      linkTagBuilder.Attributes.Add("href", "#");
   9:      linkTagBuilder.SetInnerText(text);
  10:   
  11:      var serializer = new JavaScriptSerializer();
  12:      var mvcDeleteOptions = serializer.Serialize(deleteOptions);
  13:   
  14:      linkTagBuilder.MergeAttribute("onclick", 
  15:                                    string.Format("$.mvcDelete({0}); return false;", mvcDeleteOptions));
  16:      linkTagBuilder.MergeAttributes(new RouteValueDictionary(linkHtmlAttributes));
  17:   
  18:      return linkTagBuilder.ToString();
  19:  }

Aquí lo más destacable es como pasamos las opciones a nuestro plugin, usando un método anónimo, convertimos el objeto usando el serializador de javascript de este modo nuestro plugin, recibe los parámetros.