次の方法で共有


ASP.NET MVC: Partial rendering and AjaxAttribute

On a number of occasions I’ve been working with a customer on a scenario where we were adding some Ajax partial rendering support.. All too often demos for this scenario are over-simplified and only render a minimal amount of data. I’m guilty of this too, so I thought I’d try to make amends by showing a slightly more realistic example here. I’m going to show how you can use Ajax for partial rending when you have multiple items on the page, I’ve got a page that lists links to web sites(name and url) and allows you to edit them. Clicking on an item in the list changes which item you are editing.

image

Implementing the basic functionality

Before looking at the Ajax code, I’ll take a look at the basic functionality. I’m a big fan of progressive enhancement (i.e. taking an already functioning page and enriching it with JavaScript so that you end up with a rich experience but also still have one that functions without script) so I’ll get a functioning page first Smile

There are three scenarios to handle:

  1. First hit – this is a GET request but doesn’t load a link into the edit form
  2. Clicking on “edit”  in the link list – this is a GET that loads a link into the edit form
  3. Saving edits to a link – this is a POST that submits the edit form

The first two of these can be combined by taking an optional linkId in the action method as shown below. This code retrieves the list of links from the link service and queries for the link specified by linkId.

         [HttpGet]
        public ActionResult ListAndEdit(int linkId = -1)
         {
             ListAndEditModel model =
                 new ListAndEditModel
                    {
                         Links = _linkService.GetAllLinks(),
                         LinkToEdit = _linkService.GetLink(linkId) ?? new Link {Id = -1}
                     };
             return View(model);
         }

The form submission is handled by the action below. This takes the POSTed Link entity data and saves it via the service if the validation passed. If the validation fails then it re-renders the page with the validation messages.

         [HttpPost]
         public ActionResult ListAndEdit(Link linkToEdit)
         {
             if (ModelState.IsValid)
             {
                 _linkService.Save(linkToEdit);
                 ModelState.Clear();
                 return RedirectToAction("ListAndEdit");
             }
             ListAndEditModel model = new ListAndEditModel
                                     {
                                          Links = _linkService.GetAllLinks(),
                                          LinkToEdit = linkToEdit
                                      };
             return View(model);
         }

These two action methods have the same name which would normally cause an error from ASP.NET MVC informing you that you have ambiguous action methods, but I’ve added the HttpGet and HttpPost attributes which MVC uses to filter the candidate action methods based on the request type.

I’ve used the following Razor view to generate the page in the screenshot above

 @model PartialRendering.Models.Link.ListAndEditModel
@{     ViewBag.Title = "Links";
}
<h2>Links</h2>
<div>
    @using (Html.BeginForm())
     {
          @Html.ValidationSummary(true)
         <fieldset>
            <legend>Edit</legend>
            @Html.EditorFor(m=>m.LinkToEdit)
             <p>
                <input type="submit" value="Save" />
            </p>
        </fieldset>
    }
</div>
<h3>All links</h3>
<table>
    <thead>
        <tr>
            <td></td>
            <td>Name</td>
            <td>Link</td>
        </tr>
    </thead>
    @foreach (PartialRendering.Domain.Link link in Model.Links)
     {
          <tr>
            <td>@Html.ActionLink("edit", "ListAndEdit", "Link", new { linkId = link.Id }, null)</td>
            <td>@link.Name</td>
            <td><a href="@link.Url">@link.Url</a></td>
        </tr>
    }
</table>

 

Initial stab at Ajax support

With the above controller code and view I now have the basic functionality in place. Currently when you click on a link entry in the list there is a page refresh as the new page is loaded. I’ll show how to take advantage of partial rendering via Ajax to avoid this page refresh and update the edit portion of the page when clicking on an edit link (this is the second scenario in the list earlier).

To achieve this I want to be able to render just the update section, so I extracted that section into a partial view (“ListEdit.cshtml”). I also added an id to the div so that I can identify it later.

 @model PartialRendering.Domain.Link
<div id="linkEdit">
    @using (Html.BeginForm())     {          @Html.ValidationSummary(true)         <fieldset>
            <legend>Edit</legend>
            @Html.EditorForModel()             <p>
                <input type="submit" value="Save" />
            </p>
        </fieldset>
    }
</div>

The next step is to update the controller. I modified the action method that handles the get to behave differently if it sees an Ajax request (which I detect using the IsAjaxRequest extension method):

         [HttpGet]         public ActionResult ListAndEdit(int linkId = -1)         {             if (Request.IsAjaxRequest())             {                 Link link = _linkService.GetLink(linkId) ?? new Link {Id = -1};                 return PartialView("LinkEdit", link);             }             else
            {                 ListAndEditModel model =                     new ListAndEditModel
                        {                             Links = _linkService.GetAllLinks(),                             LinkToEdit = _linkService.GetLink(linkId) ?? new Link {Id = -1}                         };                 return View(model);             }         }

To serve an Ajax request, the action retrieves the individual blog post and returns just the LinkEdit partial view. For non-Ajax requests the list of links are also loaded and the full ListAndEdit view is rendered.

Now I just need to hook up the Ajax call from the rendered table. To do this I changed the original Html.ActionLink

 <td>@Html.ActionLink("edit", "ListAndEdit", "Link", new { linkId = link.Id }, null)</td>

to a call to Ajax.ActionLink

 <td>@Ajax.ActionLink("edit", "ListAndEdit", "Link", new { linkId = link.Id },  
new AjaxOptions{ HttpMethod = "GET", UpdateTargetId = "linkEdit", InsertionMode = InsertionMode.Replace })</td>            

The main difference is in the AjaxOption parameter which allows us to specify some extra context around how to make the request (GET) and what to do with the response (Replace the element with the specified id). With the changes to the action method, the content that is returned will be the editor form for the new link, i.e. just the linkEdit div. Because I specified linkEdit as the UpdateTargetId this will simply swap out the old editor form for the new one.

One other minor tweak was to set up jQuery to not cache the Ajax requests. To do this I added a script section to the layout page and called the ajaxSetup function. See the code download if you’re interested.

And that’s all there is to it – easy, huh?

Adding AjaxAttribute

Wait, I thought we were done? Well yes, I have got the functionality that I set out to achieve. However, even for this simple example the ListAndEdit method has started heading towards being unwieldy. One thing that jumps out is that I have a big (alright, middle-ish) if-block at the top level of the function to give different behaviour between the Ajax- and non-Ajax code paths. I didn’t have to do that for the GET vs POST methods – for those I simply applied the HttpGet & HttpPost attributes and I was able to have action methods that were targeted at individual scenarios.

If you dig into the HttpGet/HttpPost attributes they derive from System.Web.Mvc.ActionMethodSelectorAttribute. This base type is used by ASP.NET MVC to filter action methods based on the current request and is available to use to create custom selectors. I’m going to create an attribute similar to the HttpGet/HttpPost attributes that allow me to write separate methods for the Ajax- & non-Ajax action implementations. It turns out to be pretty simple:

     public class AjaxAttribute : ActionMethodSelectorAttribute
    {         private readonly bool _ajax;         public AjaxAttribute(bool ajax)         {             _ajax = ajax;         }         public override bool IsValidForRequest(ControllerContext controllerContext, MethodInfo methodInfo)         {             return _ajax == controllerContext.HttpContext.Request.IsAjaxRequest();         }     }

Most of the code is in taking a constructor parameter and storing it in a field! That value is then compared to the return value from the IsAjaxRequest extension method that I used earlier. With this attribute defined I can separate out the previous action implementation into the following two methods

         [HttpGet]         [Ajax(false)]         public ActionResult ListAndEdit(int linkId = -1)         {             ListAndEditModel model =                 new ListAndEditModel
                {                     Links = _linkService.GetAllLinks(),                     LinkToEdit = _linkService.GetLink(linkId) ?? new Link { Id = -1 }                 };             return View(model);         }         [HttpGet]         [Ajax(true)]         [ActionName("ListAndEdit")]         public ActionResult ListAndEdit_Ajax(int linkId = -1)         {             Link link = _linkService.GetLink(linkId) ?? new Link { Id = -1 };             return PartialView("LinkEdit", link);         }

The first of these methods has Ajax(false) to indicate that it is the non-Ajax method and the second has Ajax(true) to indicate that it is is the Ajax method. Note that I had to rename the method as C# doesn’t like me to have two methods with the same signature, so I applied the ActionName attribute to override the action name (by default it just uses the method name).

Summary

We’ve taken a look at how to take a basic implementation of a page and add Ajax support via partial rendering. The combination of the Ajax helpers (Ajax.ActionLink) and being able to return a partial view from an action make partial rendering relatively simple to achieve. We then went a step further and added the AjaxAttribute to allow us to restructure the controller code to keep the action methods simple.

Attachment: PartialRendering.zip

 

Original post by Stuart Leeks on 13/04/11 here: https://blogs.msdn.com/b/stuartleeks/archive/2011/04/13/asp-net-mvc-partial-rendering-and-ajaxattribute.aspx