C# Corner

Moving from Partial Views to AJAX Calls

Use AJAX and some JavaScript libraries to decouple your data and presentation logic.

In this article, I'll take an ASP.NET application that uses partial views and convert it to use AJAX calls and some JavaScript templating. This will give me more options for how I can use the application in the future.

Why Eliminate Partial Views?
Is there something wrong with partial views? No, absolutely not. They're a great feature in ASP.NET MVC and they allow you to encapsulate view-rendering logic. However, they aren't a good fit for all situations.

The biggest issue is that they're a mix of data and presentation -- HTML presentation, to be exact. Now, HTML is quite ubiquitous and is available on a wide range of platforms and devices, but let's be honest -- not all consumers of HTML treat it the same way (I'm looking at you, Internet Explorer!). The tools are available to help me generate custom HTML based on the currently connected client, but I like my presentation to be lightweight. I don't want to have a lot of logic that sprinkles checks on the User-Agent string.

And if I ever decide to move the functionality of my application to a native platform such as iOS or Android, the data-plus-presentation that comes out of a partial view won't help me at all. In fact, it'll slow me down. To fix this, I'm going to refactor the server-side logic to return raw data instead of HTML data. Then, my client-side code will utilize a JavaScript library to render the data in HTML.

The Original Application
As I write this, Google has recently announced the scheduled retirement of Google Reader. While I don't spend a lot of time reading blogs, the time I did spend doing it was made much easier by Google Reader. With that in mind, the sample code included with this article was influenced by the desire to have a Web-based RSS reader. Because it's demo code, it simply displays a categorized list of feed subscriptions. There's some basic functionality to delete a subscription as well as add a new one. The full sample code for this is available in PartialViewSample.zip, in the code download.

The model used to generate this display is basic -- just a Category class that contains List<Subscription> objects. For demo purposes, the collection of data is stored inside a session variable. Remember, this is just demo code!

I created a partial view for displaying the data:

<div id="categoryList">
  @Html.Partial("CategoryList", Model)
</div>

It's rendered in a simple grid layout thanks to Twitter Bootstrap, as shown in Listing 1 .

Listing 1. A simple grid layout.
@using MovingFromPartialViews.Models
@model IndexModel

@foreach (var cat in Model.Categories)
{
  <div class="row category">
    <div class="span12">@cat.Name</div>
  </div>
  foreach (var sub in cat.Subscriptions)
  {
    <div class="row @(sub.UnreadCount > 0 ? "unread-items" : "no-items")">
      <div class="offset1 span6">[@sub.UnreadCount] @sub.Name</div>
      <div class="span2">
        <button class="btn btn-mini" type="button" action="del"
         data-name="@sub.Name">Delete</button>
      </div>
    </div>
  }
}

Adding a new subscription is pretty straightforward. I defined a model that represents the new subscription information:

public class NewSubscription
{
  public string CategoryName { get; set; }
  public string Name { get; set; }
}

A simple form is displayed at the bottom of the list to collect that information and POST it to the server, as shown in Listing 2.

Listing 2. A form to collect and POST information to the server.
<div id="addItem">
  @using (Html.BeginForm("Add", "Home"))
  {
    <div class="row">
      <div class="span3">
        @Html.DropDownList("CategoryName", 
          Model.AllCategories
          .Select(s => new SelectListItem {Text = s, Value = s}))
      </div>
      <div class="span9">
        <div class="input-append">
          @Html.TextBox("Name", "", new { @class = "span8" })    
          <button class="btn btn-primary">Add</button>
        </div>
      </div>
    </div>
  }
</div>

The Add method on the Home controller handles adding a new subscription. Note that the GetModel call simply grabs the model from the Session:

[HttpPost]
public ActionResult Add(NewSubscription subscription)
{
  var model = GetModel();
  var cat = model.Categories.First(c => c.Name == subscription.CategoryName);
  cat.Subscriptions.Add(new Subscription{Name = subscription.Name ?? string.Empty});

  return RedirectToAction("Index");
}

In this sample, I have the form redirect back to the Index method so the user's browser is updated with the new information.

The delete functionality is handled in a similar way. I created a hidden form to handle the POSTback to the server with the information on which subscription to delete:

@using (Html.BeginForm("Delete", "Home", FormMethod.Post, new {id = "deleteForm"}))
{
  @Html.Hidden("subName")
}

Each delete button that's generated has a custom "action" attribute along with a custom "data-name" attribute. This allows me to have a single event click handler for all of the buttons. That handler grabs the "data-name" attribute and submits the hidden form:

$(document).on('click', 'button[action=del]', function (e) {
  var name = e.target.attributes["data-name"].value;
  $('#subName').val(name);
  $('#deleteForm').submit();
});

On the server, I handle the delete by removing the item from whatever category it was in and, like Add, redirecting it back to the Index method so the user's browser can be updated with the new list of subscriptions:

[HttpPost]
public ActionResult Delete(string subName)
{
  var model = GetModel();
  var cat = model.Categories
    .FirstOrDefault(c => c.Subscriptions.Any(s => s.Name == (subName ?? string.Empty)));

  if( cat != null)
    cat.Subscriptions.Remove(cat.Subscriptions.First(s => s.Name == subName));

  return RedirectToAction("Index");
}

All of this code, as shown, works and works well. It's designed to be consumed by a browser, and it works in that capacity. However, if I were actually creating a new RSS reader, I might want the functionality of maintaining a list of subscriptions to not be tied to producing HTML output. If I wanted to create a native Android client, I'd prefer to do simple Representational State Transfer (REST) calls and handle JSON data instead of using this application's existing calls (which produce HTML data).

Client-Side Templates
Because the new version of my controller is going to return JSON objects instead of a redirect or raw HTML, the first thing I need to do is find a way to generate the HTML on the client. I could use string concatenation in JavaScript, but that's just plain ugly. It's too brittle and doesn't hold up over time as changes are made to the layout.

I decided to use a JavaScript library called Embedded JavaScript (EJS). It's compact (one .js file), easy to use and, for my purposes, very performant. Why not use Knockout.js or Backbone.js? I've used those in the past and will continue to use them when the need arises. They can certainly handle HTML generation, but they're so much more than that. Knockout.js has powerful data binding that makes it effortless to keep a JSON model and the UI in sync. Backbone.js has similar features along with automatic communications with REST endpoints. For my purposes here, all I need is a simple way to take a piece of JSON data and generate some HTML. EJS fits the bill.

Refactoring the Code
Now that I have my template engine, I have to convert the Razor code that built up my HTML to use the EJS template engine. EJS supports a style similar to the Web Forms engine: <% %> to surround code and <%= %> to output data. EJS also supports two ways of loading templates -- either by URL or by embedding the template inside my page. I opted to embed the template inside a <script> tag (like other template engines). Note that if you embed your template inside your page, the script tags must be [% and %] instead of <% and %>.

The complete sample code using the AJAX calls is included in the code download, available as AjaxSample.zip.

With the combination of jQuery .each(), converting the Razor code into an EJS template was pretty easy. The resulting template embedded in a <script> tag is shown in Listing 3.

Listing 3. The template embedded in a <script> tag.
<script id="itemsTemplate" type="text/template">
  [% $.each(Categories, function(index, cat) { %]
    <div class="row category">
      <div class="span12">[%= cat.Name %]</div>
    </div>
    [% $.each(cat.Subscriptions, function(index, sub) { %]
      <div class="row">
        <div class="offset1 span6 [%= sub.UnreadCount > 0 ? 'unread-items' : 'no-items' %]">
          [[%= sub.UnreadCount %]] [%= sub.Name %]</div>
        <div class="span2">
          <button class="btn btn-mini" type="button" action="del" data-name="
            [%= sub.Name %]">Delete</button>
        </div>
      </div>
    [% }) %]
  [% }) %]
</script>

I ripped out the @Html.Partial call from my categoryList <div> because it will be loaded from the client and not during server-side processing:

<div id="categoryList">
</div>

I don't need to change my controller's Index method at all. It's already sending the model to Razor, so I just need to serialize that into a JSON object and pass it to the browser for loading on the client. Here's the code to load the template from the <script> tag and populate the list directly on the client (this is placed in my document.ready function):

itemsTemplate = new EJS({ element: 'itemsTemplate' });
var model = @Html.Raw(Json.Encode(Model));
itemsTemplate.update('categoryList', model);

First, I load the EJS template from the `itemsTemplate' element. Then, I tell Razor to spit out the JSON representation of the model. This will cause the model variable on the client-side to be a representation of my server-side model. I then have the EJS template update the `categoryList' <div> based on the model data. Easy!

Changes for Add and Delete
Add and Delete functionality is pretty easy to convert as well. I change the controller so that after an Add, it will return the model instead of redirecting and doing a full-page refresh:

[HttpPost]
public ActionResult Add(NewSubscription subscription)
{
  var model = GetModel();
  var cat = model.Categories.First(c => c.Name == subscription.CategoryName);
  cat.Subscriptions.Add(new Subscription{Name = subscription.Name ?? string.Empty});

  return Json(model);
}

Inside index.cshtml, I can rip out the Html.BeginForm using block because I'll POST the data myself. Wiring up the event handler for the add button is straightforward, as shown in Listing 4.

Listing 4. Wiring up the event handler for the add button.
$('#addNew').on('click', function() {
  $.ajax({
    url: '/home/add',
    type: 'POST',
    data: {
      CategoryName: $('#CategoryName option:selected').text(),
      Name: $('#Name').val()
    },
    success: function(model) {
      itemsTemplate.update('categoryList', model);
      $('#Name').val('');
    }
  });
});

As you can see, I do an AJAX post to the /home/add method and, upon completion, I take the returned model (which represents the updated data) and update the browser. I also clear out the name field so it doesn't retain the previous value -- remember, I didn't do a full-page refresh, so I'm still maintaining some state on the client.

The delete functionality requires similar changes. I can rip out the entire hidden form I used previously for deletes. Like Add, I'll POST the data via an AJAX call, as shown in Listing 5.

Listing 5. POSTing the data via an AJAX call.
$(document).on('click', 'button[action=del]', function(e) {
  var name = e.target.attributes["data-name"].value;
  $.ajax({
    url: '/home/delete',
    type: 'POST',
    data: {
      subName: name
    },
    success: function() {
      refreshList();
    }
  });
});

Notice that upon completion of this call, I'm not receiving an updated model. Here's what the updated Delete method on the controller looks like:

[HttpPost]
public ActionResult Delete(string subName)
{
  var model = GetModel();
  var cat = model.Categories.FirstOrDefault(
    c => c.Subscriptions.Any(s => s.Name == (subName ?? string.Empty)));
  if( cat != null)
    cat.Subscriptions.Remove(cat.Subscriptions.First(s => s.Name == subName));

  return Json(new {subName});
}

This method returns the item that was deleted. This gives me the option of displaying some kind of UI to the user about which item was deleted. However, I'm not using it in this demo. Instead, I make a call to a function called refreshList:

function refreshList() {
  $.getJSON('/home/subslist', function(model) {
    itemsTemplate.update('categoryList', model);
  });
}

Using jQuery getJSON, this function loads the current model from the server and updates the UI with the complete list of data. The SubsList method was implemented in the controller as:

public ActionResult SubsList()
{
  return Json(GetModel(), JsonRequestBehavior.AllowGet);
}

That's it! The changes listed here convert code that relies on postbacks and full-page refreshes to a single-page application that behaves the same way, but is much more efficient. But how much more efficient? And have I sacrificed anything?

Performance Numbers
I did some highly unscientific testing using Google Chrome DevTools to tell me the payload size of a request along with how many milliseconds it took. I measured the original app as well as the updated application. I performed three operations and repeated each operation three times to get an average. The average numbers from the original version are shown in Table 1. The averages from the AJAX version are shown in Table 2.

PAYLOAD SIZE (KB) TIME (MILLISECONDS)
INITIAL DISPLAY 4KB 136 ms
ADD 2.3KB 109 ms
DELETE 2.4KB 109 ms

Table 1. The average numbers from the original application.



PAYLOAD SIZE (KB) TIME (MILLISECONDS)
INITIAL DISPLAY 4.3KB 176 ms
ADD 1.2KB 4 ms
DELETE 1.7KB 10 ms

Table 2. The average numbers from the AJAX application.

The initial display time was slightly increased, as was the payload size. The additional JavaScript code along with the template probably accounted for increased payload. And populating the template client-side could add a few milliseconds here and there.

But take a look at the numbers for Add and Delete. Those aren't typos -- those times are 4 ms and 10 ms! The payload size on these methods is much smaller -- a JSON object versus a full HTML page -- and processing the template is much easier for the browser vs. rendering an entire HTML document.

In addition to giving the user a better UI experience, I've also made the server-side code much more accessible to non-HTML clients. The Add and Delete methods (as well as the SubsList method) on the controller can be utilized by anything that can POST data to the server and process JSON.

comments powered by Disqus

Featured

  • Diving Deep into .NET MAUI

    Ever since someone figured out that fiddling bits results in source code, developers have sought one codebase for all types of apps on all platforms, with Microsoft's latest attempt to further that effort being .NET MAUI.

  • Copilot AI Boosts Abound in New VS Code v1.96

    Microsoft improved on its new "Copilot Edit" functionality in the latest release of Visual Studio Code, v1.96, its open-source based code editor that has become the most popular in the world according to many surveys.

  • AdaBoost Regression Using C#

    Dr. James McCaffrey from Microsoft Research presents a complete end-to-end demonstration of the AdaBoost.R2 algorithm for regression problems (where the goal is to predict a single numeric value). The implementation follows the original source research paper closely, so you can use it as a guide for customization for specific scenarios.

  • Versioning and Documenting ASP.NET Core Services

    Building an API with ASP.NET Core is only half the job. If your API is going to live more than one release cycle, you're going to need to version it. If you have other people building clients for it, you're going to need to document it.

  • TypeScript Tops New JetBrains 'Language Promise Index'

    In its latest annual developer ecosystem report, JetBrains introduced a new "Language Promise Index" topped by Microsoft's TypeScript programming language.

Subscribe on YouTube