Thursday, September 16, 2010

jQuery dialog extender for Asp.Net MVC Forms

“This post and the demo solution is still valid for MVC 2.o.

However, a new version of this post using MVC 3.0 and Razor with minor improvements is available here.

Continuing the previous post, let’s go ahead and implement the dialog extender for MVC forms (post actions).

The mechanism will be similar with some details added. In case of a for we need to post to the server.  If the post action was not successful, that is if validations failed, we need to show validation errors in the dialog. In case of any other unexpected error, appropriate message needs to be shown.

Finally, if the post action is successful (all validations passed and data successfully saved), notify the client success so that the client can close the dialog. For this I created a dummy view edit success in the shared folder.

Let’s walk thru the implementation now -

The Model using the MVC framework’s validation provider-

using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel;

namespace MVCDialogExtender.Models {

    public class Product {

        public Guid Id { get; set; }

        [Required]
        [DisplayName("Product name")]
        public string Name { get; set; }

        [Required]
        [DisplayName("Product description")]
        public string Description { get; set; }
    }
}

The view (Edit.ascx):

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MVCDialogExtender.Models.Product>" %>

    <% using (Ajax.BeginForm("Edit", "Products",
           new AjaxOptions { OnComplete = "addEditProductComplete", HttpMethod = "POST" })) { %>

        <%: Html.ValidationSummary(true)%>
        <%: Html.AntiForgeryToken()%>

        <fieldset>
            <legend>Product details</legend>
            
            <div style="display:none;">
                <%: Html.TextBoxFor(model => model.Id)%>
            </div>
            
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Name)%>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Name)%>
                <%: Html.ValidationMessageFor(model => model.Name)%>
            </div>
            
            <div class="editor-label">
                <%: Html.LabelFor(model => model.Description)%>
            </div>
            <div class="editor-field">
                <%: Html.TextBoxFor(model => model.Description)%>
                <%: Html.ValidationMessageFor(model => model.Description)%>
            </div>
            
            <p id="EditProductButtons">
                <input type="submit" value="Save" id="SubmitAddEditProduct"/>

                <input type="button" value="Cancel"/>
                <!-- Implement what the cancel button is expected to do when not rendered in a dialog extender-->
            </p>
        </fieldset>

    <% } %>

The Actions:

        public ActionResult Edit(Guid id) {

            var model = _data.SingleOrDefault(p => p.Id == id);
            return View(model);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Edit(Product product) {

            if (ModelState.IsValid) {
                var list = ProductList;
                list.Single(p => p.Id == product.Id).Name = product.Name;
                list.Single(p => p.Id == product.Id).Description = product.Description;
                ProductList = list;
                return View("EditSuccessful");
            }
            else {
                return View(product);
            }
        }

As you can see that the post action returns a dummy view called EditSuccess to notify success to the client. So here’s my dummy view(EditSuccess.ascx for the Shared folder:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<dynamic>" %>
"E-d-i-t---S-u-c-c-e-s-s-f-u-l"

And finally the dialog extender to bring it all together (only if required) at runtime. Also, consumes the hideous message from the dummy view – Mr “ProductEditDialog.ascx” from the Shared folder:

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MVCDialogExtender.Models.DialogExtender>" %>

<div id="ProductEditDialogDiv" title="Product" style="display:none;"></div>

<script type="text/javascript">

    $(document).ready(function () {
        $('#ProductEditDialogDiv').dialog({
            autoOpen: false,
            width : '500px',
            modal: true,
            close: function (event, ui) { $("#ProductDetailsDialogDiv").html(""); },
            buttons: {
                "Save": function () { $("#SubmitAddEditProduct").click(); },
                "Cancel": function () { $(this).dialog("close"); } 
                }
        });
    });

    function openProductEditDialog(id) {

        $.ajax({
            type: "GET",
            url: encodeURI('<%= Url.Action("Edit", "Products") %>' + "?id=" + id),
            cache: false,
            dataType: 'html',
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                $("#ProductEditDialogDiv").html(XMLHttpRequest.responseText);
            },
            success: function (data, textStatus, XMLHttpRequest) {
                $("#ProductEditDialogDiv").html(data); 
            },
            complete: function (XMLHttpRequest, textStatus) {
                // since these buttons are not specific to the dialog extender
                $("#EditProductButtons").hide();
                $('#ProductEditDialogDiv').dialog("open"); 
            }
        });
    }

    function addEditProductComplete(xmlHttpRequest, textStatus) {

        var data = xmlHttpRequest.get_data();

        if (data.indexOf("E-d-i-t---S-u-c-c-e-s-s-f-u-l", 0) >= 0) {
            $('#ProductEditDialogDiv').dialog('close');
            //  call reload / refresh or any other post edit action.
        }
        else {
            $('#ProductEditDialogDiv').html(data);
            $("#EditProductButtons").hide();
        }

    }
</script>

As mentioned in the previous blog, since we’re only leveraging the extender pattern to make the code efficient, well structure and manageable, the naming convention a consistent naming convention is not just customary but mandatory. If not followed properly, the client side logic might get messy.

Finally, here’s dropping the extender as link on a random page:

    <p>
        <!--The link that will show the dialog-->
        <a href="javascript:openProductEditDialog('4C341BA3-E971-43C0-8BB1-07F51320E10B');">Edit product details</a>
        <!--The Dialog extender will sit here quitely-->
        <% Html.RenderPartial("ProductEditDialog", new DialogExtender()); %>
    </p>

And here’s, plugging it in a list:

<%@ Page Title="" Language="C#" MasterPageFile="~/Views/Shared/Site.Master" Inherits="System.Web.Mvc.ViewPage<IEnumerable<MVCDialogExtender.Models.Product>>" %>

<asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">

    <h2>List</h2>

    <table>
        <tr>
            <th></th>
            <th></th>
            <th>
                Name
            </th>
        </tr>

    <% foreach (var item in Model) { %>
        <tr>
            <td>
                <a href="javascript:openProductDetailsDialog('<%: item.Id %>');">Details</a>
            </td>
             <td>
                <a href="javascript:openProductEditDialog('<%: item.Id %>');">Edit</a>
            </td>
            <td>
                <%: item.Name %>
            </td>
        </tr>    
    <% } %>

    </table>
    <% Html.RenderPartial("ProductDetailsDialog", new DialogExtender()); %>
    <% Html.RenderPartial("ProductEditDialog", new DialogExtender()); %>
</asp:Content>

The results:

Link1

List1

 

The working Demo solution can be downloaded here.

Thoughts, opinions, ideas and comments welcomed.

8 comments:

  1. Hi,

    Nice post!

    I may have a question not directly related to your topic. I tried to move edit partial view out from dialog, which means the content index page contains all the fields and Ajax.BeginForm, but from here all the model validation (ValidationMessage) broke without showing up.

    Any clue why it works inside your dialog but not in a view directly?

    Thanks
    Hardy

    ReplyDelete
  2. Thanks Hardy.

    About your problem - I don’t think I’m getting your problem clearly.

    Are you saying you moved the edit form to the index to create some kind of detail view on the list itself and the validations are not being fired?

    Feel free to post / mail me (from my blogger profile) a sample code, I’ll try and help out the best I can.

    ReplyDelete
  3. Hi
    My name is Nat. I saw this tutorial. I am impressed. This is what I was looking for. But can u tell me how to add the reference to the DialogExtender. I am not finding it.

    Thanks

    ReplyDelete
  4. Thanks Nat, the idea behind is to load the form in a maintainable and efficient manner.

    You would need to create an extender for your form. Download the source code to refer to the working solution.

    Please ping back in case I didn't get your question right.

    ReplyDelete
  5. Thanks. I got it working.

    ReplyDelete
  6. Hi Srivastava

    One more clarification. I am getting an error in the ajax part when I try to implement. It get the html data using the controller, but errors out on the #div.dialog("open"). Can you tell me why I get object does not support this property.
    Thanks

    ReplyDelete
  7. I fixed the javascript error. I had to declare the dialog as a variable. for ex:
    var dialog = ${'#EditDialogDiv').... and then use the dialog variable in the other functions. I think the problem is the document ready function init is not visible to other functions like openEditDialog, addSaveDialog etc.

    ReplyDelete
  8. Great to know, you could also just refer to the div with -

    $("#divId").dialog("open");

    in your JS functions.

    ReplyDelete