之前寫過用ASP.NET WebForm作為AJAX式資料源的Kendo UI Grid範例,最近計劃在一個小專案試用ASP.NET MVC 4 RC,面對的第一個需求又是"以清單呈現查詢結果",就來看看如何用ASP.NET MVC 4 RC滿足Kendo UI Grid的需求吧!

記得前一次用ashx寫資料端,花了不少功夫處理分頁及排序,而且還沒實做Filter過濾功能。但在ASP.NET MVC上,要整合Kendo UI Grid,則有很酷的方便選擇 -- KendoGridBinder!!

以下是我實做Kendo UI Grid + ASP.NET MVC 4的過程:

  1. 建立一個ASP.NET MVC 4專案
  2. 使用NuGet安裝KendoUIWeb及KendoGridBinder
  3. 借用上回的SimMemberInfo Model類別 ,放在Model目錄下:
    using System;
    using System.Collections.Generic;
    using System.Drawing;
    using System.Linq;
    using System.Reflection;
    using System.Web;
     
    namespace KendoGridMvc.Models
    {
        //模擬資料物件
        public class SimMemberInfo
        {
            public string UserNo; //會員編號
            public string UserName; //會員名稱
            public DateTime RegDate; //註冊日期
            public int Points; //累積點數
     
            //模疑資料來源
            public static List<SimMemberInfo> SimuDataStore = null;
     
            static SimMemberInfo()
            {
                Random rnd = new Random();
                //借用具名顏色名稱來產生隨機資料
                string[] colorNames = typeof(Color)
                    .GetProperties(BindingFlags.Static | BindingFlags.Public)
                    .Select(o => o.Name).ToArray();
                SimuDataStore =
                    colorNames
                    .Select(cn => new SimMemberInfo()
                    {
                        UserNo = string.Format("C{0:00000}", rnd.Next(99999)),
                        UserName = cn,
                        RegDate = DateTime.Today.AddDays(-rnd.Next(1000)),
                        Points = rnd.Next(9999)
                    }).ToList();
            }
        }
    }

  4. 要引用Kendo UI,需要載入必要的JS及CSS,此時昨天介紹過的ASP.NET MVC打包壓縮功能馬上派上用場! 編輯App_Start/BundleConfig.cs,加入以下程式:

                bundles.Add(new ScriptBundle("~/bundles/kendoUI").Include(
                    "~/Scripts/kendo/2012.1.322/kendo.web.min.js"
                    ));
    //經實測,SytleBundle virtualPath參數使用"2012.1.322"會有問題,故向上搬移一層
    //將/Content/kendo/2012.1.322的內容搬至Content/kendo下
                bundles.Add(new StyleBundle("~/Content/kendo/css").Include(
                    "~/Content/kendo/kendo.common.min.css",
                    "~/Content/kendo/kendo.blueopal.min.css"
                    ));

    PS: 此處有一個眉角:由於CSS檔路徑會被當成引用圖檔的基準,原本Kendo UI的.css及圖檔被放在~/Content/kendo/2012.1.322/下,理論上StyleBundle應設成"~/Content/kendo/2012.1.322/css”,才能引導瀏覽器到該目錄下取用圖檔。不幸地,我發現StyleBundle的virtualPath參數出現2012.1.322時,會導致Styles.Render("~/Content/kendo/2012.1.322/css”)時傳回HTTP 404錯誤~ 為克服問題,我決定將2012.1.322目錄的內容向上搬一層,直接放在~/Content/keno目錄下,並將virtualPath設成"~/Content/kendo/css",這樣就能避開問題。
    [2014-10-13更新]關於此問題的深入探討

  5. 為了省去每個View都要加掛Kendo UI JS及CSS的麻煩,我索性將它們加在~/Views/Shared/_Layout.cshtml中:
    <!DOCTYPE html>
    <html>
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width" />
        <title>@ViewBag.Title</title>
        @Styles.Render("~/Content/themes/base/css", "~/Content/css",
                       "~/Content/kendo/css")
        @Scripts.Render("~/bundles/modernizr")
        @Scripts.Render("~/bundles/jquery", "~/bundles/kendoUI")
        @RenderSection("scripts", required: false)
    </head>
    <body>
        @RenderBody()
    </body>
    </html>

  6. 網頁Index.cshtml的Client端做法,則跟上回WebForm AJAX範例幾乎完全相同:
    @section Scripts
    {
        <style>
            body { font-size: 9pt; }
            #dvGrid { width: 500px; }
            span.hi-lite { color: red; }
            #dvGrid th.k-header { text-align: center; }
        </style>
        <script>
            $(function () {
                //建立資料來源物件
                var dataSrc = new kendo.data.DataSource({
                    transport: {
                        read: {
                            //以下其實就是$.ajax的參數
                            type: "POST",
                            url: "/Home/Grid",
                            dataType: "json",
                            data: {
                                //額外傳至後方的參數
                                keywd: function () {
                                    return $("#tKeyword").val();
                                }
                            }
                        }
                    },
                    schema: {
                        //取出資料陣列
                        data: function (d) { return d.data; },
                        //取出資料總筆數(計算頁數用)
                        total: function (d) { return d.total; }
                    },
                    pageSize: 10,
                    serverPaging: true,
                    serverSorting: true
                });
                //JSON日期轉換
                var dateRegExp = /^\/Date\((.*?)\)\/$/;
                window.toDate = function (value) {
                    var date = dateRegExp.exec(value);
                    return new Date(parseInt(date[1]));
                }
                $("#dvGrid").kendoGrid({
                    dataSource: dataSrc,
                    columns: [
                        { field: "UserNo", title: "會員編號" },
                        { field: "UserName", title: "會員名稱",
                            template: '#= "<span class=\\"u-name\\">" + UserName + "</span>" #'
                        },
                        { field: "RegDate", title: "加入日期",
                            template: '#= kendo.toString(toDate(RegDate), "yyyy/MM/dd")#'
                        },
                        { field: "Points", title: "累積點數" },
                    ],
                    sortable: true,
                    pageable: true,
                    dataBound: function () {
                        //AJAX資料Bind完成後觸發
                        var kw = $("#tKeyword").val();
                        //若有設關鍵字,做Highlight處理
                        if (kw.length > 0) {
                            var re = new RegExp(kw, "g");
                            $(".u-name").each(function () {
                                var $td = $(this);
                                $td.html($td.text()
                               .replace(re, "<span class='hi-lite'>$&</span>"));
                            });
                        }
                    }
                });
                //按下查詢鈕
                $("#bQuery").click(function () {
                    //要求資料來源重新讀取(並指定切至第一頁)
                    dataSrc.read({ page: 1, skip: 0 });
                    //Grid重新顯示資料
                    $("#dvGrid").data("kendoGrid").refresh();
                });
            });
        </script>
    }
    <div style="padding: 10px;">
        關鍵字:
        <input id="tKeyword" /><input type="button" value="查詢" id="bQuery" />
    </div>
    <div id="dvGrid">
    </div>

  7. 最後來到重頭戲,負責以AJAX方式傳回資料的HomeController.cs的Grid() Action:
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Web.Mvc;
    using KendoGridBinder;
    using KendoGridMvc.Models;
     
    namespace KendoGridMvc.Controllers
    {
        public class HomeController : Controller
        {
            //
            // GET: /Home/
     
            public ActionResult Index()
            {
                return View();
            }
     
            public JsonResult Grid(KendoGridRequest request, string keywd)
            {
                var result = SimMemberInfo.SimuDataStore.Where(o =>
                string.IsNullOrEmpty(keywd) || o.UserName.Contains(keywd));
                return Json(new KendoGrid<SimMemberInfo>(request, result));
            }
        }
    }
    什麼? 沒看錯吧? 這樣就好?
    是的,感謝Ryan Whitmire及Jose Ball的佛心與巧思,只要return Json(new KendoGrid<T>(KendoGridRequest, IEnumerable<T>)),餘下的換頁、排序,甚至欄位過濾功能,就都交給KendoGridBinder全權處理囉!

實際測試,換頁、排序功能一切正常,有了Kendo UI助陣,ASP.NET MVC之路順利多了~


Comments

# by linsf

請問「2.使用NuGet安裝KendoUIWeb及KendoGridBinder」這畫面要從那裡叫出來,謝謝。

# by Jeffrey

to linsf, 在Solution Explorer的指定專案按右鍵,選單中會有個"Manage NuGet Packages"(前題是VS要先安裝NuGet Package Manager擴充套件),參考: http://docs.nuget.org/docs/start-here/Managing-NuGet-Packages-Using-The-Dialog

# by Aeson

我遇到一个很严重的问题,我照着您的步骤做,在BundleConfig.cs那边加入那些代码,可是遇到一些问题,具体的问题在这里 http://www.kendoui.com/forums/mvc/general-discussions/vs2012-rtm-mvc4-kendo-bundle-not-rendering.aspx,所以我照着方法做在BundleConfig.cs那里加入了 // Clear all items from the default ignore list to allow minified CSS and JavaScript files to be included in debug mode bundles.IgnoreList.Clear(); // Add back the default ignore list rules sans the ones which affect minified files and debug mode bundles.IgnoreList.Ignore("*.intellisense.js"); bundles.IgnoreList.Ignore("*-vsdoc.js"); bundles.IgnoreList.Ignore("*.debug.js", OptimizationMode.WhenEnabled); ,问题是解决了可是马上又遇到了另一个一个问题,我从chrome的develop tools里看到以下这些error, Uncaught TypeError: Cannot call method 'ready' of undefined jquery:1 i.fn.i.init jquery:1 u jquery:1 n jquery:1 i.sub jquery:1 (anonymous function) kendojs:1 (anonymous function) kendojs:1 找了很久都解决不到,请问有人有遇过这样的问题吗?请问有人能帮我吗?

# by AndyYou

想請問 為何直接用 DbContext 子類別實體回傳 .ToList() 資料都會失敗。 例如: public JsonResult Grid(KendoGridRequest request) { List<Person> result = db.People.ToList(); var grid = new KendoGrid<Person>(request, result); return Json(grid); } 但是自己 new 一個全新的物件沒問題

# by Jeffrey

to AndyYou, 有失敗的詳細訊息嗎?

# by AndyYou

這邊是我測試的範例: Controller : http://goo.gl/yvvoK Action : Grid 裡面有三種測試 只有 ToList()有錯 而錯誤訊息是在js http://cl.ly/image/090y0Y2h2q0p View 中的 JS 我有參考原作在比對一下 http://goo.gl/WgpPg Model 是這個 http://goo.gl/IF09N 下圖則是我用監看式比對 應該只有差一點點吧 http://cl.ly/image/2S0b3Y1Q3P27

# by Jeffrey

to AndyYou, 由 http://cl.ly/image/090y0Y2h2q0p 判斷,最方下的/Daily/Grid傳回了一個HTTP 500,代表Server端程式發生Exception,訊息文字內容有16K,其中應該有進一步的線索,可以看一下當時傳回的錯誤訊息找方向。

# by lin

照你步驟做,出現錯誤,有無完整程式碼可下載 ? 0x800a1391 - JavaScript 執行階段錯誤: 'kendo' 未經定義

# by Jeffrey

to lin, kendo未經定義應是kendo.web.min.js未正確載入所致,建議用IE Dev Tool檢查一下。我猜一個可能原因是目前NuGet下載到的KendoUIWeb已更新至2013.2.716版,故BundleConfig.cs程式的~/Scripts/kendo/2012.1.322/kendo.web.min.js也要調整。

# by Shen

Hi,黑大,感謝您的這篇文章,我跑一個table來接是正常的,但遇到有複雜關聯的table就會出錯,不知道您有沒有遇到過EF循環參考的問題?我試過將JsonResult改為JSON.NET的回傳方式(可忽略循環參考),但JSON.NET似乎沒辦法解析KendoGridBinder產生的Data

# by Jeffrey

to Shen, 關於"JSON.NET似乎沒辦法解析KendoGridBinder產生的Data",可否再多提供一些錯誤細節?

# by Shen

hi,黑大,謝謝您的回覆,後來已經解決了,一樣是使用JSON.NET自訂Result即可解決循環參考的問題(我想是下午鬼打牆了...) 謝謝您 ^^

# by Tim

Hi,黑暗大前輩,我正在使用KendoUI的Pop Windows Create功能,後端是用ActionResult接,我想問的是ActionResult能直接轉換class,而不用類似Request["id"]去取得資料嗎?? js: create: { type: "POST", url: "/Service/CreateStaff", dataType: "json", } Controller [AcceptVerbs(HttpVerbs.Post)] public ActionResult CreateAdditional(Staff _staff) { //_staff都是Null,改用string或int等欄位接收就能接得到... return Content(""); }

# by Jeffrey

to Tim, 沒有很懂你的意思。ActionResult是Controller Action方法回傳的型別,不是用來接收前端送入的資料,應該不會跟Request["id"]放在一起比較。

# by Tim

不好意思描述不太清楚@@ 我想要的功能是類似官網的Gird pop window - add new record http://demos.kendoui.com/web/grid/editing-popup.html 照他的Sample Code在前端設定 create: { type: "POST", url: "/Service/CreateStaff", //Controller接收Ajax的Function dataType: "json", } 而後端其實就是接收Ajax post [AcceptVerbs(HttpVerbs.Post)] public ActionResult CreateStaff(Staff _staff) { //_staff class都是Null return Content(""); } 改用欄位指定就可以,但欄位太多挺麻煩的 [AcceptVerbs(HttpVerbs.Post)] public ActionResult CreateStaff(string StaffName) { return Content(""); } 我想讓後端可以接收自訂的class,應該是要在前端將新增的資料(object)做JSON.stringify,但我找不到KendoUI在哪下手 不知道黑大用KendoUI做Ajax Post新增時,都是用什麼方式?謝謝

# by Jeffrey

to Tim, 我在工作專案上通常會使用編輯UI透過自訂模組處理CRUD作業,倒沒用過KendoUI內建的編輯更新功能。由你的描述,應是KendoUI送出的資料格式無法靠Model Binder自動轉成MVC Action的輸入參數,我想透過一點客製技巧就可克服,這個議題挺有趣,容我找時間研究再寫範例。

# by Jeffrey

to Tim, 依我實測,Gird Pop Window會將編輯物件的所有屬性以POST Form形式傳至後端,理論上ASP.NET MVC可自動將其轉成你所定義成Staff _staff參數物件,但有個前題: Staff的StaffName要定義成屬性(有get; set;那種),我範例中的SimMemberInfo只寫成public string UserName;是不可行的,要改成public string UserName { get; set; },你看看是否問題出在這裡?

# by Ricky

Dear 黑暗大 , 可以請教您兩個問題嗎 , 謝謝 不知道 KendoGridBinder這個plugin,能不能搭配Web API用呢? 我嘗試過將後端改成以下code...好像jQuery都無法呼叫到... transport: { read: { data: { keyword: $('#txtFilter').val() }, type: "POST", url: "/Customer/GetCustomers", //url: "/api/Customer", dataType: "json" } }, public HttpResponseMessage Get(KendoGridRequest request, string keyword) { var result = CustomerService.GetCustomers(keyword); var jsonData = new KendoGrid<CustomerViewModel>(request, result); return Request.CreateResponse(HttpStatusCode.OK, jsonData); } 另外就是KendoUI open source好像要停止支援了...不知道黑暗大有什麼替代的Grid Freamwork嗎 謝謝您 ^^

# by Jeffrey

to Ricky, 基於WebAPI與MVC Action的行為差異,我想沒法直接在WebAPI裡用KendoGridBinder,但Telerik有篇Blog介紹過KendoGrid與WebAPI的整合方式: http://blogs.telerik.com/kendoui/posts/12-11-29/the_facts_on_using_kendo_ui_with_asp_net_webapi 公司之前就已買了Telerik元件授權,主要是經過評估,確定「花錢買元件避免大量腦細胞死亡」很划算! XD 我在工作上對Grid的依賴度極高,Telerik Grid元件提供的彈性與擴充性,能相抗衡的幾乎都是付費元件,以前KendoGrid屬於開源算是賺到,現在只能算回歸正常,仔細算算還是很值得,也不太會想花時間去尋找替代的免費方案。以上是我的經驗,與你分享。

# by Ricky

謝謝您耐心地回答 :)

# by Justin

想請問黑大前輩, 關於樓上Tim的問題,我也是感到困惑, 而且我是兩種方法都抓不到資料, 我資料來源於資料庫,目前要實作更新部分, 卡在KendoUI使用修改按鈕按下儲存的時候不知該如何下手, 後端是如何取得前端送來的資料呢? 根據 http://demos.telerik.com/aspnet-mvc/grid/editing-popup 與 http://demos.telerik.com/kendo-ui/grid/editing-popup 的方法嘗試失敗..

# by Jeffrey

to Justin, 嘗試失敗是指依照 http://demos.telerik.com/aspnet-mvc/grid/editing-popup 提供的GridController.EditingPopup_Update() 範例測試不成功嗎?

Post a comment