Monday, February 6, 2012

Showing ASP.NET MVC 3.0 Views in jQuery ui dialog & Posting MVC forms from jQuery ui dialog

“I blogged on this topic in September 2010 and received a few mail about showing how to add new entities (as opposed to update) and also an implementation of this in MVC 3.0 using Razor. While the concepts and explanation in the previous blogs still hold correct, here’s an implementation using MVC 3.0 and Razor with some improvements. My apologies for not posting this blog as sooner; also really appreciate your comments & mails.”
The requirement I was given was, to show the form to Add/Edit Products and the details view, in a jQuery UI dialog. Also these dialogs were to be shown at many places all across the application. I certainly did not want to render views by default, everywhere the dialog was to be shown. So I created extenders which would have scripts to load the view when needed.
To start with, the scripts that we would need to have in the layout would be jQuery and jQuery UI with the style sheet. Additional scripts will be needed for Ajax form posting but We’ll be including them in the form itself.
Here’s some basic infrastructure -
The Product class-
public class Product {
    
    public int Id { get; set; }
    
    [Required]
    public string Name { get; set; }
    
    [Required]
    public string Description { get; set; }

}
And a product’s service for CRUD operations
public class ProductService {


    private readonly string ProductsInSession = "ProductsInSession";

    /// <summary>
    /// Some Dummy products
    /// </summary>
    private List<Product> _initialData {
        get {
            return new List<Product> {

                new Product() { 
                        Id = 1, 
                        Name = "My New Product", 
                        Description = "This is a cool way to extend the jQuery UI dialog!" },

                new Product() { 
                        Id = 2,
                        Name = "My New B", 
                        Description = "Details for Product Beee!" },

                new Product() { 
                        Id = 3,
                        Name = "My New C", 
                        Description = "Details for Product Ceee!" },

                new Product() { 
                        Id = 4,
                        Name = "My New D", 
                        Description = "Details for Product Deee!" },

                new Product() { 
                        Id = 5,
                        Name = "My New E", 
                        Description = "Details for Product Eeee!" }
            };
        }
    }
    
    /// <summary>
    /// Get and Set List of products from and to the session
    /// </summary>
    private List<Product> _Products {
        get {
            List<Product> data = null;
            if (HttpContext.Current.Session[ProductsInSession] == null) {
                data = _initialData;
                HttpContext.Current.Session[ProductsInSession] = data;
            }
            else {
                data = HttpContext.Current.Session[ProductsInSession] as List<Product>;
            }
            return data;
        }
        set {
            HttpContext.Current.Session[ProductsInSession] = value;
        }
    }


    public List<Product> GetProducts() {
        return _Products;
    }


    public Product GetProduct(int id) {
        return _Products.Single(p => p.Id == id);
    }


    public void AddUpdateProduct(Product product) {
        // Could definitely do better that this but the demo is about the dialog
        var products = _Products;
        if (product.Id == 0) {
            product.Id = _Products.Max(p => p.Id) + 1;
            products.Add(product);
        }
        else {
            products.Single(p => p.Id == product.Id).Name = product.Name;
            products.Single(p => p.Id == product.Id).Description = product.Description;
        }
        //Save back to session
        _Products = products;
    }

}
Displaying Non-Form views -
Here’s the Action method from the ProductController -
public ActionResult Details(int id) {

    return View(_productService.GetProduct(id));
}
The View -
@model DialogExtender_MVC3.Models.Product

<fieldset>
    <legend>Product</legend>

    <div class="display-label">Name</div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Name)
    </div>

    <div class="display-label">Description</div>
    <div class="display-field">
        @Html.DisplayFor(model => model.Description)
    </div>
</fieldset>
The and Extender with the script to load the data (I’m calling it ProductDetailsDialog and putting it in the shared folder)
<div id="ProductDetailsDialogDiv" title="Product details" style="display: none;">
</div>

<script type="text/javascript">

    function openProductDetailsDialog(id) {

        $.ajax({
            type: "GET",
            url: encodeURI('@Url.Action("Details", "Product")' + "?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) {

                $('#ProductDetailsDialogDiv').dialog({
                    modal: true,
                    width: "300px",
                    close: function (event, ui) { $("#ProductDetailsDialogDiv").html(""); },
                    buttons: {
                        "Ok": function () { $(this).dialog("close"); }
                    }
                });
            }
        });
    }

</script>
The openProductDetailsDialog received the product Id and calls the Details action and load whet ever it gets in the DIV ProductDetailsDialogDiv. and finally show the DIV in a dialog. The DIV is cleared when the dialog is closed. It’s always a good practice to use $.ajax (as opposed to $.get() or $.load()) as .ajax gives us handlers for error enabling better control for debugging and maintenance.
And here’s how we invoke it  - just place the two lines of markup in any view in the application -
<!--The link that will show the dialog-->
<a href="javascript:openProductDetailsDialog(1);">Product detail</a>
<!--The Dialog extenders will sit here quietly unless asked to do something-->
@Html.Partial("ProductDetailsDialog")

Displaying Form views -
To the same trick for forms we would have to take care of showing the validations in the dialog itself.
The action should send back the form if any validations have failed and something to notify that the post is successful if all the validations passed.
Here’s that something – I created a one line View Called AddEditSuccess.cshtml that looks like -
"E-d-i-t---S-u-c-c-e-s-s-f-u-l"
and placed it in the shared folder.
I created a ProductViewModel class with one member – Product. In a practical scenario this view model will have a lot more stuff than what is has now.
public class ProductViewModel {

    public Product Product { get; set; }
}
Here’s the AddEditAction from ProductController -
public ActionResult AddEdit(int? id) {

    if (id.HasValue && id.Value != 0) {
        //Edit an existing Product
        return View(new ProductViewModel { Product = _productService.GetProduct(id.Value) });
    }
    else {
        //Add a new Product
        return View(new ProductViewModel { Product = new Product { Id = 0 } });
    }
}


[HttpPost]
public ActionResult AddEdit(ProductViewModel vm) {

    if (ModelState.IsValid) {

        _productService.AddUpdateProduct(vm.Product);
        return PartialView("AddEditSuccess");
    }

    return View(vm);
}
Note that we identify whether it is a add or edit based on the availability of the primary key. A similar logic sits in the Product service too which is why the same action and service method is used to achieve both adding and editing products.
Here’s the AddEdit view
@model DialogExtender_MVC3.Models.ProductViewModel

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

@using (Ajax.BeginForm(new AjaxOptions { OnComplete = "addEditProductComplete" })) {
    
    @Html.ValidationSummary(true)
    
    <fieldset>
        <legend>Product</legend>

        @Html.HiddenFor(model => model.Product.Id)

        <div class="editor-label">
            @Html.LabelFor(model => model.Product.Name)
        </div>
        <div class="editor-field">
            @Html.EditorFor(model => model.Product.Name)
            @Html.ValidationMessageFor(model => model.Product.Name)
        </div>

        <div class="editor-label">
            @Html.LabelFor(model => model.Product.Description)
        </div>
        <div class="editor-field">
            @Html.TextAreaFor(model => model.Product.Description)
            @Html.ValidationMessageFor(model => model.Product.Description)
        </div>

        <p>
            <input type="submit" value="Save" />
        </p>

    </fieldset>
}
For the Ajax options and client validations to work we need the scripts – jquery.unobtrusive-ajax, jquery.validate, jquery.validate.unobtrusive. The Ajax options in the form will call the JavaScript method – addEditProductComplete after the response from the form post is received. This method checks if the form was successfully posted or not. If not it keep the dialog in place and updated the contents of the dialog.
I’m calling this ProductAddEditDialog and keeping it in the shared folder. For naming the dialog extenders I’m using the format <ObjectTypeName><ActionName>Dialog just to keep things consistent. You can choose your own naming conventions. Here’s the ProductAddEditDialog.cshtml -
<div id="ProductEditDialogDiv" title="Product" style="display: none;">
</div>

<script type="text/javascript">

    function openProductEditDialog(id) {

        $("#ProductDetailsDialogDiv").html("");

        $.ajax({
            type: "GET",
            url: encodeURI('@Url.Action("AddEdit", "Product")' + "?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) {
                
                $('#ProductEditDialogDiv').dialog({
                    width: '500px',
                    modal: true
                });
            }
        });
    }

    function addEditProductComplete(xmlHttpRequest, textStatus) {
        
        var data = xmlHttpRequest.responseText;

        if (data.indexOf("E-d-i-t---S-u-c-c-e-s-s-f-u-l", 0) >= 0) {
            $("#ProductDetailsDialogDiv").html("");
            $('#ProductEditDialogDiv').dialog('close');
            //  call reload / refresh or any other post edit action.
        }
        else {
            $('#ProductEditDialogDiv').html(data);
            $("#EditProductButtons").hide();
        }
    }
</script>
And just like the non-form dialogs, here’s the two lines of markup that will show your dialog anywhere you want -
<a href="javascript:openProductEditDialog(1);">Edit Product</a>
@Html.Partial("ProductAddEditDialog")
To provide  the link to add, all we need is to pass a 0 or null or blank to the openProductEditDialog method -
<a href="javascript:openProductEditDialog('');">Add Product</a>
@Html.Partial("ProductAddEditDialog")
A Demo Solution for the post can be downloaded here.
Any comments/suggestions welcomed.

6 comments:

  1. would you like to help me, to convert a demo solution from c# to vb ?

    ReplyDelete
  2. Would you like to help me ? now i only can programming with vb language, please convert a demo solution with vb language for me ?

    ReplyDelete
  3. Hello,

    Thank you for your article. I have implemented it and it is working great, but the jquery ui overlay that is supposed to show when the dialog opens is not being displayed. Any ideas how to get this to work with this setup?

    Thank you,

    Frankie

    ReplyDelete
    Replies
    1. Hi Frankie,
      Just calling the $(“”).dialog({. . .}); should show the dialog and adding “modal: true” to the options would make it a modal (block the parent page).
      I’m not sure if I follow your question completely but take a look at the demo solution, it might be helpful to see a working example.

      Delete
  4. Hi Mayank,

    Thank you for your response. I'm not sure if I went about it the right way but I ended up adding a div (as an overlay) to my partial view (where the dialog html gets added) and then toggling the display when the dialog is opened and closed.

    div class="overlaydiv"
    .overlaydiv
    {
    background-color: #000000;
    display: none;
    height: 100%;
    -moz-opacity: 0.7;
    opacity:.70;
    filter: alpha(opacity=70);
    left: 0%;
    position: fixed;
    top: 0%;
    width: 100%;
    z-index:1001;
    }


    I also had to adjust the z index of my dialog so that it is:

    .ui-dialog
    {
    z-index: 9999 !important;
    }

    ...because the overlay was on top of my dialog at first. I'm using MVC 4 so I'm not sure if that has anything to do with it, but I had the dialog as "model:true" and the overlay still wasn't working. I wasn't able to find the overlay in the DOM when looking at it in Firebug, so it was like it wasn't adding the overlay at all when the dialog opened.

    Anyways, I did get it to work.

    Thank you :)

    ReplyDelete
  5. This is a very nice article on "CRUD Operations Using Entity Framework in ASP.NET MVC". I have find out some other articles link from which I have learnt "CURD operations using Ajax Model Dialog in ASP.NET MVC!". I think this is very helpful for developers like me.

    http://www.mindstick.com/Articles/279bc324-5be3-4156-a9e9-dd91c971d462/?CRUD%20operation%20using%20Modal%20d

    http://www.dotnet-tricks.com/Tutorial/mvc/42R0171112-CRUD-Operations-using-jQuery-dialog-and-Entity-Framework---MVC-Razor.html

    ReplyDelete