Friday, October 11, 2013

Expanding jQuery datatable not working in IE 10

In previous post, we put master data in jQuery datatable and when user expands a row in master data we show detail data under the row. So when expanded, the inner detail rows are inserted as shown below. Please note that this actually increased the height of datatable section.



In most of browsers (including IE 9) this works fine, but in IE 10.0 I ran into the case that the detail expansion actually did not expand the datatable. It only added vertical scroll bar to datatable and the UI looked ugly as below.



So how to solve this problem?
If end user clicks IE 10.0 compatibility mode icon (red circle in the picture above), it will resolve the issue.
But since developer better not to force end user to do that, the following meta tag section can be added to resolve the issue. That is, add X-UA-Compatible meta tag at the first position of head section.

<html lang="en">
    <head>
        <meta http-equiv="X-UA-Compatible" content="IE=EmulateIE9" />  
        <meta charset="utf-8" />
        <title>@ViewBag.Title - My ASP.NET MVC Application</title>


Thursday, October 10, 2013

Simple MVC Example - Master/Detail drill-down in jQuery datatable

This post shows a simple ASP.NET MVC example where master data table is displayed in initial state and when user clicks expand icon it drills in detail table under the selected row.



The below steps are logical flow to implement this master-detail relationship in a web page.

1) Let's first look at a very simple main input form as a staring point. In the input view (Home/Index), we have two textboxes for input date range. Once submitted, it calls Home/ResultView as defined in BeginForm().

@using (Html.BeginForm("ResultView", "Home", FormMethod.Post))
{
   <div>Start Date: @Html.TextBox("StartDate")</div>
   <div>End Date: @Html.TextBox("EndDate")</div>
   <input type="submit" />    
}

2) In Home Controller, ResultView() method handles form input and gets order data for given input order dates. The method calls repository's GetOrders() method, explained in Step 4.

public class HomeController : Controller
{
    private Repository _repository = new Repository();

    public ActionResult Index()
    {
        return View();
    }

    [HttpPost]
    [ValidateInput(false)]
    public ActionResult ResultView(FormCollection collection)
    {
        DateTime start = DateTime.Parse(collection["StartDate"]);
        DateTime end = DateTime.Parse(collection["EndDate"]);

        var model = _repository.GetOrders(start, end);
        return View(model);
    }

    public ActionResult ResultDetailView(int id)
    {
        var model = _repository.GetOrderItems(id);
        return PartialView(model);
    }
}

3) In Model, there are two entities - Order and OrderItem. Order (master) class contains multiple OrderItems (details or children).

public class Order
{
    public int OrderId { get; set; }
    public DateTime OrderDate { get; set; }
    public int CustomerId { get; set; }        
    public IEnumerable<orderitem> OrderItems { get; set; }
}

public class OrderItem
{
    public int OrderId { get; set; }
    public int Seq { get; set; }
    public int ProductId { get; set; }        
    public int Qty { get; set; }
}

4) Based on Order/OrderItem classes, we have a repository class (optional, but good to have) that handles GET requests from Controller. As said in Step 2, GetOrders() method call from Controller's ResultView() method collects sample orders for given dates.

public class Repository
{
    // Sample data
    private List<Order> _orders = new List<Order>()
        {
            new Order
                {
                    OrderId = 1,
                    OrderDate = new DateTime(2013, 10, 1),
                    CustomerId = 100,
                    OrderItems = new []
                        {
                            new OrderItem {OrderId = 1, Seq = 1, ProductId = 101, Qty = 1},
                            new OrderItem {OrderId = 1, Seq = 2, ProductId = 201, Qty = 2},
                            new OrderItem {OrderId = 1, Seq = 3, ProductId = 301, Qty = 3},
                        }
                },
            new Order
                {
                    OrderId = 2,
                    OrderDate = new DateTime(2013, 10, 2),
                    CustomerId = 200,
                    OrderItems = new []
                        {
                            new OrderItem {OrderId = 2, Seq = 1, ProductId = 401, Qty = 4},
                            new OrderItem {OrderId = 2, Seq = 2, ProductId = 501, Qty = 5}                                
                        }
                }
        };

    public IEnumerable<Order> GetOrders(DateTime startDate, DateTime endDate)
    {
        return _orders.Where(p => p.OrderDate >= startDate && p.OrderDate <= endDate)
                .Select(o => new Order { OrderId = o.OrderId, 
                    OrderDate = o.OrderDate, 
                    CustomerId = o.CustomerId} );            
    }

    public IEnumerable<OrderItem> GetOrderItems(int orderId)
    {
        var ord = _orders.SingleOrDefault(p => p.OrderId == orderId);
        return (ord != null) ? ord.OrderItems : null;
    }
}

5) After getting order data from repository, Step 2 calls ResultView.cshtml with the order data and this view shows orders (Orders only without Order Items!) in jQuery datatable. The view has a table called ResultTable and it is converted to jQuery ui datatable in $('#ResultTable').dataTable() method call as shown in the first part of javascript below.

@using MvcApplication1.Models
@model IEnumerable<Order>
@{
    ViewBag.Title = "Result View";
    Layout = "~/Views/Shared/_Layout.cshtml";
}

@{    
    <table id="ResultTable">
        <thead>  
            <tr>  
                <th>&nbsp;</th>
                <th>OrderId</th>
                <th>OrderDate</th>
                <th>CustomerId</th>                    
            </tr>  
        </thead>  
        <tbody>
            @foreach (var order in Model)
            {                                
                <tr>  
                    <td>
                        <img class="expand" src="@Url.Content("~/Content/open.png")" 
                            alt="Expand/Collapse" rel="@order.OrderId" />
                    </td>                                                           
                    <td>@order.OrderId</td>
                    <td>@order.OrderDate</td>
                    <td>@order.CustomerId</td>
                </tr>                                    
            }
        </tbody>
    </table>
    
    <div id="DetailsDialog"></div>
}
  
<script type="text/javascript">
$(document).ready(function () {
    var resultTable = $('#ResultTable').dataTable({
        "bJQueryUI": "true",        
        "sScrollX": "100%",        
        "sPaginationType": "full_numbers"
    });       
    
    $('#ResultTable tbody td img.expand').click(function () {
        var nTr = this.parentNode.parentNode;        
        if (this.src.match('close')) {
            this.src = "@Url.Content("~/Content/open.png")";
            resultTable.fnClose(nTr);
        } else {
            this.src = "@Url.Content("~/Content/close.png")";
            var orderId = $(this).attr("rel");
            var url = "@Url.Action("ResultDetailView", "Home")";
            $.get(url, { id: orderId }, function (details) {                
                resultTable.fnOpen(nTr, details, 'Details');
            });
        }
    });    
    // DetailsDialog Section
    var detailsDialog = $("#DetailsDialog").dialog({
        autoOpen: false,
        modal: true,
        title: "Details",        
        width: "auto"
    });
    $("#ResultTable").on('click', 'a.productLink', function () {        
        var productId = $(this).text();                
        detailsDialog.html(productId);                
        detailsDialog.dialog('open');
    });
});
</script>

6) In javascript above, $('#ResultTable tbody td img.expand').click() detects user click on the image icon. If image is clicked, it checks to see if the image src attribute has a substring match with (close). This pattern matching is simply to determine what the current icon name is, say, open,png or close.png. If it is open.png, we change the icon to close.png and makes an AJAX calls to Home/ResultDetailView with order id parameter. Please note that we used (id) parameter name in $.get() and that should match the parameter name of ResultDetailView(int id) method in Controller.

7) So AJAX call in Step 6 goes to ResultDetailView() method in Controller. It fetches order items (detail) with a given order id. Once data is retrieved, the partial view for order details will be generated with this view template.

@model IEnumerable<MvcApplication1.Models.OrderItem>

<table>            
    <tr>
        <th>Seq</th>   
        <th>ProductId</th>   
        <th>Qty</th>   
    </tr>    
        
    @foreach (var item in Model)
    {            
        <tr>                              
            <td>@item.Seq</td>
            <td>
                <a href="#" class="productLink">@item.ProductId</a>
            </td>
            <td>@item.Qty</td> 
        </tr>
    }    
</table>

8) Once Step 7 partial view is returned, the AJAX call in $.get (Step 5) goes to resultTable.fnOpen(nTr, details, 'Details') code to insert the partial view html snippet under the (nTr) tr tag. This simulates drill-down (or drill-in) behavior.

As a side note, what if we want to add a click event onto dynamically generated OrderItems detail table section? For example, the second column in order detail table (see Step 7) has a link tag. If user clicks this ProductId, we want to display a dialog box containing some product info. To add click event, normally we adds click() event onto a tag directly. So we can code this way.

$('a.productLink').click(...);

However, this code will not work because the event a tag whose class is productLink does not exist when the jQuery runs (aka when the document is ready). The drill down section is dynamically generated only when user clicks the expand icon.

In order to make this work, we attach the event onto any parent (or any ancestor that already exists during .on() method call) by using .on() method as shown in Step 5 script.

 $("#ResultTable").on('click', 'a.productLink', function ()...)

Selected element #ResultTable table does exist when .on() method is called. We put click event there with inner target element (a.productLink) specified in 2nd parameter. So when and only when user clicks on the inner target element (a tag), click event bubbles up to #ResultTable and executes delegated event handler. If 2nd parameter is null or omitted, it's called direct event handler where the event fires whenever the selected element (#ResultTable) or its descendant element is clicked.