Reusing Views in ASP.NET MVC for Ajax Updates
The other day I came across this post in Marteen Balliauw's blog where he demonstrates and interesting way to make the same ASP.NET MVC view render correctly from both a regular request and an Ajax request.
In Marteen's post he uses a custom ActionFilterAttribute
to
detect the Ajax call and replace the standard master page with an
unadorned Empty master page.
I liked the idea that you can add that behavior to any action simply by bolting that attribute to it.
What I'm going to illustrate here is a variation of that idea that can be used when you have your own base class to all your controllers, which is something I always do.
SIDE NOTE: I have the habit of creating base classes for all the important things in my ASP.NET applications right after I create the project. I carried this habit from the ASP.NET Webforms-style to the MVC projects. So I typically haveApplicationController
, ApplicationView<T>
,
PartialView<T>
, ApplicationMaster
, etc.
These classes start off empty but code starts finding its way to them
rather quickly.
What I'm going to do is override the Controller.View()
method to detect the Ajax calls and replace the master page right there.
public abstract class ApplicationController: Controller { protected override ViewResult View( string viewName, string masterName, object model) { if(IsAjaxRequest) return base.View(viewName, "Empty", model); return base.View(viewName, masterName, model); } protected virtual bool IsAjaxRequest { //Both Prototype.js and jQuery send // the X-Requested-With header in Ajax calls get { var request = ControllerContext.HttpContext.Request; return (request.Headers["X-Requested-With"] == "XMLHttpRequest"); } } }
With that in place I don't need to change anything in my action code to start supporting the Ajax calls (as long as the controller inherits from the above base class)
public class SampleController : ApplicationController { public ActionResult Index() { return View(); } public ActionResult Details(int id) { var user = new UserInfo {Name = "user" + id, Age = 30}; return View(user); } }
My Index
action calls the Details
action
using both a regular request and an Ajax call, for the sake of the example.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Index.aspx.cs" Inherits="MvcBeta1.Views.Sample.Index" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server"> <h2>Sample View Index</h2> <%= Html.ActionLink("Go to Details", "Details", new { id = 123 })%> <hr /> <input type="button" id="loadDetails" value="Load with Ajax" /> <div id="detailsDiv" style="background-color:#aaa;"> [details should load here] </div> <script> $(function() { $('#loadDetails').click(function() { $('#detailsDiv'). load('<%= Url.Action("Details", new {id = 123}) %>'); }); }); </script> </asp:Content>
The Details view is trivial. It's just a table.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Details.aspx.cs" Inherits="MvcBeta1.Views.Sample.Details" %> <asp:Content ID="Content1" ContentPlaceHolderID="MainContent" runat="server"> <h2>Details for <%= ViewData.Model.Name %></h2> <table border="1"> <tr><th>Col 1</th><th>Col 2</th><th>Col 3</th></tr> <% for(int i=1; i<= 5; i++) { %> <tr> <td>cell (<%= i %>, 1)</td> <td>cell (<%= i %>, 2)</td> <td>cell (<%= i %>, 3)</td> </tr> <%} %> </table> </asp:Content>
When at the Index page we can click on the "Go to Details" link and see the full rendering of the Details view.
If we had instead clicked the "Load with Ajax" button we would cause a simpler version of that page to be inserted in the detailsDiv element (note that the tabs and the blue background that surrounds the table did not come in the rendered content.)
Of course, ASPX files are not the indicated place for placing content that can be used in partial updates, ASCX files would be a better choice for that. That said, sometimes it can be really convenient to have this ability.