Relatively recently it was discovered that the MVC framework was inadvertently leading to some bad practices around deleting resources with HTTP GET requests. Specifically, HTTP best practices (and RESTful best practices) state that GET requests should never modify resources. Some people consider this a "security" hole and, while that may be true, I consider it more of a "best practices" hole. Stephen Walther has a great post on this topic here. In his post, Walther demonstrates two different alternatives to using a normal HtmlHelper ActionLink: 1) Ajax Deletes and 2) nested forms with image buttons.
In this post, I'm simply going to show an alternative implementation to his first option of the Ajax Delete.
Let's say you have a list of contacts in a grid like this:
with markup that looks like this:
<%=Html.ActionLink("Add New Contact", "Create") %> <table cellspacing="0" cellpadding="4" border="1"> <tr> <th scope="col">First Name</th> <th scope="col">Last Name</th> <th scope="col">Email</th> <th scope="col"> </th> <th scope="col"> </th> </tr> <%foreach (GetContactListResult contact in this.Model) { %> <tr> <td><%=contact.FirstName %></td> <td><%=contact.LastName %></td> <td><%=contact.Email %></td> <td> <%=Html.ActionLink("Edit", "Create", new { id = contact.ContactID }) %> </td> <td> <%=Html.ActionLink("Delete", "Delete", new { id = contact.ContactID }) %> </td> </tr> <% } %> </table>
On line #21 you will see the line that is the common offender – a hyperlink which will result in a GET request to delete a record to this corresponding Action Method:
public RedirectToRouteResult Delete(int id) { this.contactManager.DeleteContact(id); return RedirectToAction("Index"); }
In Walther's post, he uses the raw Sys.Net.WebRequest object to perform his Ajax operations. Here I'm going to start out an alternative implementation but using an AjaxHelper. First off, we can change line #21 above to this:
<%=Ajax.ActionLink("Delete", "Delete", new { id = contact.ContactID }, new AjaxOptions { Confirm = "Are you sure you want to delete?", OnComplete = "deleteComplete", HttpMethod = "DELETE" })%>
The first three arguments (link text, action name, and route values) match the original implementation exactly but note the AjaxOptions in the fourth parameter. We have a nice little confirm message that will be displayed in a JavaScript prompt. You also see that the OnComplete property is pointing to a function delegate called "deleteComplete" to invoke after the Ajax call has completed. Finally, we're specifying "DELETE" for the HTTP verb. There are a couple of additional items that you have to do to make this work. Specifically, we need to add this script block to the head section of our page:
<script src="/Scripts/MicrosoftAjax.js" type="text/javascript"></script> <script src="/Scripts/MicrosoftMvcAjax.js" type="text/javascript"></script> <script type="text/javascript"> function deleteComplete() { window.location.reload(); } </script>
Notice we have to include the Microsoft javascript libraries to ensure the the AjaxHelper methods will work correctly. Also, I'm defining a simple callback to reload the page. If you refer back to my original Delete() action method, it is returning a RedirectToRouteResult. Additionally, we now need to make a couple of changes to our Delete() action method like this:
[AcceptVerbs(HttpVerbs.Delete)] public ContentResult Delete(int id) { this.contactManager.DeleteContact(id); return this.Content(string.Empty); }
Notice it is now allowing only requests for the DELETE verb. If a GET request for this URI is issued, it will result in a 404. I also have to return an empty ContentResult since the javascript is now going to do my redirect client side. So this will be an incredibly lightweight Ajax server call.
As another alternative, you could even write your action method using an EmptyResult type like this:
[AcceptVerbs(HttpVerbs.Delete)] public EmptyResult Delete(int id) { this.contactManager.DeleteContact(id); return null; }
Now when we click the delete link, we can see the request via the Web Development Helper IE add-in:
You'll see we're now issuing the request with a DELETE verb and the server is returning a 200. Behind the scenes, the AjaxHelper is using the XMLHttpRequest object. One little side note, it order to make the request show up correctly in the web dev helper add-in, I had to change line #5 of my Action method to this:
return this.Content(" ");
I'm considering this a bug in the web dev helper because both an empty string and a space work just fine at run-time.
This is all well and good but the one thing is that our AjaxHelper ActionLink is a little verbose. If I'm going to be having Delete links in multiple places in my app, I sure don't want to have to repeat this same thing all over the place. I also don't want to have to put in the same 1-line javascript callback function every time just to do a little redirect. In order to accomplish this, I can write my own little AjaxHelper method called "DeleteLink" that will allow me to change my mark-up to the much simpler version and avoid having to include an inline javascript callback like this:
<%=Ajax.DeleteLink("Delete", "Delete", new { id = contact.ContactID }) %>
Creating your own Html or Ajax Helper in MVC is typically a pretty straightforward process (much easier than creating ASP.NET server controls for example). The complete implementation for my DeleteLink helper method is:
public static string DeleteLink(this AjaxHelper ajaxHelper, string linkText, string actionName, object routeValues) { return ajaxHelper.ActionLink(linkText, actionName, routeValues, new AjaxOptions { Confirm = "Are you sure you want to delete this item?", HttpMethod = "DELETE", OnSuccess = "function() { window.location.reload(); }" }); }
Notice that I've inlined the javascript function to reload the page. Other than that, everything has just been moved into this method to encapsulate it here. This method could be further customized to include overloads for htmlAttributes, alternative link text, or more. You could also extend this sample by making it more robust by adding a callback for the OnFailure property.