Implementing a Delete Link with MVC 2 and HttpMethodOverride

A while back I blogged about creating an MVC Delete Link with the AjaxHelper. This was based on another blog post from Stephen Walther where he explained the drawbacks of using hyperlinks for delete scenarios.  HTTP and REST best practices state that GET requests should never modify a resource. The most "RESTful" implementation is is use a DELETE verb. In Walther's post he shows two primary examples: 1) using AJAX to issue a true "DELETE" request, and 2) using individual forms to do the delete operations. In my previous post on MVC DeleteLink, I essentially showed a variation of his first example (i.e., the AJAX method). In this post I'll show a variation on his second example using MVC 2 and the new HttpMethodOverride (i.e., using individual forms for the delete).

An HTML form supports only GET and POST for the form action. It does not support PUT or DELETE verbs. Hence, the typical solution to this is the leverage the form POST to do delete operations as well (in effect, "overloading" the meaning of form POST). MVC 2 introduces a new HTML helper called HttpMethodOverride which allows your form POST to "act like" another verb. This is a common convention that is used by other frameworks (Ruby on Rails does something similar) – sometimes passing "X-HTTP-Method-Override" in an HTTP header and sometimes passing it in a hidden form variable like this. Therefore, if we want a simple delete link, we can now do it like this:

<%Html.BeginForm("Delete", "Contacts", new { id = workout.Id }); %>
    <input type="image" src="/images/remove-icon.png" alt="Delete" />
    <%=Html.HttpMethodOverride(HttpVerbs.Delete) %>
<%Html.EndForm(); %>

This will render HTML output like this:

<form action="/Contacts/Delete/1" method="post">
    <input type="image" src="/images/remove-icon.png" alt="Delete" />
    <input name="X-HTTP-Method-Override" type="hidden" value="DELETE" />

The best part about this is the the MVC action method attributes will respect this form variable:

public ActionResult Delete(int id)  
    return this.RedirectToAction("Index");

Notice the [HttpDelete] attribute. This is also new in MVC 2 as a more succinct version of [AcceptVerbs(HttpVerbs.Delete)]. This is a much better and cleaner implementation than we had available in MVC 1.  However, needing a delete link is something that is going to be pretty common in your applications and creating a form, an input tag and the HttpMethodOverride() is a lot to go through every time you need it. As always, the best approach to keep things simpler (and more DRY) is to create your own re-usable HTML helper method that will allow you to call it like this:

<%=Html.DeleteLink("Delete", "Workouts", new { id = workout.Id }, "/images/remove-icon.png") %>

The complete code for that HTML helper method:

public static MvcHtmlString DeleteLink(this HtmlHelper helper, string actionName, string controllerName, object routeValues, string imageUrlPath)  
    var urlHelper = new UrlHelper(helper.ViewContext.RequestContext);
    var url = urlHelper.Action(actionName, controllerName, routeValues);
    var formTag = new TagBuilder("form");
    formTag.MergeAttribute("action", url);
    formTag.MergeAttribute("method", "post");
    var inputTag = new TagBuilder("input");
    inputTag.MergeAttribute("type", "image");
    inputTag.MergeAttribute("src", imageUrlPath);
    inputTag.MergeAttribute("alt", "Delete");
    formTag.InnerHtml = inputTag.ToString(TagRenderMode.SelfClosing) + helper.HttpMethodOverride(HttpVerbs.Delete);
    return MvcHtmlString.Create(formTag.ToString());

Also notice this is returning the new MvcHtmlString type rather than just a string.

So does this allows us to be more "RESTful"? Yes, it does. But we could still go further.  I really like the approach by Dylan Beattie here in which he introduces route constraints so the URL's don't need to contain segments like "/Delete" or "/Edit". His approach could be further generalized for all controllers such that any POST request that came in with and X-HTTP-Method-Override of "DELETE" would automatically be routed to an action method named Delete(). This is a convention that makes a lot of sense and could also be further enforced with a layer supertype controller.