2012年3月9日 星期五

ASP.NET MVC 3 資料套用範本 - 使用jQuery.Templates


在ASP.NET MVC中想要使用AJAX來顯示一個頁面中某部分資料時,此情境下大部分都會使用PartialView的方式,

而這個類似的作法也曾經在之前的文章有介紹過:

ASP.NET MVC - 資料分頁(4) MvcSimplePostPager + AJAX

雖然那一篇文章內容是用ASP.NET MVC 2來開發,不過分頁內容的顯示就是使用PartialView(部分檢視)方式,

讓每一個分頁資料的顯示是用jQuery以及AJAX的方式向後端取得該分頁的內容。

 

然而有的時候我們取得的資料並不會直接給我們一個已經Render好的部分頁面內容,例如:JSON,

這個時候就必須在前端頁面中以JavaScript程式來套用JSON資料然後顯示在頁面上,

在之前的專案中就會看到專案其他成員處理這個部分時會在JavaScript程式中去組HTML碼,

常常一個不小心把HTML碼給組錯,或是同樣一段HTML碼會重複的出現在JavaScript的程式當中,

當時看到就覺得這樣的方式蠻不好的,而希望把另一種方式介紹給當時的專案成員來使用,

不過成員對於JavaScript或是jQuery的熟練程度不一,無法讓他們一體適用這種方法,在當時覺得蠻可惜的。

 

當時想要介紹的方法就是在JavaScript中使用範本(jQuery.Templates)的方式,

把動態取得的資料套用在事先組織好的HTML範本,然後再Render到頁面上顯示,

這篇文章接下來就會來演練這方式。



jQuery + PartialView方式

在正式進入使用jQuery.Templates主題之前,先看看使用jQuery的AJAX方式取得PartialView內容並顯示於頁面上的作法,

在Controller中有以下兩個Action方法:

第一個Action:Normal,這是顯示的主要頁面,對應到View的Normal.cshtml。

第二個Action:NormalContent,這是要讓前端AJAX操作時去取得要顯示的部分檢視內容。

public ActionResult Normal()
{
    ViewData["CategoryDDL"] = categoryService.GenerateCategoryDDL(null);
    return View();
}
public ActionResult NormalContent(string categoryId)
{
    List<Product> products = new List<Product>();
    if (string.IsNullOrWhiteSpace(categoryId))
    {
        products = null;
    }
    else
    {
        int id = 0;
        bool parseResult = int.TryParse(categoryId, out id);
        if (!parseResult)
        {
            products = null;
        }
        else
        {
            products = this.productService.GetCollectionBy(id).ToList();
        }
    }
    return PartialView(products);
}

在「NormalContent」這個Action方法中所回傳的是「PartialView()」而不是使用「View()」,

因為在ASP.NET MVC3 中使用Razor,由Action所產生的檢視(View)或是部分檢視(PartialView)檔案其副檔名都是「*.cshtml」,

在View頁面中要顯示一個部分檢視的內容時,可以使用「@Html.RenderPartial(“NormalContent”)」,如此就可以在該頁面顯示時就一併把部分檢視內容給Render出來,

但是在前端的JavaScript中,透過AJAX要動態顯示部分檢視的內容時就無法使用「@Html.RenderPartial(“NormalContent”)」這個方法,

所以在前端的JavaScript程式中就用以下的方式來取得要顯示的部分檢視內容:

$.ajax({
    url: '@Url.Action("NormalContent", "Home")',
    data: { categoryId: categoryId },
    type: 'post',
    async: false,
    cache: false,
    dataType: 'html',
    success: function (data)
    {
        $('#table1 tbody').empty();
        $('#table1 tbody').html(data);
    }
});

在Action方法「NormalContent」的回傳如果是使用「View()」,那麼所產出的部分檢視內容將會包含完整的HTML Tag,

image

在頁面上去顯示部分檢視的內容就會有錯誤,

image

 

如果是使用「PartialView()」則只會產出我們所真正想要的部分檢視,不會有HTML Tag的部份,

image

使用「PartialView()」產出部分檢視的內容到頁面上的顯示就不會有錯誤的情況發生,

image

 

View「Normal.cshtml」的頁面內容:

@{
    ViewBag.Title = "Normal";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
 
<div id="TableContent" class="div_default">
    <h2>Normal</h2>
    <div id="formArea" style="width: 90%; margin-left: auto; margin-right: auto;">
        Category:@Html.Raw(ViewData["CategoryDDL"].ToString())
        <input type="button" id="ButtonSearch" value="List" />
    </div>
    <hr />
    <p></p>
    <div id="Information" style="display: none; width: 90%; margin-left:auto; margin-right:auto; background-color: #fffeee;">
        Category:<span id="CategoryName" style="font-weight: bold;"></span>
        The number of products:<span id="ProductNumber" style="font-weight: bold;"></span>
        <p></p>
    </div>
    <table id="table1" class="grid" style="width: 90%; margin-left:auto; margin-right:auto; background-color: #fffeee;">
        <thead>
            <tr>
                <th>
                    ProductID
                </th>
                <th>
                    ProductName
                </th>
                <th>
                    SupplierID
                </th>
                <th>
                    CategoryID
                </th>
                <th>
                    QuantityPerUnit
                </th>
                <th>
                    UnitPrice
                </th>
                <th>
                    UnitsInStock
                </th>
                <th>
                    UnitsOnOrder
                </th>
                <th>
                    ReorderLevel
                </th>
                <th>
                    Discontinued
                </th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
    <p></p>
</div>

 

View「Normal.cshtml」的JavaScript內容:

@section JavaScriptContent
{
    <script type="text/javascript" language="javascript">
    <!--
        $(document).ready(function ()
        {
            $('#CategoryDDL option:eq(0)').attr('selected', true);
            $('#ButtonSearch').click(function () { FetchCategoryProducts(); });
        });
 
        function FetchCategoryProducts()
        {
            var categoryId = $.trim($('#CategoryDDL option:selected').val());
            if (categoryId.length == 0)
            {
                alert('Please select a Category.');
                return false;
            }
            else
            {
                SelectedCategoryID = parseInt(categoryId, 10);
 
                $.ajax({
                    url: '@Url.Action("NormalContent", "Home")',
                    data: { categoryId: categoryId },
                    type: 'post',
                    async: false,
                    cache: false,
                    dataType: 'html',
                    success: function (data)
                    {
                        $('#table1 tbody').empty();
                        $('#table1 tbody').html(data);
 
                        $('#table1 tbody tr:odd').css('background-color', '#F5FBFC')
                        $('#CategoryName').html($.trim($('#CategoryDDL option:selected').text()));
                        $('#ProductNumber').html($('#ProductCount').val());
                        $('#Information').show();
                    }
                });
            }
        }
    -->
    </script>
}

 

PartialView「Normal.cshtml」的頁面內容:

@model IList<Product>
@using MVC3_Page.Models;
 
@if (Model == null || Model.Count.Equals(0))
{
    @Html.Raw("<tr><td colspan=\"10\" align=\"center\">No Data.</td></tr>");
    <input type="hidden" id="ProductCount" value="0" />
}
else
{
    foreach (var item in Model)
    {
        <tr>
            <td>
                @item.ProductID
            </td>
            <td>
                @item.ProductName
            </td>
            <td>
                @item.SupplierID
            </td>
            <td>
                @item.CategoryID
            </td>
            <td>
                @item.QuantityPerUnit
            </td>
            <td>
                @String.Format("{0:F}", item.UnitPrice)
            </td>
            <td>
                @item.UnitsInStock
            </td>
            <td>
                @item.UnitsOnOrder
            </td>
            <td>
                @item.ReorderLevel
            </td>
            <td>
                @item.Discontinued
            </td>
        </tr>
    }
    <input type="hidden" id="ProductCount" value="@Model.Count" />
} 

 

經由以上的建立步驟與設定之後就完成了ASP.NET MVC 3 使用 jQuery AJAX 動態顯示部分檢視,

image

image

image

 

ASP.NET MVC 3 + jQuery AJAX + PartialView操作示意:

(這段影片沒有聲音~)

 



使用 jQuery.Templates

jQuery.Templates

http://api.jquery.com/jQuery.template/
http://api.jquery.com/category/plugins/templates/

經過前面冗長的開頭之後終於正式進入主題,jQuery.Templates是由微軟與jQuery開發團隊、開發社群聯手開發出的一個jQuery Plugin,

它提供了一個方法,讓開發人員可以設定一個範本,在範本中將要用來顯示資料的地方使用標籤的方式做設定,而在前端程式中,當取得資料後再以資料套入範本中所對應的標籤,最後顯示於頁面上的指定區域。

我知道我的說明會讓很多人看不懂以及不明白,所以有關jQuery.Templates的詳細說明我就跳過,

請各位可以到「黑暗執行緒」的部落格看個究竟,裡面有一系列關於jQuery.Templates的文章有相當詳細說明:

jQuery Templates Plugin筆記1 - 黑暗執行緒
jQuery Templates Plugin筆記2 - 黑暗執行緒
jQuery Templates Plugin筆記3 - 黑暗執行緒
jQuery Templates Plugin筆記4 - 黑暗執行緒
jQuery Templates Plugin筆記5 - 黑暗執行緒
jQuery Templates Plugin筆記6 - 黑暗執行緒
jQuery Templates Plugin筆記7 - 黑暗執行緒
jQuery Templates Plugin筆記8 - 黑暗執行緒

 

為什麼用PartialView加上jQuery AJAX一樣可以達到的功能要轉而使用jQuery.Templates的方式呢?

這是因為在開發的時候所取得的資料來源並不一定是同一個專案或同一個網站,

或是在前端於取得資料後,會需要對資料做進一步的處理之後才會顯示到頁面上。

(或是閒閒沒事然後找個東西來搞自己……)

 

為什麼不在取得資料後直接使用組HTML Tag字串的方式來處理就好,偏偏要用這個呢?結果不都是一樣?!

一、組HTML Tag字串的作法很容易出錯,常常哪邊多個單引號或是哪個雙引號沒做好Escape處理而使得頁面顯示時會有錯誤。

二、在JavaScript程式中避免重複的輸出內容。重複的輸出內容,當有修改需求時就必須要對其它相同的內容做修改,當重複的內容過多時就會產生管理不易的狀況,也一定會經常發生改了A卻忘了改B的情形。

三、將HTML Tag字串移出程式中。避免顯示內容有修改時就必須連帶更動程式的內容,讓影響變動的部分限縮到最少。

 

講那麼多…直接來看看怎麼做吧!

 

主要的Index頁面

Controller / Action方法「Index」

這邊就產生產品分類的下拉選單

public ActionResult Index()
{
    ViewData["CategoryDDL"] = categoryService.GenerateCategoryDDL(null);
    return View();
}

 

View:「Index.cshtml」

Index頁面的主要內容,先準備好要顯示的表格外面框架部分,要做動態顯示的內容則會是在Table的tbody,

<div id="TableContent" class="div_default">
    <h2>Index</h2>
    <div id="formArea" style="width: 90%; margin-left: auto; margin-right: auto;">
        Category:@Html.Raw(ViewData["CategoryDDL"].ToString())
        <input type="button" id="ButtonSearch" value="List" />
    </div>
    <hr />
    <p></p>
    <div id="Information" style="display: none; width: 90%; margin-left:auto; margin-right:auto; background-color: #fffeee;">
        Category:<span id="CategoryName" style="font-weight: bold;"></span>
        The number of products:<span id="ProductNumber" style="font-weight: bold;"></span>
        <p></p>
    </div>
    <table id="table1" class="grid" style="width: 90%; margin-left:auto; margin-right:auto; background-color: #fffeee;">
        <thead>
            <tr>
                <th>
                    ProductID
                </th>
                <th>
                    ProductName
                </th>
                <th>
                    SupplierID
                </th>
                <th>
                    CategoryID
                </th>
                <th>
                    QuantityPerUnit
                </th>
                <th>
                    UnitPrice
                </th>
                <th>
                    UnitsInStock
                </th>
                <th>
                    UnitsOnOrder
                </th>
                <th>
                    ReorderLevel
                </th>
                <th>
                    Discontinued
                </th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
    <p></p>
</div>

 

Template Content

這個要給jQuery.Template使用的範本,我是放在「index.cshtml」頁面中,

<script id="TemplateProduct" type="text/x-jquery-tmpl">
    <tr id="tr_${ProductID}">
        <td>
            ${ProductID}
        </td>
        <td>
            ${ProductName}
        </td>
        <td>
            ${SupplierID}
        </td>
        <td>
            ${CategoryID}
        </td>
        <td>
            ${QuantityPerUnit}
        </td>
        <td>
            ${UnitPrice}
        </td>
        <td>
            ${UnitsInStock}
        </td>
        <td>
            ${UnitsOnOrder}
        </td>
        <td>
            ${ReorderLevel}
        </td>
        <td>
            ${Discontinued}
        </td>
    </tr>
</script>

template中要讓之後要顯示對應資料的地方會是以 ${資料欄位名稱} 的樣式來填寫。

 

Controller / Action 「ProductContent」

再來就是在Controller中去建立一個輸出Product JSON的Action方法,這個方法接收 index 裡面 jQuery AJAX 所提出的POST需求,

前端傳送產品的分類編號(Category ID),然後 ProductContent Action方法再將JSON給送回到前端,

[HttpPost]
public JsonResult ProductContent(string categoryId)
{
    Dictionary<string, string> jo = new Dictionary<string, string>();
 
    if (string.IsNullOrWhiteSpace(categoryId))
    {
        jo.Add("Msg", "Please Select a Category");
        return this.Json(jo);
    }
 
    int id = 0;
    bool parseResult = int.TryParse(categoryId, out id);
 
    if (!parseResult)
    {
        jo.Add("Msg", "Wrong Category ID");
        return this.Json(jo);
    }
 
    var result = this.productService.GetCollectionBy(id);
 
    List<Dictionary<string, object>> ja = new List<Dictionary<string, object>>();
 
    foreach (var item in result)
    {
        Dictionary<string, object> product = new Dictionary<string, object>();
 
        product.Add("ProductID", item.ProductID);
        product.Add("ProductName", item.ProductName);
        product.Add("SupplierID", item.SupplierID);
        product.Add("CategoryID", item.CategoryID);
        product.Add("QuantityPerUnit", item.QuantityPerUnit);
        product.Add("UnitPrice", String.Format("{0:F}", item.UnitPrice));
        product.Add("UnitsInStock", item.UnitsInStock);
        product.Add("UnitsOnOrder", item.UnitsOnOrder);
        product.Add("ReorderLevel", item.ReorderLevel);
        product.Add("Discontinued", item.Discontinued);
 
        ja.Add(product);
    }
 
    return this.Json(ja);
} 

 

View:「Index.cshtml」的前端程JavaScript程式

這邊就是重點所在,在頁面上選擇好Category並按下「List」Button後,jQuery程式就會把CategoryID給POST到後端的Action方法,

後端傳送回JSON資料後,使用$.each() 逐一把JSON內的每一筆資料轉換為Javascript Object Literal,

然後把轉換後的Object Literal使用jQuery.Templates的 「.tmpl()」方法,讓資料去對應剛才所設定的Template內容,並且將轉換後所傳回的HTML給append到要顯示的 table1 tbody中,

<script type="text/javascript" language="javascript">
<!--
    $(document).ready(function ()
    {
        $('#CategoryDDL option:eq(0)').attr('selected', true);
        $('#ButtonSearch').click(function(){ FetchCategoryProducts(); });
    });
    function FetchCategoryProducts()
    {
        var categoryId = $.trim($('#CategoryDDL option:selected').val());
        if (categoryId.length == 0)
        {
            alert('Please select a Category.');
            return false;
        }
        else
        {
            SelectedCategoryID = parseInt(categoryId, 10);
            $.ajax({
                url: '@Url.Action("ProductContent", "Home")',
                data: { categoryId: categoryId },
                type: 'post',
                async: false,
                cache: false,
                dataType: 'json',
                success: function (data)
                {
                    $('#table1 tbody').empty();
                    if (data.Msg)
                    {
                        $('#CategoryName').empty();
                        $('#ProductNumber').empty();
                        $('#Information').hide();
                        alert(data.Msg);
                    }
                    else
                    {
                        $.each(data, function (i)
                        {
                            var item =
                            {
                                ProductID: data[i]["ProductID"],
                                ProductName: data[i]["ProProductNameductID"],
                                SupplierID: data[i]["SupplierID"],
                                CategoryID: data[i]["CategoryID"],
                                QuantityPerUnit: data[i]["QuantityPerUnit"],
                                UnitPrice: data[i]["UnitPrice"],
                                UnitsInStock: data[i]["UnitsInStock"],
                                UnitsOnOrder: data[i]["UnitsOnOrder"],
                                ReorderLevel: data[i]["ReorderLevel"],
                                Discontinued: data[i]["Discontinued"]
                            };
                            $('#table1 tbody').append($('#TemplateProduct').tmpl(item));
                        });
                        $('#table1 tbody tr:odd').css('background-color', '#F5FBFC')
                        $('#CategoryName').html($.trim($('#CategoryDDL option:selected').text()));
                        $('#ProductNumber').html(data.length);
                        $('#Information').show();
                    }
                    return false;
                }
            });
        }
    }
-->
</script>

最後就是完成我們要的需求,在ASP.NET MVC 3中,使用jQuery.Templates的方式完成動態更換顯示內容。

 

操作示範

一開始的初始頁面

image

選擇「Category:1」

image

選擇「Category:5」

image

 

ASP.NET MVC 3 + jQuery AJAX + jQuery.Templates 操作示意:

(這段影片沒有聲音~)

 

完整的View:「Index.cshtml」內容

@{
    ViewBag.Title = "Index";
    Layout = "~/Views/Shared/_Layout.cshtml";
}
<div id="TableContent" class="div_default">
    <h2>Index</h2>
    <div id="formArea" style="width: 90%; margin-left: auto; margin-right: auto;">
        Category:@Html.Raw(ViewData["CategoryDDL"].ToString())
        <input type="button" id="ButtonSearch" value="List" />
    </div>
    <hr />
    <p></p>
    <div id="Information" style="display: none; width: 90%; margin-left:auto; margin-right:auto; background-color: #fffeee;">
        Category:<span id="CategoryName" style="font-weight: bold;"></span>,
        The number of products:<span id="ProductNumber" style="font-weight: bold;"></span>
        <p></p>
    </div>
    <table id="table1" class="grid" style="width: 90%; margin-left:auto; margin-right:auto; background-color: #fffeee;">
        <thead>
            <tr>
                <th>
                    ProductID
                </th>
                <th>
                    ProductName
                </th>
                <th>
                    SupplierID
                </th>
                <th>
                    CategoryID
                </th>
                <th>
                    QuantityPerUnit
                </th>
                <th>
                    UnitPrice
                </th>
                <th>
                    UnitsInStock
                </th>
                <th>
                    UnitsOnOrder
                </th>
                <th>
                    ReorderLevel
                </th>
                <th>
                    Discontinued
                </th>
            </tr>
        </thead>
        <tbody>
        </tbody>
    </table>
    <p></p>
</div>
 
@section JavaScriptContent
{
    <script id="TemplateProduct" type="text/x-jquery-tmpl">
        <tr id="tr_${ProductID}">
            <td>
                ${ProductID}
            </td>
            <td>
                ${ProductName}
            </td>
            <td>
                ${SupplierID}
            </td>
            <td>
                ${CategoryID}
            </td>
            <td>
                ${QuantityPerUnit}
            </td>
            <td>
                ${UnitPrice}
            </td>
            <td>
                ${UnitsInStock}
            </td>
            <td>
                ${UnitsOnOrder}
            </td>
            <td>
                ${ReorderLevel}
            </td>
            <td>
                ${Discontinued}
            </td>
        </tr>
    </script>
 
    <script type="text/javascript" language="javascript">
    <!--
        $(document).ready(function ()
        {
            $('#CategoryDDL option:eq(0)').attr('selected', true);
            $('#ButtonSearch').click(function(){ FetchCategoryProducts(); });
        });
 
        function FetchCategoryProducts()
        {
            var categoryId = $.trim($('#CategoryDDL option:selected').val());
            if (categoryId.length == 0)
            {
                alert('Please select a Category.');
                return false;
            }
            else
            {
                SelectedCategoryID = parseInt(categoryId, 10);
 
                $.ajax({
                    url: '@Url.Action("ProductContent", "Home")',
                    data: { categoryId: categoryId },
                    type: 'post',
                    async: false,
                    cache: false,
                    dataType: 'json',
                    success: function (data)
                    {
                        $('#table1 tbody').empty();
 
                        if (data.Msg)
                        {
                            $('#CategoryName').empty();
                            $('#ProductNumber').empty();
                            $('#Information').hide();
                            alert(data.Msg);
                        }
                        else
                        {
                            $.each(data, function (i)
                            {
                                var item =
                                {
                                    ProductID: data[i]["ProductID"],
                                    ProductName: data[i]["ProProductNameductID"],
                                    SupplierID: data[i]["SupplierID"],
                                    CategoryID: data[i]["CategoryID"],
                                    QuantityPerUnit: data[i]["QuantityPerUnit"],
                                    UnitPrice: data[i]["UnitPrice"],
                                    UnitsInStock: data[i]["UnitsInStock"],
                                    UnitsOnOrder: data[i]["UnitsOnOrder"],
                                    ReorderLevel: data[i]["ReorderLevel"],
                                    Discontinued: data[i]["Discontinued"]
                                };
 
                                $('#table1 tbody').append($('#TemplateProduct').tmpl(item));
                            });
 
                            $('#table1 tbody tr:odd').css('background-color', '#F5FBFC')
                            $('#CategoryName').html($.trim($('#CategoryDDL option:selected').text()));
                            $('#ProductNumber').html(data.length);
                            $('#Information').show();
                        }
                        return false;
                    }
                });
            }
        }
    -->
    </script>
}
 

 


寫這一篇是為了要作為接下來相關系列文章的開端,也順便介紹大家認識一下「jQuery.Templates」的使用,

並且說明在ASP.NET MVC 3 與jQuery AJAX的情境下有別於使用PartialView的另一種作法,

因為有的時候也真的會碰到資料來源是只有JSON的狀況,所以在前端的程式就可以使用jQuery.Templates來完成這個需求,

讓程式開發人員不要再用「組字串」的方式來做「兜資料、產結果」的舊方法,使用更加聰明而且方便的作法,以避免不必要的錯誤產生。

 

所以接下來會有幾篇文章會使用到「jQuery.Templates」,以這個jQuery Plugin為出發點來完成其它的幾個練習。

 

其它參考

In 91 - [jQuery]透過jQuery Template把JSON資料套入範本

http://www.dotblogs.com.tw/hatelove/archive/2011/11/23/jquery-template-ajax-json-like-listview.aspx

 

Stepheb Walther - An Introduction to jQuery Templates

http://stephenwalther.com/blog/archive/2010/11/30/an-introduction-to-jquery-templates.aspx

 

以上

沒有留言:

張貼留言

提醒

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