Partial Rendering with ASP.NET MVC and jQuery
Yesterday, I wrote a post detailing how easy we can invoke an ASP.NET MVC controller action from JavaScript using jQuery. Lets up the ante a bit, and see if we can’t use the same approach to get some partial rendering going. To accomplish this, we’ll use a fact about the RenderViewResult class that hasn’t got a lot of attention so far – the fact that its View propetry can be set to point at a UserControl, not just a WebForm. When doing so, only the user control will be rendered – which sounds like exactly what we need for doing partial rendering..
Rendering User Controls
Lets say we’re building a simple task list. We might have the following UserControl representing a single task:
<%@ Control Language="C#" AutoEventWireup="true" CodeBehind="Task.ascx.cs" Inherits="MvcApplication2.Views.Tasks.Task" %>
<div class="task">
<div class="task_name"><%= ViewData.Name %></div>
<div class="task_due"><%= ViewData.Due %></div>
<div class="task_desc"><%= ViewData.Description %></div>
</div>
Our TaskList view may then use this to render a list of tasks:
<h1>Tasks</h1>
<div id="task_list">
<% foreach(Task task in ViewData) {%>
<%= Html.RenderUserControl("~/Views/Tasks/Task.ascx", task) %>
<%} %>
</div>
<h1>Add task</h1>
Name: <input type="text" id="newtask_name" /><br />
Due: <input type="text" id="newtask_due" /><br />
Description: <input type="text" id="newtask_desc" /><br />
<input type="button" onclick="AddTask()" value="Add" />
The TaskList view is rendered by the Index action on our TaskController, which looks like this:
public ActionResult Index()
{
Task[] tasks = // get tasks from repository
return RenderView("TaskList", tasks);
}
Next, we need an action for creating a new task. However, we want to invoke it from JavaScript, and have it only return the markup for the new task item so that we can append it to the task list dynamically, instead of refreshing the entire page. To accomplish this, we implement it using the previously mentioned trick of the RenderViewResult class:
public ActionResult New(string name, string desc, DateTime due)
{
Task task = new Task { Name = name, Description = desc, Due = due };
// ...save task to repository
return RenderView("Task", task);
}
Notice here that «Task» points to the Task.ascx user control, not a Web Form. When invoked, this action will render the markup for that control only.
Manipulating the DOM
Like I mentioned in yesterdays post, we can tell jQuery to handle the response from an Ajax call as html. More than that, we can use jQuery to create a DOM element for us from the returned html, and drop it anywhere in our document. The following JavaScript function accomplishes both:
function AddTask()
{
var name = $("#newtask_name").attr("value");
var due = $("#newtask_due").attr("value");
var desc = $("#newtask_desc").attr("value");
$.ajax(
{
type: "POST",
url: "/Tasks/New",
data: "name=" + name + "&due=" + due + "&desc= "+ desc,
dataType: "html",
success: function(result)
{
var domElement = $(result); // create element from html
$("#task_list").append(domElement); // append to end of list
}
});
}
Here, we’re making an Ajax call to the New action on the Tasks controller. We then create a new DOM element from the resulting html that it renders, and append it to the task list. And that’s it! When the user adds a task, the following POST is sent:
The following response is received from the server, which we append to the task list:
If you want to take a look at a working example, you can download the project I used to demo this here.
Note: For some reason, the Ajax calls are quite slow (~1 sec) when running the demo using Visual Studio’s built-in development server. Publish the site to IIS however, and things are blazingly fast (as you can see from the screenshots above) 🙂
Comments
This post is also available in: English
6/1/2008 4:03:47 AM
Another great post!
I got sick of doing ASP.NET Web Forms applications back at 1.1/2.0. Didn’t like the abstraction level. With ASP.NET MVC, support for multiple AJAX frameworks, and better browsers with CSS 2.1 support I really want to get back to doing web development again.
The combination of ASP.NET MVC with a touch of Silverlight 2 is going to enable us to build som truely amazing web applications .
My next project is deff. going to involve MVC + JQuery.
Cheers,
Jonas
Jonas Follesø
6/1/2008 10:08:52 AM
For the slowness of the Ajax… and in FF in general… did you set ‘network.dns.disableIPv6’ equal to true? This speads up FF when a project is in debug mode…
FF in address bar:
about:config
Josh
6/1/2008 10:40:18 AM
@Jonas: Totally agree. Even though I was fairly happy with ASP.NET and what it let me achieve, moving to ASP.NET MVC was very liberating and I’m not looking back. Add to that the fact that JavaScript frameworks are really maturing now (jQuery, Prototype etc), along with great tools (Firebug!), the fun has definitively been injected back into web development
@Josh: Thanks for the tip! It did the trick
Fredrik
6/1/2008 11:56:08 AM
Good initial sample, though I suppose it would be great to include Add Task in a Form element to allow full postback when javascript is disabled. I’d be then interested in which CancelLayout type approach you would then take to either render the UserControl or a full page. And perhaps use val() rather than attr(‘value’) with jQuery.
Andy
6/3/2008 1:08:29 PM
That’s a good point, Andy. By wrapping the input in a form as you say, you could then make the New action return an ActionRedirectResult instead of a RenderViewResult when the form was posted with javascript disabled, redirecting to the Index action which would then render the full page and not just the added task.
Fredrik
6/15/2008 1:51:32 PM
Hi.
I`ve just started to play around with MVC and stumbled in a problem. I`ve used your sample code and it worked as dream with simple user controls, but when I tried to put an ImageButton on it and it ceased to work. The message says:
Control ‘ctl00_ImageButton1’ of type ‘ImageButton’ should be inserted inside a markup with runat=server
I`ve changed all containers to runat=server but it didn’t work. Am I missing something here?
By the way, the RenderView() method was renamed to View() in MVC preview 3?
Thanks a lot,
Rafael.
Rafael
6/19/2008 4:47:28 AM
gnvbg
jhggv
6/22/2008 12:23:14 AM
Fredrik, fantastic example… straight to the point. Thank you!
Rafael, MVC doesn’t support the full ASP.NET page lifecycle, thus the server controls are not supported. Instead, you should use the UI Helpers. See blog.wekeroad.com/…/ for an explanation / example.
Regards,
Kevin
Kevin Ortman
8/4/2008 11:24:52 AM
If you developed a real-world application with Asp.net mvc you should already know that developing forms with lots of validation reaalllyyy takes time. Knowing what is happening behind the curtain and having full control on the application is great but take my advice and don’t get into developing form heavy applications yet with mvc. Just wait for some improvements (one came out with preview 4 already, ajax support), especially for validation. (Yes I know we have serious client-side validation frameworks like jquery.validate, but client-side validation is only the half)
yang
9/18/2008 4:36:55 PM
Fredrik, thanks for posting this tutorial its helped me out a lot in getting my first MVC app up and running while using jQuery to take care of AJAX calls. I’m still pretty new to jQuery and I’m trying to do something similar to what you have done in your AddTask() function. This is my ajax call:
$.ajax(
{
type: «POST»,
url: «/Product/New»,
data: «name=» + name + «&desc=» + desc + «&stock=»+ stock + «&price=» + price,
dataType: «html»,
success: function(result)
{
var domElement = $(result); // create element from html
if($(«div.new-product»).length > 0){
(domElement).replaceAll(«div.new-product»);
}
else {
$(«#product_list»).append(domElement); // append to end of list
}
}
});
It works except that I am trying to decide where to place the result based on whether or not it contains an element with class «new-product» and it always just appends the result to the #product_list div in my page. I can’t seem to figure out how to search the result html for an element by class. If you have any suggestions I’d appreciate it and again thanks for the great tutorial.
Sean
9/18/2008 4:38:41 PM
Sorry the If block in that previous code was in the process of editing, this is how it actually looks and it still doesn’t work.
if($(«div.new-product»,result).length > 0){
(domElement).replaceAll(«div.new-product»);
}
else {
$(«#product_list»).append(domElement); // append to end of list
}
Sean
9/19/2008 3:12:10 PM
At a glance, my guess is that you need to change $(«div.new-product», result) to $(«div.new-product», domElement).
Fredrik
9/19/2008 5:01:45 PM
Thanks Fredrik, I did get it working I had to do the if block like this:
if($(result).filter(«div.new-product»).length > 0){
$(«div.new-product»).replaceWith(domElement);
}
else {
$(«#product_list»).append(domElement);
}
Works perfectly like that.
Sean
9/19/2008 5:15:56 PM
Cool
Fredrik
10/3/2008 9:49:47 PM
Pingback from justanothercoder.com
My RenderControl Anti-Pattern at Just Another Coder
justanothercoder.com