Introduction
Recently, we have had a web-based project on which a very few of us (developers) worked. One of the most important goals was to create a complex UI like Facebook, but we didn't want to build a complicated, heavy weight client-side code, so we decided to minimize the amount of JavaScript code, and to render the HTML content on the server-side instead. My plan was to slice the page into many, nested, sometimes very small (like a single line in a listbox
) partial views, and transfer the final HTML snippets to the client side, using very simple JS .ajax functions, and doing most of the work on the server-side.
Pretty soon, I understood that the built-in ActionResults
of the MVC framework aren't suitable for this at all, so I have created an own solution.
The Solution
I have created a new ActionResult
class, which allows an ASP.NET MVC application to update more, unrelated parts of the HTML DOM, in a single request-response roundtrip. The aim of this is to render more partial views on the client side, so it's called MultipartialResult
.
For example, you are working on a web-based email reader. When the user selects an email and then clicks on the Delete button, several things should happen at the same time:
- the current email should be removed from the email
listbox
, and the next email should be selected - the preview pane should be updated by the content of the newly selected email
- the indicator that shows the number of the emails in the inbox should be updated
- the browser title (since it also shows the number of the unread emails) should be updated
In my case, the email list is a partial view, so is the preview pane, and the part that contains the number of the emails is a simple text in the DOM.
With the MultipartialResult
, the action that handles the Delete click looks like this:
public ActionResult OnDelete(long EmailId)
{
MultipartialResult result = new MultipartialResult();
result.AddView("_InboxList","InboxListDiv",InboxListModel);
result.AddView("_PreviewPane","PreviewDiv",PreviewPaneModel);
result.AddContent(EmailCount.ToString(),"EmailCountDiv");
result.AddScript(string.Format("document.title='{0}';",BrowserTitle));
return result;
}
The AddView
function will cause the "InboxListDiv
" HTML element to be updated with the _InboxList
view.
The AddContent
function will cause the content of the "EmailCountDiv
" HTML element to be updated with the given string
.
The AddScripts
function will cause the given JavaScript code to be executed on the client side.
In the event-handler on the client side (which works without page refresh of course), the only thing you have to do is to call the MultipartialUpdate
in the OnSuccess
JavaScript event:
@Ajax.ActionLink("Delete", "OnDelete", new { EmailId = Model.CurrentEmail.Id }, new AjaxOptions { OnSuccess = "MultipartialUpdate" })
or, you can use it in an Ajax form:
@using (Ajax.BeginForm("OnDelete",
new { EmailId = Model.CurrentEmail.Id }, new AjaxOptions { OnSuccess = "MultipartialUpdate" }))
or, you can use it in a jQuery .post
or .ajax
function
function deleteClicked(emailId) {
$.ajax({
url: "/inbox/ondelete",
type: "POST",
data: { emailId: emailId },
success: function (result) {
MultipartialUpdate(result);
},
});
Background
The concept of the MultiPartial
is very simple. It is inherited from the JsonResult
class, it renders the specified elements into string
s, collects those string
s, packs them into a json data, and sends this json to the client side.
On the client side, a small JavaScript function iterates through those string
s, and updates the DOM respectively.
When collecting the different kind of results, the MultipartialResult
flags them properly by content:
public MultipartialResult AddView(string viewName, string containerId, object model = null)
{
views.Add(new View() { Kind = ViewKind.View,
ViewName = viewName, ContainerId = containerId, Model = model });
return this;
}
public MultipartialResult AddContent(string content, string containerId)
{
views.Add(new View() { Kind = ViewKind.Content, Content = content, ContainerId = containerId });
return this;
}
public MultipartialResult AddScript(string script)
{
views.Add(new View() { Kind = ViewKind.Script, Script = script });
return this;
}
After the action returns the Result
object, the MVC framework calls the ExecuteResult
function, which produces a json string
by processing the elements one by one:
public override void ExecuteResult(ControllerContext context)
{
List<object> data = new List<object>();
foreach (var view in views)
{
string html = string.Empty;
if (view.Kind == ViewKind.View)
{
html = RenderPartialViewToString(mController, view.ViewName, view.Model);
data.Add(new { updateTargetId = view.ContainerId, html = html });
}
else if (view.Kind == ViewKind.Content)
{
html = view.Content;
data.Add(new { updateTargetId = view.ContainerId, html = html });
}
else if (view.Kind == ViewKind.Script)
{
data.Add(new { script = view.Script });
}
}
Data = data;
base.ExecuteResult(context);
}
Note that rendering the partial view into string
is not the subject of this tip.
On the client side, the only thing to do is to iterate through the json, and for all the elements update the DOM, or run the script:
function MultipartialUpdate(views) {
for (v in views)
if (views[v].script) {
eval(views[v].script);
}
else {
$('#' + views[v].updateTargetId).html(views[v].html);
}
return false;
}
I hope there is someone out there who can benefit from this. :)