2013年10月21日 星期一

ASP.NET MVC 使用 jQuery EasyUI DataGrid - 顯示 Details(使用 PartialView)

有時候我們會遇到需要顯示 Master-Details 資料的需求,而 ASP.NET WebForm 的 GridView 也蠻多這種的範例,而通常使用者最希望顯示的 Master-Details 樣式 Grid 的某個 Row 下面去展開 Details 資料,例如:

image

from Expandable Rows in GridView - CodeProject

而使用 jQuery EasyUI DataGrid 也可以很容易就可以做出這樣的 Expand Row 的功能來顯示 Details 資料,而這次先說明如何使用 ASP.NET MVC 的 Partial View 來完成這次的功能。

 


我們在之前已經有做好一個顯示 Northwind – Category 資料的 DataGrid,

image

而我們可以在每個 Category 項目下增加展開的功能,以顯示該 Category 的所有 Product 資料。

 

參考 jQuery EasyUI 官方的 Tutorial 的內容:Expand row in DataGrid to show details

在 Toturial 內容裡,有個重要的檔案必須要加入使用,而這個檔案並未附加在我們所下載的 jQuery EasyUI 檔案裡,所以這個「datagrid-detailview.js」檔案就必須另外下載並複製到專案裡,

http://www.jeasyui.com/easyui/datagrid-detailview.js

var detailview = $.extend({}, $.fn.datagrid.defaults.view, {
    render: function(target, container, frozen){
        var state = $.data(target, 'datagrid');
        var opts = state.options;
        if (frozen){
            if (!(opts.rownumbers || (opts.frozenColumns && opts.frozenColumns.length))){
                return;
            }
        }
        
        var rows = state.data.rows;
        var fields = $(target).datagrid('getColumnFields', frozen);
        var table = [];
        table.push('<table class="datagrid-btable" cellspacing="0" cellpadding="0" border="0"><tbody>');
        for(var i=0; i<rows.length; i++) {
            // get the class and style attributes for this row
            var css = opts.rowStyler ? opts.rowStyler.call(target, i, rows[i]) : '';
            var classValue = '';
            var styleValue = '';
            if (typeof css == 'string'){
                styleValue = css;
            } else if (css){
                classValue = css['class'] || '';
                styleValue = css['style'] || '';
            }
            
            var cls = 'class="datagrid-row ' + (i % 2 && opts.striped ? 'datagrid-row-alt ' : ' ') + classValue + '"';
            var style = styleValue ? 'style="' + styleValue + '"' : '';
            var rowId = state.rowIdPrefix + '-' + (frozen?1:2) + '-' + i;
            table.push('<tr id="' + rowId + '" datagrid-row-index="' + i + '" ' + cls + ' ' + style + '>');
            table.push(this.renderRow.call(this, target, fields, frozen, i, rows[i]));
            table.push('</tr>');
            
            table.push('<tr style="display:none;">');
            if (frozen){
                table.push('<td colspan=' + (fields.length+2) + ' style="border-right:0">');
            } else {
                table.push('<td colspan=' + (fields.length) + '>');
            }
            table.push('<div class="datagrid-row-detail">');
            if (frozen){
                table.push('&nbsp;');
            } else {
                table.push(opts.detailFormatter.call(target, i, rows[i]));
            }
            table.push('</div>');
            table.push('</td>');
            table.push('</tr>');
            
        }
        table.push('</tbody></table>');
        
        $(container).html(table.join(''));
    },
    
    renderRow: function(target, fields, frozen, rowIndex, rowData){
        var opts = $.data(target, 'datagrid').options;
        
        var cc = [];
        if (frozen && opts.rownumbers){
            var rownumber = rowIndex + 1;
            if (opts.pagination){
                rownumber += (opts.pageNumber-1)*opts.pageSize;
            }
            cc.push('<td class="datagrid-td-rownumber"><div class="datagrid-cell-rownumber">'+rownumber+'</div></td>');
        }
        for(var i=0; i<fields.length; i++){
            var field = fields[i];
            var col = $(target).datagrid('getColumnOption', field);
            if (col){
                var value = rowData[field];    // the field value
                var css = col.styler ? (col.styler(value, rowData, rowIndex)||'') : '';
                var classValue = '';
                var styleValue = '';
                if (typeof css == 'string'){
                    styleValue = css;
                } else if (cc){
                    classValue = css['class'] || '';
                    styleValue = css['style'] || '';
                }
                var cls = classValue ? 'class="' + classValue + '"' : '';
                var style = col.hidden ? 'style="display:none;' + styleValue + '"' : (styleValue ? 'style="' + styleValue + '"' : '');
                
                cc.push('<td field="' + field + '" ' + cls + ' ' + style + '>');
                
                if (col.checkbox){
                    style = '';
                } else if (col.expander){
                    style = "text-align:center;height:16px;";
                } else {
                    style = styleValue;
                    if (col.align){style += ';text-align:' + col.align + ';'}
                    if (!opts.nowrap){
                        style += ';white-space:normal;height:auto;';
                    } else if (opts.autoRowHeight){
                        style += ';height:auto;';
                    }
                }
                
                cc.push('<div style="' + style + '" ');
                if (col.checkbox){
                    cc.push('class="datagrid-cell-check ');
                } else {
                    cc.push('class="datagrid-cell ' + col.cellClass);
                }
                cc.push('">');
                
                if (col.checkbox){
                    cc.push('<input type="checkbox" name="' + field + '" value="' + (value!=undefined ? value : '') + '">');
                } else if (col.expander) {
                    //cc.push('<div style="text-align:center;width:16px;height:16px;">');
                    cc.push('<span class="datagrid-row-expander datagrid-row-expand" style="display:inline-block;width:16px;height:16px;cursor:pointer;" />');
                    //cc.push('</div>');
                } else if (col.formatter){
                    cc.push(col.formatter(value, rowData, rowIndex));
                } else {
                    cc.push(value);
                }
                
                cc.push('</div>');
                cc.push('</td>');
            }
        }
        return cc.join('');
    },
    
    insertRow: function(target, index, row){
        var opts = $.data(target, 'datagrid').options;
        var dc = $.data(target, 'datagrid').dc;
        var panel = $(target).datagrid('getPanel');
        var view1 = dc.view1;
        var view2 = dc.view2;
        
        var isAppend = false;
        var rowLength = $(target).datagrid('getRows').length;
        if (rowLength == 0){
            $(target).datagrid('loadData',{total:1,rows:[row]});
            return;
        }
        
        if (index == undefined || index == null || index >= rowLength) {
            index = rowLength;
            isAppend = true;
            this.canUpdateDetail = false;
        }
        
        $.fn.datagrid.defaults.view.insertRow.call(this, target, index, row);
        
        _insert(true);
        _insert(false);
        
        this.canUpdateDetail = true;
        
        function _insert(frozen){
            var v = frozen ? view1 : view2;
            var tr = v.find('tr[datagrid-row-index='+index+']');
            
            if (isAppend){
                var newDetail = tr.next().clone();
                tr.insertAfter(tr.next());
            } else {
                var newDetail = tr.next().next().clone();
            }
            newDetail.insertAfter(tr);
            newDetail.hide();
            if (!frozen){
                newDetail.find('div.datagrid-row-detail').html(opts.detailFormatter.call(target, index, row));
            }
        }
    },
    
    deleteRow: function(target, index){
        var opts = $.data(target, 'datagrid').options;
        var dc = $.data(target, 'datagrid').dc;
        var tr = opts.finder.getTr(target, index);
        tr.next().remove();
        $.fn.datagrid.defaults.view.deleteRow.call(this, target, index);
        dc.body2.triggerHandler('scroll');
    },
    
    updateRow: function(target, rowIndex, row){
        var dc = $.data(target, 'datagrid').dc;
        var opts = $.data(target, 'datagrid').options;
        var cls = $(target).datagrid('getExpander', rowIndex).attr('class');
        $.fn.datagrid.defaults.view.updateRow.call(this, target, rowIndex, row);
        $(target).datagrid('getExpander', rowIndex).attr('class',cls);
        
        // update the detail content
        if (this.canUpdateDetail){
            var row = $(target).datagrid('getRows')[rowIndex];
            var detail = $(target).datagrid('getRowDetail', rowIndex);
            detail.html(opts.detailFormatter.call(target, rowIndex, row));
        }
    },
    
    bindEvents: function(target){
        var state = $.data(target, 'datagrid');
        var dc = state.dc;
        var opts = state.options;
        var body = dc.body1.add(dc.body2);
        var clickHandler = ($.data(body[0],'events')||$._data(body[0],'events')).click[0].handler;
        body.unbind('click').bind('click', function(e){
            var tt = $(e.target);
            var tr = tt.closest('tr.datagrid-row');
            if (!tr.length){return}
            if (tt.hasClass('datagrid-row-expander')){
                var rowIndex = parseInt(tr.attr('datagrid-row-index'));
                if (tt.hasClass('datagrid-row-expand')){
                    $(target).datagrid('expandRow', rowIndex);
                } else {
                    $(target).datagrid('collapseRow', rowIndex);
                }
                $(target).datagrid('fixRowHeight');
                
            } else {
                clickHandler(e);
            }
            e.stopPropagation();
        });
    },
    
    onBeforeRender: function(target){
        var state = $.data(target, 'datagrid');
        var opts = state.options;
        var dc = state.dc;
        var t = $(target);
        var hasExpander = false;
        var fields = t.datagrid('getColumnFields',true).concat(t.datagrid('getColumnFields'));
        for(var i=0; i<fields.length; i++){
            var col = t.datagrid('getColumnOption', fields[i]);
            if (col.expander){
                hasExpander = true;
                break;
            }
        }
        if (!hasExpander){
            if (opts.frozenColumns && opts.frozenColumns.length){
                opts.frozenColumns[0].splice(0,0,{field:'_expander',expander:true,width:24,resizable:false,fixed:true});
            } else {
                opts.frozenColumns = [[{field:'_expander',expander:true,width:24,resizable:false,fixed:true}]];
            }
            
            var t = dc.view1.children('div.datagrid-header').find('table');
            var td = $('<td rowspan="'+opts.frozenColumns.length+'"><div class="datagrid-header-expander" style="width:24px;"></div></td>');
            if ($('tr',t).length == 0){
                td.wrap('<tr></tr>').parent().appendTo($('tbody',t));
            } else if (opts.rownumbers){
                td.insertAfter(t.find('td:has(div.datagrid-header-rownumber)'));
            } else {
                td.prependTo(t.find('tr:first'));
            }
        }
        
        var that = this;
        setTimeout(function(){
            that.bindEvents(target);
        },0);
    },
    
    onAfterRender: function(target){
        var that = this;
        var state = $.data(target, 'datagrid');
        var dc = state.dc;
        var opts = state.options;
        var panel = $(target).datagrid('getPanel');
        
        $.fn.datagrid.defaults.view.onAfterRender.call(this, target);
        
        if (!state.onResizeColumn){
            state.onResizeColumn = opts.onResizeColumn;
        }
        if (!state.onResize){
            state.onResize = opts.onResize;
        }
        function setBodyTableWidth(){
            var columnWidths = dc.view2.children('div.datagrid-header').find('table').width();
            dc.body2.children('table').width(columnWidths);
        }
        
        opts.onResizeColumn = function(field, width){
            setBodyTableWidth();
            var rowCount = $(target).datagrid('getRows').length;
            for(var i=0; i<rowCount; i++){
                $(target).datagrid('fixDetailRowHeight', i);
            }
            
            // call the old event code
            state.onResizeColumn.call(target, field, width);
        };
        opts.onResize = function(width, height){
            setBodyTableWidth();
            state.onResize.call(panel, width, height);
        };
        
        this.canUpdateDetail = true;    // define if to update the detail content when 'updateRow' method is called;
        
        dc.footer1.find('span.datagrid-row-expander').css('visibility', 'hidden');
        $(target).datagrid('resize');
    }
});
 
$.extend($.fn.datagrid.methods, {
    fixDetailRowHeight: function(jq, index){
        return jq.each(function(){
            var opts = $.data(this, 'datagrid').options;
            if (!(opts.rownumbers || (opts.frozenColumns && opts.frozenColumns.length))){
                return;
            }
            var dc = $.data(this, 'datagrid').dc;
            var tr1 = opts.finder.getTr(this, index, 'body', 1).next();
            var tr2 = opts.finder.getTr(this, index, 'body', 2).next();
            // fix the detail row height
            if (tr2.is(':visible')){
                tr1.css('height', '');
                tr2.css('height', '');
                var height = Math.max(tr1.height(), tr2.height());
                tr1.css('height', height);
                tr2.css('height', height);
            }
            dc.body2.triggerHandler('scroll');
        });
    },
    getExpander: function(jq, index){    // get row expander object
        var opts = $.data(jq[0], 'datagrid').options;
        return opts.finder.getTr(jq[0], index).find('span.datagrid-row-expander');
    },
    // get row detail container
    getRowDetail: function(jq, index){
        var opts = $.data(jq[0], 'datagrid').options;
        var tr = opts.finder.getTr(jq[0], index, 'body', 2);
        return tr.next().find('div.datagrid-row-detail');
    },
    expandRow: function(jq, index){
        return jq.each(function(){
            var opts = $(this).datagrid('options');
            var dc = $.data(this, 'datagrid').dc;
            var expander = $(this).datagrid('getExpander', index);
            if (expander.hasClass('datagrid-row-expand')){
                expander.removeClass('datagrid-row-expand').addClass('datagrid-row-collapse');
                var tr1 = opts.finder.getTr(this, index, 'body', 1).next();
                var tr2 = opts.finder.getTr(this, index, 'body', 2).next();
                tr1.show();
                tr2.show();
                $(this).datagrid('fixDetailRowHeight', index);
                if (opts.onExpandRow){
                    var row = $(this).datagrid('getRows')[index];
                    opts.onExpandRow.call(this, index, row);
                }
            }
        });
    },
    collapseRow: function(jq, index){
        return jq.each(function(){
            var opts = $(this).datagrid('options');
            var dc = $.data(this, 'datagrid').dc;
            var expander = $(this).datagrid('getExpander', index);
            if (expander.hasClass('datagrid-row-collapse')){
                expander.removeClass('datagrid-row-collapse').addClass('datagrid-row-expand');
                var tr1 = opts.finder.getTr(this, index, 'body', 1).next();
                var tr2 = opts.finder.getTr(this, index, 'body', 2).next();
                tr1.hide();
                tr2.hide();
                dc.body2.triggerHandler('scroll');
                if (opts.onCollapseRow){
                    var row = $(this).datagrid('getRows')[index];
                    opts.onCollapseRow.call(this, index, row);
                }
            }
        });
    }
});

image

datagrid-detailview.js 是 jQuery EasyUI DataGrid 的一個擴展方法,讓 DataGrid 可以在每個 Row 去增加可以展開以及收合的區塊。

而要讓 DataGrid 可以使用 detailview,則要在 DataGrid 的 view 這個 property 裡去指定「detailview」,然後再去指定 datagrid-detailview 所提供的方法,所增加設定的詳細內容如下:

image

$(function () {
    $('#DataGrid').datagrid({
        title: 'Northwind - Category',
        url: '@Url.Action("GetGridJSON", "Category")',
        width: '800',
        height: '400',
        rownumbers: true,
        columns: [[
            { field: 'CategoryID', title: 'ID' },
            { field: 'CategoryName', title: 'Name' },
            { field: 'Description', title: 'Description', width: 600 }
        ]],
        view: detailview,
        detailFormatter: function (index, row) {
            return '<div id="ddv-' + index + '" style="padding:5px;"></div>';
        },
        onExpandRow: function (index, row) {
            $('#ddv-' + index).panel({
                border: false,
                cache: false,
                href: '@Url.Action("GetByCategory", "Product", new { categoryId = "_id_" })'
                    .replace("_id_", row.CategoryID),
                onLoad: function () {
                    $('#DataGrid').datagrid('fixDetailRowHeight', index);
                }
            });
            $('#DataGrid').datagrid('fixDetailRowHeight', index);
        }
    });
});

要特別說明的是,在 Expand Row 裡有使用了 jQuery EasyUI 的另一個 plugin「Panel」,使用 href 這個 property 來載入後端所產生的 Partial View 內容並顯示於 panel 裡。

jQuery EasyUI Panel – Documentation

 

前端程式準備好之後,接著就是要完成取得 Product 資料的後端程式,這邊的作法很簡單,在 ProductController 裡建立 GetByCategory 方法,當收到前端所傳過來的 CategoryID 後,就取得相關的 Product 資料,然後回傳一個將 Product 資料以 Table 呈現的 Partial View 送回前端,這個地方就不須特別再去多做解釋了,

image

Partial View : _GetByCategory.cshtml

@model IEnumerable<BlogSample.Models.Product>
 
<style type="text/css">
    .dv-table td
    {
        border: 0;
        vertical-align: middle;
    }
</style>
 
<table class="dv-table table table-striped">
    <thead>
        <tr style="background-color: #f5f5dc;">
            <th>
                @Html.DisplayNameFor(model => model.ProductName)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.QuantityPerUnit)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UnitPrice)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UnitsInStock)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.UnitsOnOrder)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.ReorderLevel)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Discontinued)
            </th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr style="height: 30px; line-height: 30px;">
                <td>
                    @Html.DisplayFor(modelItem => item.ProductName)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.QuantityPerUnit)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.UnitPrice)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.UnitsInStock)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.UnitsOnOrder)
                </td>
                <td>
                    @Html.DisplayFor(modelItem => item.ReorderLevel)
                </td>
                <td>
                    @(item.Discontinued ? "True" : "False")
                </td>
            </tr>
        }
    </tbody>
</table>

 

執行結果:

jquery_easyui_datagrid_20131021_01


其實這個用 Partial View 來呈現 Detail 資料的作法會比較適合用在要呈現單筆資料的時候,例如產品資料,因為以 Grid 所呈現的產品資料無法清楚表達,例如產品的圖片、細節等,所以可以用 Partial View 的方式,清楚地呈現產品詳細內容,例如 jQuery EasyUI DataGrid 在 Toturial 裡的範例,

View Demo

image

而如果以這篇文章的範例來看,在 Detail View 內所呈現的 Product 資料還是用 DataGrid 會比較好,而下一篇文章就來說明這部份的作法。

 

以上

沒有留言:

張貼留言

提醒

千萬不要使用 Google Talk (Hangouts) 或 Facebook 及時通訊與我聯繫、提問,因為會掉訊息甚至我是過了好幾天之後才發現到你曾經傳給我訊息過,請多多使用「詢問與建議」(在左邊,就在左邊),另外比較深入的問題討論,或是有牽涉到你實作程式碼的內容,不適合在留言板裡留言討論,請務必使用「詢問與建議」功能(可以夾帶檔案),謝謝。