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.

Sunday, September 12, 2010

jQuery dialog extender for Asp.Net MVC Views

“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.

This idea came to me during a discussion at work. The requirement was to create put an Add/Edit form in a dialog box. I’ve always been against the idea of rendering the markup inside the dialog div before hand, as the user should NOT pay the cost of a rendering something that might not be needed.

The idea went further to being able to provide the link to edit dialog any where in the application. This triggered the thought of the extender pattern that we use to ajaxify server controls in the web forms. So here’s the implementation, create a view then create a dialog extender for the view. The first example will be a regular view (without a form), the second (in the next blog) will be an edit form.

Extending a simple view (without a form in it):

As always, the application starts with a model:

    public class Product {

        public Guid Id { get; set; }
        public string Name { get; set; }
        public string Description { get; set; }
   }

Here’s a controller (with dummy data) for it:

    public class ProductsController : Controller {

        private List<Product> _data = new List<Product> {

                        new Product() { 
                                Id = new Guid("4C341BA3-E971-43C0-8BB1-07F51320E10B"), 
                                Name = "My New Product", 
                                Description = "This is a cool way to extend the jQuery UI dialog!" },

                        new Product() { 
                                Id = new Guid("D12C7542-99D0-4610-AB47-7A38F45CD026"), 
                                Name = "My New B", 
                                Description = "BBBeeeeeeeeeeeeeeeeeeeeeeeeeee!" },

                        new Product() { 
                                Id = new Guid("66B1F0EA-1230-4D62-9BE1-A5158BE10784"), 
                                Name = "My New C", 
                                Description = "CCCeeeeeeeeeeeeeeeeeeeeeeeeeee!" },

                        new Product() { 
                                Id = new Guid("959FB827-BBEE-47D8-9B4D-14BF3092A1F1"), 
                                Name = "My New D", 
                                Description = "DDDeeeeeeeeeeeeeeeeeeeeeeeeeee!" },

                        new Product() { 
                                Id = new Guid("C093608F-E252-4758-9924-62B2955C0AC5"), 
                                Name = "My New E", 
                                Desctiption = "EEEeeeeeeeeeeeeeeeeeeeeeeeeeee!" }
        };

        public ActionResult Details(Guid id) {

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

        public ActionResult Index() {

            return View();
        }
    }

And here’s the details view (Details.ascx):

<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<MVCDialogExtender.Models.Product>" %>
<fieldset>
    <legend>Fields</legend>
        
    <div class="display-label">Name</div>
    <div class="display-field"><%: Model.Name %></div>
        
    <div class="display-label">Description</div>
    <div class="display-field"><%: Model.Description %></div>
        
</fieldset>

The details view can be a view or a partial view. Here’s the extender to the view (ProductDetailsDialog.ascx). I will add this view in the Shared folder so that it can be accessed from any view across the application. Also, since this is going in the Shared folder, I will follow the naming convention - “<Controller name><Action Name>Dialog” .

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

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

<script type="text/javascript">

    $(document).ready(function () {
        $('#ProductDetailsDialogDiv').dialog({
            autoOpen: false,
            modal: true,
            close: function (event, ui) { $("#ProductDetailsDialogDiv").html(""); }
        });
    });
        $.ajax({
            type: "GET",
            url: encodeURI('<%= Url.Action("Details", "Products") %>' + "?id=" + id),
            cache: false,
            dataType: 'html',
            error: function (XMLHttpRequest, textStatus, errorThrown) {
                $("#ProductDetailsDialogDiv").html(errorThrown); 
            },
            success: function (data, textStatus, XMLHttpRequest) {
                $("#ProductDetailsDialogDiv").html(data); 
            },
            complete: function (XMLHttpRequest, textStatus) {
                $("#ProductDetailsCloseButton").click(function () { $('#ProductDetailsDialogDiv').dialog("close"); });
                $('#ProductDetailsDialogDiv').dialog("open"); 
            }
        });
    }

</script>

As soon in the view above, the dialog container DIV tag is emply an will be populated only when the user demands it. The javascript method responsible for populating and opening the dialog box will be called from outside this view so it will be handy to have a consistent naming convention, in my example I’ve chosen - “open<View Name>”. Again, since multiple such dialog may be present on the page I will follow the naming convention of “<View Name>Div” to avoid duplicate Id issue.

The detail extender inherits from a view model called DialogExtender which is an empty class right now but later if something needs to be added to the dialogs at a global level, the DialogExtender will be the way to go.

Finally here’s how we place a link to see the dialog extender:

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

OR in a list view

<%@ 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>
                Name
            </th>
        </tr>

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

    </table>

    <% Html.RenderPartial("ProductDetailsDialog", new DialogExtender()); %>

</asp:Content>

The results:

Link

List

 

The source code of this demo can be downloaded here.

With the intention to keep the blogs short I will be extending MVC forms in the next blog.

 

Thoughts, opinions, ideas and comments welcomed.