MVC – Updating multiple partial views from a single (AJAX) action

Sometimes you need to make an AJAX call from your MVC view to perform some server side actions on user changes. You may then want to return some calculated data in a partial view so that you can update the view as presented to the user.

What if you have modified data in more than one partial view?

An application I worked on required some server side calculations that updated both data in a table (one partial view) and summary data displayed in the footer of the page (a second partial view). I would not want to make two separate service side calls to update the two partial views, so I looked into a mechanism to allow me to return the two partial views in the response from the server side call.

I realised that if I could package the two partial view responses into a single object, I could pass this to the client in the response, then use my client side script (JQuery in my case) to unpack the responses and update the two partial views.

The tricky bit was how to package up the partial views. Google pointed to this method to render a Razor view into a string(obtained from ):

public static String RenderRazorViewToString(ControllerContext controllerContext, String viewName, Object model)
{
  controllerContext.Controller.ViewData.Model = model;

  using (var sw = new StringWriter())
  {
    var ViewResult = ViewEngines.Engines.FindPartialView(controllerContext, viewName);
    var ViewContext = new ViewContext(controllerContext, ViewResult.View, controllerContext.Controller.ViewData, controllerContext.Controller.TempData, sw);
    ViewResult.View.Render(ViewContext, sw);
    ViewResult.ViewEngine.ReleaseView(controllerContext, ViewResult.View);
    return sw.GetStringBuilder().ToString();
  }
}

Using this method, I was able to create a controller action that returned a JSON object containing the rendering for my two partial views:

[HttpPost]
public ActionResult _CalculateValues(MyViewModel model)
{
  ModelState.Clear();
  calculationService.CalculateValues(model, User.Identity.Name);
  // The total values and summary values are displayed in two partial views
  // We can't normally return two partial views from an action, but we don't want to have another server
  // call to get the second one, so we render the two partial views into HTML strings and package them into an
  // an anonymous object, which we then serialize into a JSON object for sending to the client
  // the client side script will then load these two partial views into the relevant page elements
  var totalValuesPartialView = RenderRazorViewToString(this.ControllerContext, "_TotalValues", model);
  var summaryValuesPartialView = RenderRazorViewToString(this.ControllerContext, "_SummaryValues", model);
  var json = Json(new { totalValuesPartialView, summaryValuesPartialView });
  return json;
}

Note that my two partial views (“_TotalValues.cshtml” and “_SummaryValues.cshtml”) both use the same model in my example as the view that they are displayed on (MyModelView). I don’t believe this would be a necessary restriction though.

In my view I render the two partial views using mark up similar to:

<div id="_TotalValues" data-url='@Url.Action("_CalculateValues", "Improvement")'>
 @{
   Html.RenderPartial("_TotalValues", Model);
 }
</div>

and

<div id="_SummaryValues">
 @{
   Html.RenderPartial("_SummaryValues", Model);
 }
</div>

Finally, I have a JScript method to make the AJAX call on some event (such as the user changing a value or clicking a button – whatever event the method is bound to) and update the partial views with the response:

// Make an ajax call to recalculate the total values and summary values
function calculateTotalAndSummaryValues() {
  // Get the controlller action url 
  var url = $("#_TotalValues").data('url');
  var data = $("form").serialize();

  // Post the current contents of the form, so we show the Year 1 to 5 benefit values (which might be unsaved)
  $.post(url, data, function (response) {
    // This post will return a JSON object with two properties called totalValuesPartialView and summaryValuesPartialView
    // these will contain the rendering for the two partial views _TotalValues and _SummaryValues
    // by packaging these two up, we can update two partial views from this ajax post
    // with one single server method call
    $("#_TotalValues").html(response.totalValuesPartialView);
    $("#_SummaryValues").html(response.summaryValuesPartialView);
    });
}

This is a very simplistic implementation, with no error handling, just to show the technique.

The key features are:

  • Use a helper method to render a razor partial view into a string
  • Package the partial views into a JSON object
  • Use JScript / JQuery to unpack the AJAX call response and update HTML elements (DIVs) containing the partial views
Advertisement

Use JQuery to automatically update edited data on other parts of a form

If you have a form that has multiple tabs, or other display sections, you might have a need to display data that is entered on one part of the form on other parts. For example the first tab could allow the user to enter a name, this name may then be displayed as read only on other tabs.

There are various methods to do this, but if the read only display is positioned in different parts of the form and in more than one place, you might want to have separate elements for each display.

This little bit of jQuery will allow any changes to the data to be automatically copied to all of the relevant read only display elements.

There are three steps to implement this functionality:

  1. Include the following jQuery in your form (e.g. via a js file)
$(document).ready(function () {
  // On change event for any element with the hasDisplayElement 
  // class
  $(".hasDisplayElement").change(function () {
    updateElementDisplay($(this));
  });

  // Initialise the display element values
  $(".hasDisplayElement").each(function () {
    updateElementDisplay($(this));
  });
});

// Any element with a hasDisplayElement class will check on change
// for other elements with the classes isElementDisplay id, where 
// id is the id of the triggering element (the one with the 
// hasDisplayElement class) if using @HtmlTextFor, etc. the id will 
// be the same as the model property name e.g.
//  @Html.TextAreaFor(model =&gt; model.Description, 
//    new { rows = "8", @class = "form-control asDisplayElement"})
//
// changes will update value in any element like this:
// <textarea class="form-control isDisplayElement Description" 
//    rows="8" disabled="true">@Model.Description</textarea>

function updateElementDisplay(element) {
  var id = element.attr("id");
  var displayElements = $(".isDisplayElement." + id)

  if (element.is("select")) {
    var value = element.children("option:selected").text();
    displayElements.val(value);
  }
  else
    displayElements.val(element.val());
}
  1. Add the “hasDisplayElement” class to all editors that will need to update corresponding display elements elsewhere on the form. Also make sure that all the editor elements have an id.
  2. Add the class “isDisplayElement” followed by the id of the relevant editor element to all of the display elements. By including the id of the editor as a class on the display element, the jQuery script will be able to find the correct display elements whenever a edited value is changed.

Example:

In a MVC project, I needed to displayed the selected Start Date on other parts of the form. The editor for the start date was defined as:

@Html.TextBoxFor(model => model.StartDate, new { @class = "hasDisplayElement" })

MVC will automatically give the text box element an Id of “StartDate”

Elsewhere on the form, where I needed to display the specified start date, I used:

<input type="text" class="isDisplayElement BenefitStartDate" disabled="disabled" />

N.B. The updateElementDisplay method uses the jQuery val() method to update the display elements. Therefore, you need to ensure that any elements that have the “isDisplayElement” class support val() (such as Input elements). If you want to use other elements, such as div, you will need to modify the updateElementDisplay method to detect these and use text() instead.