Introduction
This article was inspired by a recent need to include modal dialogs in a data-intensive ASP.NET MVC application for all CRUD operations. As usual in such cases, I did a quick Google search and came up with about a dozen possible candidates (see list at end of article). Unfortunately, when I tried to explore a little deeper I quickly found out that with many of these it was hard to get a good sense of whether they could do the things I needed. Documentation was frequently sparse, some examples seemed not to work, and on-line forums were often filled with questions from frustrated users, but few or unhelpful answers.
In working with a little with some of the examples, I began to suspect not only that the examples were rarely complete enough to answer my specific questions, but also there might be undetected gotchas lying in wait for the unsuspecting developer who naively tried to use one in an MVC application. For this reason I decided to create a simple demo page containing two kinds of data entries I typically need: a single data item, representing one row in a table, and a data grid with multiple items. Then I tried to incorporate one dialog library after the other to see what happened when I tried to make it do all the things I wanted.
Overall, I looked at about eight libraries but gave up on some fairly quickly and ended up with only three that I thought worth including for this article. My frustration over the difficulty of finding answers to my questions gave me the idea to present a complete, working, top-to-bottom solution. I hope others may get some benefit from it.
Project Requirements
- My main requirement for this project was to see if I could get one of these things to work, ie, popup a modal dialog from a page of data, make changes to the record and save it back to the database.
- The dialog should submit changes via ajax and do a partial refresh of the relevant section of the page.
- It should be possible to perform updates to multiple items on a page.
- It should be possible to make repeated updates to the same item.
- A personal requirement, just for this demo, was to test the capabilities of each dialog out-of-the-box, without resorting to any supplemental libraries, including the MicrosoftMvcAjax libraries.
Items 2,3 and 4 together posed a problem for all the plugins I tested. A partial refresh replaces the DOM objects in that section that were connected up with the dialog scripts when the page loaded with new ones that look the same but are no longer bound, causing the scripts to fail in various ways. The typical result is that you can do one update and then you're done unless you reload the page. The jQuery live()
function is designed for such situations but I could not get it to work reliably with some of the plugins. As I describe later on, only with the UI-Dialog was I able to figure out workarounds for all the test scenarios.
Using the Code
The included code has complete solutions for three different dialog libraries: jQuery-UIDialog, ColorBox, and Simple Dialog. Because each dialog has different styling and script requirements and to avoid confusing myself excessively, I gave each library its own space, so the Scripts, Views, Content and Controllers folders all have sub-folders for the separate dialog files. A quick look at the project folder should make this very clear.
The start page (Home/Index) presents a menu to the three samples, which all look like the following illustration. Each menu item leads to its own page (all named SalesInfo.aspx) that displays two independent sets of data: one for a single customer and a table listing three book titles, each having an edit link. Clicking on any of these brings up the appropriate type of dialog box for editing that data object. The server time shown in the header of the master page so you can visually tell whether any update is done with ajax or a full postback.
In order to verify that the dialogs actually can update a record and then refresh the page, something often left out of the simple on-line examples, I needed to have some sort of working database to store and retrieve data. To keep things simple I'm using two bare-bones XML files (1 customer and 3 books), accessed with LINQ to XML.
Note that you can't add or delete anything. None of the links to create or delete records are wired up, except for the jQuery-UIDialog sample, where they post to the server but don't change anything in the xml database. In the other samples, clicking on these links will generate an error.
Graceful Javascript
The modal dialogs make use of a technique sometimes referred to as graceful javascript or hijax, which allows users to do update or create operations whether javascript is turned on or off. Discussions of this technique can be found in many articles and blogs, such as this one by Richard Kimble, from which I got several ideas used here. I didn't start out with this being a requirement but it seemed like a good habit to practice, so it's used here throughout. This caused a few issues of its own, primarily around the need to change the visibility of various buttons depending on the type of view, so I want to describe briefly the way this is handled.
You begin by creating a standard page (e.g., PersonalDetail.aspx) that presents a data entry form to a user who clicks the Edit button when javascript is turned off (or there is a script error). This page is actually just a shell for a PartialView that has the actual edit fields. A user with javascript turned on gets the modal dialog, which uses ajax to retrieve only the PartialView.
| |
Javascript turned off | Javascript turned on |
The following snippet shows all there is to PersonalDetail.aspx. Note that the "Back" ActionLink, which Visual Studio normally inserts into partials, has been moved from there into this page because it can cause problems if clicked in a modal dialog, but this way the non-javascript user can still see it.
<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master"
Inherits="System.Web.Mvc.ViewPage<DialogDemo.Models.Person>" %>
<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
<h3>Edit Customer Information</h3>
<% Html.RenderPartial("_PersonDetail"); %>
<div>
<%: Html.ActionLink("Back", "SalesInfo", new { customerid = Model.PersonId })%>
</div>
</asp:Content>
The corresponding partial, which is named _PersonalDetail.ascx according to Kimble's naming convention, contains all the input fields, as shown below. The submit button has been wrapped in a "<div class='btn-Panel nonAjax'></div>" tag to allow the button to be hidden in dialogs that have their own built-in submit buttons, such as SimpleDialog and jQueryUI-Dialog. The btn-Panel class is defined for the modal in the host page (SalesInfo) as "display: none"; then in the PersonalDetail.aspx page, which is delivered only to users without javascript, the nonAjax style is defined as "display: block".
I've moved the Html.ValidationMessageFor
helpers outside the div tags because I think it looks better to display them below the input fields in the UI-Dialog. Finally, the form in both the customer and book partials is given the same id ("target") because the UI-Dialog uses the same script to submit both.
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<DialogDemo.Models.Person>" %>
<div id="form-wrapper">
<% using (Html.BeginForm("CustomerEdit", "jqUI", FormMethod.Post, new { id = "target"} ) )
{%>
<%: Html.ValidationSummary(true) %>
<div class="edit-set">
<div class="editor-label">
<%: Html.LabelFor(model => model.PersonId) %>
</div>
<div class="editor-field readonly">
<%: Html.TextBoxFor(model => model.PersonId, new { @readonly = "readonly"})%>
</div>
<div class="editor-label">
<%: Html.LabelFor(model => model.FirstName) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.FirstName) %>
</div>
<%: Html.ValidationMessageFor(model => model.FirstName) %>
<div class="editor-label">
<%: Html.LabelFor(model => model.LastName) %>
</div>
<div class="editor-field">
<%: Html.TextBoxFor(model => model.LastName) %>
</div>
<%: Html.ValidationMessageFor(model => model.LastName) %>
</div>
<div class="btn-Panel nonAjax"><input type="submit" value="Save" class="button"/></div>
<% } %>
</div>
Sidebar: A Big Gotcha
There's one other issue I wanted to warn about that had me going in circles for quite awhile around requirement #4. The test situation was allowing a user to repeat a given edit, perhaps because a mistake was made or something left out and not noticed until after submitting. If I would, for example, change the first name from Ray to Elvis, save it, then right away click edit again, the modal would reappear still showing the first name as Ray, even though it had been changed to Elvis on the underlying page. Initially I assumed the problem was with the dialog script but that turned out not to be the case. Rather, the server was simply re-sending cached data without hitting the database again (until the cache timed out). To solve this, I defined a no-cache outputCacheProfile
in web.config:
<system.web>
... other stuff
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ZeroCacheProfile"
duration="0"
varyByParam="None"
location="None" />
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
Then decorating any controller actions that retrieve data with the ZeroCacheProfile
forces the server to go the database for every request, and the problem goes away.
[OutputCache(CacheProfile = "ZeroCacheProfile")]
public ActionResult CustomerEdit(int customerid)
{
}
The Comparisons
1. Simple Dialog
Not to be confused with SimpleModal, Simple Dialog was the first library I worked with in any depth and I still like it quite a bit. I have to admit it's probably not a good choice for production sites since development seems to have stopped in mid-2009, and it only got up to version 0.1.1 at that. Nevertheless, it worked almost perfectly, in addition to being very lightweight and easy to style. The one area it had problems with were edit links in a table. The first click works fine, but clicking any links after an ajax refresh bombs out (which is a pretty big failure). However, if you don't mind resorting to regular postbacks for this situation, it works well. It can do ajax submits in cases where the Edit link can be placed outside the PartialView, but that is not possible with grids and there did not seem to be another workaround that I could find.
Since it is pretty simple but does have a few problems, I won't go into too much detail. The demo should be pretty self-explanatory. Some points worth noting:
- Each dialog uses a setup script that references the link used to open it, eg,
$('a.modalDlg').simpleDialog({options})
, but it doesn't allow you to do much with the link attributes. - It does not seem to let you use
$.bind() or $.live()
on the reference link, an unfortunate restriction. - The dialog can be closed from any button you provide in the PartialView or elsewhere that's marked with
class="close"
. - The dialog has no native ability to submit forms, so I'm intercepting the submit event on the PartialView in order to use jQuery$.ajaxPost (for non-table edits only).
- Neither success nor error events are handled by dialog. Instead, the $ajaxPost() method is used to report errors back to the host page.
- Table rows can only be updated with a full postback, using the form's submit button.
I think more could be done with this one, but by this point I'd decided to move on.
2. ColorBox
I saw mentioned this on someone's blog who thought it was good, but ColorBox turned out my least favorite. It has many of the limitations of Simple Dialog, but more so.
- It does not any support kind of ajax posting, so all the editing operations use full postbacks. (You probably could use jQuery$.ajaxPost as I did above, but I didn't have sufficient interest to test it.)
- As far as I can tell you have to make all form submissions using the input buttons on the PartialViews.
- Errors have to be returned to the host page.
- It is fairly hard to style.
To be fair, ColorBox is not designed for data entry forms, but if you want to create a slideshow, gallery or something similar it would be an easy to use and nice looking option.
3. jQuery UI-Dialog
Although I started out with a prejudice against this plugin, it ended up the clear winner. One reason I was avoiding it was that the file sizes seemed to be excessively large, but that's not entirely accurate since you can create a custom download with just the parts you need. It also seemed hard to style, which has some truth. But if you're willing to spend some time in Firebug tracing down out endless layers of div's, it turns out to also be very flexible. The download includes a slightly altered version of the Pepper Grinder theme, but you should be able to substitute any other theme you prefer.
The script deviates a little from that on the jQuery UI site, which wraps all the dialog code in the document.ready()
function. I've been experimenting with a different style that uses only one line per dialog, but I don't know that it matters. My version looks like this:
$(function () {
$('a.modalDlg').live("click", function(event) {loadDialog(this, event, '#customerInfo');});
$('a.abookModal').live("click", function(event) {loadDialog(this, event, '#bookInfo');});
});
The first line binds the click event of all <a> tags with class="modalDlg", which calls the loadDialog() function for editing customer records. The '#customerInfo
' parameter is the id of a <div> tag wrapping the entire content of the Customer.ascx partial view. Upon a successful post, the dialog replaces the content of the div with the updated html sent back from the server.
The second line binds the click events of all the edit links in the datagrid and calls the same function to open the dialog for editing book records. The 'this' parameter refers to the clicked link and the function is able to use its title and href attributes to open the correct dialog for the type of data and set text of the titlebar. The dialog is able to resize itself to correctly fit the different contents.
The jQuery $.live()
function is used to bind links that don't yet exist when the page is loaded, which happens whenever there is a partial ajax refresh.
The function that opens the dialog is mostly taken straight from the jQuery UI site, with a couple important modifications.
function loadDialog(tag, event, target) {
event.preventDefault();
var $loading = $('<img src="../../Content/images/ajaxLoading.gif" alt="loading" class="ui-loading-icon">');
var $url = $(tag).attr('href');
var $title = $(tag).attr('title');
var $dialog = $('<div></div>');
$dialog
.append($loading)
.load($url)
.dialog({
autoOpen: false
,title: $title
,width: 500
,modal: true
,minHeight: 200
,show: 'fade'
,hide: 'fade'
});
$dialog.dialog( "option", "buttons", {
"Cancel": function() {
$(this).dialog("close");
$(this).empty();
},
"Submit": function () {
var dlg = $(this);
$.ajax({
url: $url,
type: 'POST',
data: $("#target").serialize(),
success: function (response) {
dlg.dialog('close');
$(target).html(response);
dlg.empty();
$("#ajaxResult").hide().html('Record saved.').fadeIn(300, function(){
var e = this;
setTimeout(function() { $(e).fadeOut(400); }, 2500 );
});
},
error: function (xhr) {
if (xhr.status == 400)
dlg.html(xhr.responseText, xhr.status);
else
displayError(xhr.responseText, xhr.status);
}
});
}
});
$dialog.dialog('open');
};
I've slightly broken my own rule of no outside libraries by including jquery.effects.core.js and jquery.effects.fade.js to allow fading in and out.
The most important change, however, are the two lines, $(this).empty()
which must appear in both the Cancel and Submit functions. Without these the dialog will not reopen after an ajax update until the whole page is reloaded. I don't really know why this fixes the problem, but it does.
Upon a successful post the line $(target).html(response) replaces the existing html inside the <div id='xxxx'> tag (where $(target) refers to the div id passed into the function as the target parameter) with the response text returned from the server (ie, created by the "_PartialView"). Again, this one function opens the appropriate dialog for editing both the customer and book records, based on the 'href' attribute of the clicked link.
With UI-Dialog it's best (but not required) to use the dialog's own internal Submit and Cancel buttons and to hide the submit button that comes with the _PartialView. The $.ajax method does a great job and the line data: $("#target").serialize()
is all you need to send the serialized data in the "#target" form to the server. (Note that "#target" here refers, by my own naming convention, to $('form[id=target]
') in the "_PartialView", which is different from the target parameter in loadDialog; bad naming on my part but I'm too lazy to change it at the moment.)
Another nice feature of the UI-Dialog is being able to easily display validation errors right in the edit dialog if you want. You'll notice the jqUIController
has some commented out code for constructing an alternate style of error message that can be popped up over the edit dialog, but using the built-in MVC validation framework is much simpler. As an aside, there is a hokey little validation function for the customer object inside the controller that exists just to be able show how it could work. Don't judge me!
Conclusion
Since only the jQueryUI-Dialog was able to meet all the requirements I set for a modal editing dialog, it is the obvious keeper. In addition, it has the further advantages of being in active development with a large user base and being part of a framework that includes other useful ui components, autocomplete, datepicker, tabs, etc., all of which share a selectable set of themes that, even though most of them are kind of ugly, can be customized and give a consistent look to a site.
Other jQuery-Based Dialogs
Besides the three reviewed for this article, here are some other dialog plugins you might want to look at. I tried out about half of these but your mileage may vary.