ASP.NET MVC: Ajax Dialog Form Using jQuery UI

In one of my recent projects I needed to display some information, allow the user to edit it utilizing a dialog window, post the updated information and reload it for the user using Ajax. I needed to perform the named operations for multiple models so I set out to create some generic code that could be reused over and over. Below is a picture of what I am trying to accomplish.

The project used for the post below can be downloaded here.

First, the model. For this example, we will use a simple model that contains some profile information for a user.

public class Profile
{
    [Required]
    public string Name { get; set; }

    [Required]
    [StringLength(10, MinimumLength=3)]
    [Display(Name="Nick name")]
    public string NickName { get; set; }

    [Required]        
    public string Email { get; set; }

    [Required]
    public int Age { get; set; }
}

Second, we need to create three action methods. One will be used to display the profile information, another to display the form for editing the profile, and lastly another that will be used to save the edited profile object. The first two should always return a PartialView as each of these action methods will be called using Ajax and their result will be loaded into div elements; the first into a div used to display the saved profile and the second into the edit dialog. The third action method will return a PartialView if the ModelState is invalid so that the errors can be displayed to the user and a Json result indicating the save was successful if all went well. (Note that in this example I am just storing the profile information in Session but obviously this would be stored in a database or some other data store.)

public ActionResult Profile()
{
    Profile profile = new Profile();

    // Retrieve the perviously saved Profile
    if (Session["Profile"] != null)
        profile = Session["Profile"] as Profile;

    return PartialView(profile);
}

public ActionResult EditProfile()
{
    Profile profile = new Profile();

    // Retrieve the perviously saved Profile
    if (Session["Profile"] != null)
        profile = Session["Profile"] as Profile;

    return PartialView(profile);
}

[HttpPost]
public ActionResult EditProfile(Profile profile)
{
    // If the ModelState is invalid then return
    // a PartialView passing in the Profile object
    // with the ModelState errors
    if (!ModelState.IsValid)
        return PartialView("EditProfile", profile);

    // Store the Profile object and return
    // a Json result indicating the Profile 
    // has been saved
    Session["Profile"] = profile;
    return Json(new { success = true });            
}

Next we need to create the two partial views that correspond to the first two action methods we created above. In this example, the partial view for the EditProfile action is pretty much just the stock view created by the MVC frameowork except I have removed the input element to submit the form as we will use the buttons on the jQuery UI dialog to submit it.

The second partial view, the one that displays the saved Profile object again in this example is the stock view with a few added elements.

@using DialogFormExample.MvcHelpers

@model DialogFormExample.Models.Profile

<fieldset>
    <legend>Contact Info</legend>
    
    <div class="display-field">
        Name: @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-field">
        Nick name: @Html.DisplayFor(model => model.NickName)
    </div>
    
    <div class="display-field">
        Email: @Html.DisplayFor(model => model.Email)
    </div>
    
    <div class="display-field">
        Age: @Html.DisplayFor(model => model.Age)
    </div>

    <div class="right">
        @Html.DialogFormLink("Edit", Url.Action("EditProfile"), "Edit Profile", "ProfileContainer", Url.Action("Profile"))
    </div>
</fieldset>

The first portion of the partial view just displays the form elements required for editing the model. This is just the stock Edit view modified only slightly. The new code starts at line 25. Here I created an extension method for HtmlHelper named DialogFormLink that will create an anchor tag loaded with all the needed information to make the dialog form work. Here is the code for the extension method. You can read the comments to get an understanding of the parameters it requires.

/// <summary>
/// Creates a link that will open a jQuery UI dialog form.
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="linkText">The inner text of the anchor element</param>
/// <param name="dialogContentUrl">The url that will return the content to be loaded into the dialog window</param>
/// <param name="dialogTitle">The title to be displayed in the dialog window</param>
/// <param name="updateTargetId">The id of the div that should be updated after the form submission</param>
/// <param name="updateUrl">The url that will return the content to be loaded into the traget div</param>
/// <returns></returns>
public static MvcHtmlString DialogFormLink(this HtmlHelper htmlHelper, string linkText, string dialogContentUrl,
    string dialogId, string dialogTitle, string updateTargetId, string updateUrl)
{
    TagBuilder builder = new TagBuilder("a");
    builder.SetInnerText(linkText);
    builder.Attributes.Add("href", dialogContentUrl);            
    builder.Attributes.Add("data-dialog-title", dialogTitle);
    builder.Attributes.Add("data-update-target-id", updateTargetId);
    builder.Attributes.Add("data-update-url", updateUrl);

    // Add a css class named dialogLink that will be
    // used to identify the anchor tag and to wire up
    // the jQuery functions
    builder.AddCssClass("dialogLink");

    return new MvcHtmlString(builder.ToString());
}   

The above extension method that builds our anchor tag utilizes the HTML5 data attributes to store information such as the title of the dialog window, the url that will return the content of the dialog window, the id of the div to update after the form is submitted, and the url that will update the target div.

Now we need to add the container div to hold the Profile information in the Index view.

@{
    ViewBag.Title = "Home Page";
}

<h2>@ViewBag.Message</h2>

<div id="ProfileContainer">
    @{ Html.RenderAction("Profile"); }
</div>

Lastly, I have listed the scripts and css files that need to be linked below in the _Layout page for everything to work correctly.

<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>
    <link href="@Url.Content("~/Content/Site.css")" rel="stylesheet" type="text/css" />
    <link href="@Url.Content("~/Content/themes/base/jquery.ui.all.css")" rel="stylesheet" type="text/css" />
    <script src="@Url.Content("~/Scripts/jquery-1.5.1.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/modernizr-1.7.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery-ui-1.8.11.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.unobtrusive-ajax.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.min.js")" type="text/javascript"></script>
    <script src="@Url.Content("~/Scripts/jquery.validate.unobtrusive.min.js")" type="text/javascript"></script>   
    <script src="@Url.Content("~/Scripts/DialogForm.js")" type="text/javascript"></script>    
</head>

The last script reference above is to a script I wrote called DialogForm.js. This script (shown below) will make everything work.


$(function () {

    // Don't allow browser caching of forms
    $.ajaxSetup({ cache: false });

    // Wire up the click event of any current or future dialog links
    $('.dialogLink').live('click', function () {
        var element = $(this);

        // Retrieve values from the HTML5 data attributes of the link        
        var dialogTitle = element.attr('data-dialog-title');
        var updateTargetId = '#' + element.attr('data-update-target-id');
        var updateUrl = element.attr('data-update-url');

        // Generate a unique id for the dialog div
        var dialogId = 'uniqueName-' + Math.floor(Math.random() * 1000)
        var dialogDiv = "<div id='" + dialogId + "'></div>";

        // Load the form into the dialog div
        $(dialogDiv).load(this.href, function () {
            $(this).dialog({
                modal: true,
                resizable: false,
                title: dialogTitle,
                buttons: {
                    "Save": function () {
                        // Manually submit the form                        
                        var form = $('form', this);
                        $(form).submit();
                    },
                    "Cancel": function () { $(this).dialog('close'); }
                }
            });

            // Enable client side validation
            $.validator.unobtrusive.parse(this);

            // Setup the ajax submit logic
            wireUpForm(this, updateTargetId, updateUrl);
        });
        return false;
    });
});

function wireUpForm(dialog, updateTargetId, updateUrl) {
    $('form', dialog).submit(function () {

        // Do not submit if the form
        // does not pass client side validation
        if (!$(this).valid())
            return false;

        // Client side validation passed, submit the form
        // using the jQuery.ajax form
        $.ajax({
            url: this.action,
            type: this.method,
            data: $(this).serialize(),
            success: function (result) {
                // Check whether the post was successful
                if (result.success) {                    
                    // Close the dialog 
                    $(dialog).dialog('close');

                    // Reload the updated data in the target div
                    $(updateTargetId).load(updateUrl);
                } else {
                    // Reload the dialog to show model errors                    
                    $(dialog).html(result);

                    // Enable client side validation
                    $.validator.unobtrusive.parse(dialog);

                    // Setup the ajax submit logic
                    wireUpForm(dialog, updateTargetId, updateUrl);
                }
            }
        });
        return false;
    });
}

Conclusion

I know that was a lot to take in but after you do the setup once, all you need to do is make one call to Html.DialogFormLink and everything will be taken care of for you. Hope this helps!

Advertisement

24 Responses to “ASP.NET MVC: Ajax Dialog Form Using jQuery UI”

  1. ASP.NET MVC: Client Side Validation with an Ajax Loaded Form « Nick Olsen's Programming Tips Says:

    [...] Client Side Validation with an Ajax Loaded Form August 13, 2011 — Nick Olsen In my last post I discussed how to perform some CRUD operations using Ajax and the jQuery UI dialog window. In that [...]

  2. ASP.NET MVC: Displaying Client and Server Side Validation Using qTip Tooltips « Nick Olsen's Programming Tips Says:

    [...] will be building off of the example from my last post that showed how to use jQuery UI to build Ajax [...]

  3. ASP.NET MVC: Displaying Client and Server Side Validation Using Error Icons « Nick Olsen's Programming Tips Says:

    [...] done previously I will be using the same example project from this post where we created a dialog form which was submitted via [...]

  4. Alpesh Says:

    Very useful article, and thanks for posting the code download link too. Have one question. I want to generalize the DialogForm.js for all the dialog box based forms in my application. And all of my forms are going to have jquery based validations over and above server side validations. I see that you are manually submitting the form by saying $(form).submit();. I am going to have Validate jquery method on all my partials renderend in dialog boxes. Is there any way i can generalize these Validate() jquery methods to be called before the form is submitted?

    • Nick Olsen Says:

      I don’t know if I understand exactly what you are saying but from what I understand you want to make sure the jQuery validation rules are run before the form is submitted to the server correct? This is already being done. In line 46 of DialogForm.js we are checking $(this).valid() which runs all jQuery validation and returns false if anything isn’t valid. In this case we cancel the form submit.

      Is that what you meant?

  5. Rich Says:

    Hi, great post!

    I was just wondering, how do you change the width of the dialog?
    I’ve tried to set the width of ui-dialogue in my theme css, but it seems to be written as an inline stle at some point

    Thanks!

    • Nick Olsen Says:

      You have to set the width in the jquery ui dialog method like so:

      $( “.selector” ).dialog({
      width: 460
      });

      By default I believe the width gets set to 300px if you don’t specify a width.

  6. Gilbert Blanco Says:

    Thank you for share your post with the community.

  7. Dave Says:

    Excellent post, It’s helped me loads already.
    Have you any tips on how to make the behavior of a successful submit dynamic?
    So instead off just….

    // Reload the updated data in the target div
    $(updateTargetId).load(updateUrl);

    …it could somehow be…..

    $(updateTargetId).load(updateUrl, function(){
    SomePageSpecificAction();
    });

    Reason I ask is that I am trying to integrate this with a page that is using a tablesorter (http://tablesorter.com/docs/), which requires a call to (“#table”).trigger(“update”); it would be a beautiful thing if it was possible to make this function call!!!!

    Any ideas appreciated, and again, many thanks for the post.

    • Nick Olsen Says:

      You should be able to do just that! Take a look at the documentation: http://api.jquery.com/load/ One of the parameters to the load method is a function that will be called after the load. Let me know if it isn’t working.

      • Dave Says:

        Hi Nick, thanks for the reply.

        I’ve had a go at converting your code to a jquery widget. Let me know what you think….

        /* SomePage.js */
        $(‘.dialogForm’).dialogForm({ postSuccesCallBack: function () { alert(“Hello”); } });

        /* DialogForm.js */
        var dialogForm = {

        _init: function () {

        var $this = this;

        $this.element.click(function (e) {

        e.preventDefault();

        // Retrieve values from the HTML5 data attributes of the link
        var dialogTitle = $this.element.text(); //element.attr(‘data-dialog-title’);
        var updateTargetId = ‘#’ + $this.element.attr(‘data-update-target-id’);
        var updateUrl = $this.element.attr(‘data-update-url’);

        // Generate a unique id for the dialog div
        var dialogId = ‘uniqueName-’ + Math.floor(Math.random() * 1000)
        var dialogDiv = “”;

        // Load the form into the dialog div
        $(dialogDiv).load(this.href, function () {
        $(this).dialog({
        modal: true,
        show: ‘blind’,
        position: ‘top’,
        width: 680,
        resizable: false,
        title: dialogTitle,
        buttons: {
        “Save”: function () {
        // Manually submit the form
        var form = $(‘form’, this);
        $(form).submit();
        },
        “Cancel”: function () { $(this).dialog(‘close’).dialog(“distroy”).remove(); }
        }
        });

        // Enable client side validation
        $.validator.unobtrusive.parse(this);

        // Setup the ajax submit logic
        $this._wireUpForm(this, updateTargetId, updateUrl, $this.options.postSuccesCallBack);
        });
        return false;
        });
        },

        options: {
        postSuccesCallBack: null
        },

        /* private */
        _currentSortDirection: true, // true = ascending, false descending

        _debug: function (message) {
        if (window.console && window.console.log)
        window.console.log(message);
        },

        _wireUpForm: function (dialog, updateTargetId, updateUrl, succesCallBack) {
        $(‘form’, dialog).submit(function () {

        // Do not submit if the form
        // does not pass client side validation
        if (!$(this).valid())
        return false;

        // Client side validation passed, submit the form
        // using the jQuery.ajax form
        $.ajax({
        url: this.action,
        type: this.method,
        data: $(this).serialize(),
        success: function (result) {
        // Check whether the post was successful
        if (result.success) {
        // Close the dialog
        $(dialog).dialog(‘close’).dialog(“distroy”).remove();

        // Reload the updated data in the target div
        $(updateTargetId).load(updateUrl, function () {
        succesCallBack();
        });
        } else {
        // Reload the dialog to show model errors
        $(dialog).html(result);

        // Enable client side validation
        $.validator.unobtrusive.parse(dialog);

        // Setup the ajax submit logic
        wireUpForm(dialog, updateTargetId, updateUrl);
        }
        }
        });
        return false;
        });
        }
        };
        $.widget(“ui.dialogForm”, dialogForm);

  8. jovnas Says:

    Great post!

    I’ve implemented a customized version of your code.
    Since our site currently is not using unobtrusive validation I replaced the line $.validator.unobtrusive.parse(dialog); with Sys.Mvc.FormContext._Application_Load();
    However, the client validation did not fire if the user opened the dialog, pressed cancel, then opened it again. The solution to this problem was to add .dialog(‘destroy’).remove(); on line 31 and 63. (I guess just remove() would work.)

    Thought I should share this, if anyone faces the same problem.

  9. adamtuliper Says:

    You may want to include a filter for .filter(‘div) to avoid reloading scripts plus I _think_ (it may be fixed) jquery creates a dialog for scripts and for the content if both are returned. Note the bug here: http://forum.jquery.com/topic/problem-with-ui-dialog-component-and-jquery-1-4-1

  10. adamtuliper Says:

    also note the spelling error above for $(dialog).dialog(‘close’).dialog(“distroy”).remove(); (should be ‘destroy’)

  11. adamtuliper Says:

    Great post btw.

  12. czetsuya Says:

    I’m currently working on a project that requires multiple locale settings, so is it possible to change the button labels at runtime?

    • Nick Olsen Says:

      This is an example of one way to set the button text based on the current locale setting. You would have to store your button text in a global strings object or array.

      var buttons = {};
      buttons[strings.ok] = function () {
      // Do something here
      $(this).dialog(‘close’);
      };
      buttons[strings.cancel] = function () {
      $(this).dialog(‘close’);
      }

      div.dialog({
      modal: true,
      width: 400,
      height: 450,
      resizable: false,
      title: strings.title,
      buttons: buttons,
      close: function () {
      $(this).remove();
      }
      });

  13. czetsuya Says:

    Hi, sorry for the 2 successive questions but I just want to know why the dialog fail when seting the properties autoOpen: false and show: “blind”.

    Thanks,
    czetsuya

  14. Ben Says:

    Nick, great solution. I’m using it in my own little web app. But I’ve noticed that within one of my dialogs opened for editing some data any of my jquery calls are not being executed. For example: I open a dialogbox which has two checkboxes. I have some jquery code that forces only one of the checkboxes to be selected similar to the radio button logic. the code executes nicely in a regular page but not at all from within the dialog. Any ideas?

    • Nick Olsen Says:

      Is the script included on the page that is loaded into the dialog? If so, this is a jQuery issue. For security reasons jQuery will strip out any scripts that are included on a page that are loaded via ajax. Somewhat obnoxious but not the end of the world. I have found two solutions to this. After the page is loaded into the dialog, use the jQuery $.getScript() method to remotely retrieve the script or have a function defined on the parent page of the dialog form and after the load of the dialog form, call that function which runs your script on the dialog content.

      Hope that helps.

  15. czetsuya Says:

    Hi,

    I would like to ask if you’ve encountered this issue:

    I am required to do some business server side validations and when it failed I return the view. When I click the save/submit button again it doesn’t work.

    For example I have 2 date fields: date1 and date2 and date1 should be greater than date2, When that rule is satisfied it successfully post to the server. But when not it shows an error in the page (correct), then when the error is corrected and the submit button is pressed again, it won’t submit. Any idea or possible solution to this?

    Really appreciate this plugin,
    czetsuya

    • czetsuya Says:

      Nevermind it now works.

      Thank you very much for this very helpful plugin :-) .

      czetsuya

  16. David Compton Says:

    I’m not sure if this is already covered in the comments above, however in the hope that it might save someone else a few hours of debugging heartache…. note that in order to allow the dialog to work when called multiple times, in DialogForm.js I had to add in a call to $this).empty() after the call to $(this).Dialog(‘Close’) – and a call to $(dialog).empty() after the call to $(dialog).dialog(‘close’). Without this I had major problems if the dialog was called twice without the calling page being refreshed.

  17. czetsuya Says:

    I’m wondering why an input of type file doesn’t work using this popup? The same form works properly if not loaded to the popup, I can successfully upload the file and get it to the post action. But it’s always null when I put the form inside the popup.

    Any guess why?

    Thanks,
    czetsuya


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Connecting to %s

Follow

Get every new post delivered to your Inbox.

Join 36 other followers