【番外篇】ASP.NET MVC快速入門之免費jQuery控制元件庫(MVC5+EF6)
目錄
FineUIMvc簡介
FineUIMvc 是基於 jQuery 的專業 ASP.NET MVC 控制元件庫,其前身是基於 WebForms 的開源控制元件庫 FineUI(歷時9年120多個版本)。FineUIMvc(基礎版)包含開源版的全部功能,支援 30 種內建主題和 FontAwesome 圖示,支援訊息對話方塊和單元格編輯表格,功能強大,最重要的是完全免費。
以下引自FineUI官網的介紹:
FineUIMvc(基礎版)作為三石奉獻給社群的一個禮物,功能絕對強大到讓你心動:
1.擁有 FineUI(開源版)的全部功能。
2.擁有 FineUI
3.擁有 FineUIMvc(企業版)強大的通知對話方塊和單元格編輯表格。
4.內建 30 種 Metro 和 jQueryUI 主題(以及自定義主題Bootstrap Pure)。
5.內建 500 多個 FontAwesome 圖示字型,控制元件原生支援。
6.9 年技術積累,1300 多位捐贈會員,100 多家企業客戶,穩定可信賴。
7.重要的事情說三遍:完全免費!完全免費!完全免費!
這篇文章,我們將使用FineUIMvc(基礎版)來實現本示例。
FineUIMvc空專案
登入FineUI的論壇,下載FineUIMvc
雙擊FineUIMvc.EmptyProject.sln,開啟專案:
這個目錄結構和之前的很類似,多了一個res目錄,這個是FineUIMvc中的約定,用來放置靜態資源,比如CSS、JS、圖片以及一套Icon圖示。
Ctrl+F5直接執行專案:
修改Web.config
空專案已經配置好了Web.config檔案,主要是兩個地方的改動:
<configSections> <section name="FineUIMvc" type="FineUIMvc.ConfigSection, FineUIMvc"requirePermission="false" /> </configSections> <FineUIMvc DebugMode="true" Theme="Cupertino" />
這裡設定FineUIMvc的全域性引數:
lTheme: 樣式主題,內建 30 種主題(其中 6 種 Metro 主題,24 種 jQueryUI 官方主題,預設值:Default)
nMetro 主題:Default, Metro_Blue, Metro_Dark_Blue, Metro_Gray, Metro_Green, Metro_Orange
njQueryUI 主題: Black_Tie, Blitzer, Cupertino, Dark_Hive, Dot_Luv, Eggplant, Excite_Bike, Flick, Hot_Sneaks, Humanity, Le_Frog, Mint_Choc, Overcast, Pepper_Grinder, Redmond, Smoothness, South_Street, Start, Sunny, Swanky_Purse, Trontastic, UI_Darkness, UI_Lightness, Vader
lCustomTheme: 自定義樣式主題(custom_default/bootstrap_pure)
lLanguage: 控制元件語言(en/zh_CN/zh_TW,預設值:zh_CN)
lFormMessageTarget: 表單欄位錯誤提示資訊的顯示位置(Title/Side/Qtip,預設值:Side)
lFormLabelWidth: 表單欄位標籤的寬度(預設值:100px)
lFormLabelAlign: 表單欄位標籤的位置(Left/Right/Top,預設值:Left)
lFormRedStarPosition: 表單欄位紅色星號的位置(AfterText/BeforeText/AfterSeparator,預設值:AfterText)
lFormLabelSeparator: 表單欄位標籤與內容的分隔符(預設值:":")
lEnableAjax: 是否啟用AJAX(預設值:true)
lAjaxTimeout: Ajax超時時間(單位:秒,預設值:120s)
lDebugMode: 是否開發模式,啟用時格式化頁面輸出的JavaScript程式碼,便於除錯(預設值:false)
lEnableAjaxLoading: 是否啟用Ajax提示(預設值:true)
lAjaxLoadingType: Ajax提示型別,預設在頁面頂部顯示黃色提示框(Default/Mask,預設值:Default)
lEnableShim: 是否啟用遮罩層,防止ActiveX、Flash等物件覆蓋彈出窗體(預設值:false)
lEnableCompactMode: 是否啟用緊湊模式(預設值:false)
lEnableLargeMode: 是否啟用大字型模式(預設值:false)
lMobileAdaption: 是否啟用移動瀏覽器自適應(預設值:false)
lEnableAnimation: 是否啟用動畫(僅Webkit瀏覽器支援動畫效果)(預設值:false)
lLoadingImageNumber: 頁面載入動畫圖片的序號(從 1 到 30,預設值:1)
更詳細的介紹參考FineUIMvc線上示例站點:
另外一處配置HTTP處理器:
<system.web> <httpModules> <add name="FineUIMvcScriptModule" type="FineUIMvc.ScriptModule, FineUIMvc"/> </httpModules> <httpHandlers> <add verb="GET" path="res.axd" type="FineUIMvc.ResourceHandler, FineUIMvc"/> </httpHandlers> </system.web>
如果你之前用過FineUI(開源版),相信對這個配置很熟悉。
新增全域性模型繫結器
在Global.asax中,新增全部模型繫結器:
protected void Application_Start() { AreaRegistration.RegisterAllAreas(); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); ModelBinders.Binders.Add(typeof(JArray), new JArrayModelBinder()); ModelBinders.Binders.Add(typeof(JObject), new JObjectModelBinder()); }
這個設定用於模型繫結,可以將客戶端傳入的JSON陣列自動轉化為JArray物件,類似如下程式碼:
當然這個設定不是必須的,如果去掉這個設定,則上面的程式碼就需要這樣寫了:
public ActionResult Grid1_PageIndexChanged(string Grid1_fields, int Grid1_pageIndex) { JArray fields = JArray.Parse(Grid1_fields); // ..... }
佈局檢視
佈局檢視類似於WebForms的母版頁,位於Views/Home/Shared/_Layout.cshtml,我們先看下其中的程式碼:
@{ var F = Html.F(); } <!DOCTYPE html> <html> <head> <title>@ViewBag.Title - FineUIMvc 空專案</title> @F.RenderCss() <link href="~/res/css/common.css" rel="stylesheet" type="text/css" /> @RenderSection("head", false) </head> <body> @Html.AntiForgeryToken() @F.PageManager @RenderSection("body", true) @F.RenderScript() @RenderSection("script", false) </body> </html>
整體架構非常簡單,其中和FineUIMvc密切相關的程式碼:
1.var F = Html.F():例項化FineUIMvc的HTML輔助方法,檢視中所有的FineUIMvc控制元件都要通過F變數來初始化。
2.F.RenderCss():放置FineUIMvc內建的CSS檔案。
3.F.PageManager:FineUIMvc的頁面管理器,每個頁面都需要,所以放到佈局檢視中。
4.F.RenderScript():放置FineUIMvc內建的JavaScript檔案。
除此之外,我們還通過MVC內建的RenderSection函式定義了幾個段落,主要用於放置自定義CSS檔案,頁面主體HTML以及頁面自定義JavaScript指令碼檔案。
Html.AntiForgeryToken()用於防止跨站請求偽造攻擊,需要配合控制器中方法的[ValidateAntiForgeryToken]特性使用,本系列的第三篇文章曾做過詳細的介紹。
注意:和用VS自動生成的佈局檢視有一個很大的不同,這裡使用RenderSection("body", true)來方式主體HTML,第二個引數true表明這是一個必選項。所以在具體的檢視頁面,必須存在名為body的節,否則就會報錯。
首頁檢視
首頁檢視的程式碼相對有點複雜,從截圖上可以看出,整個頁面被分為三部分,上半部分放置網址標題,操作按鈕等控制元件,左側部分是一個樹控制元件,右側部分是一個選項卡控制元件,我們先來看這部分的程式碼:
@(F.RegionPanel() .ID("RegionPanel1") .ShowBorder(false) .IsViewPort(true) .Regions( F.Region() .ID("Region1") .ShowBorder(false) .ShowHeader(false) .RegionPosition(Position.Top) .Layout(LayoutType.Fit) .ContentEl("#header"), F.Region() .ID("Region2") .RegionSplit(true) .Width(200) .ShowHeader(true) .Title("選單") .EnableCollapse(true) .Layout(LayoutType.Fit) .RegionPosition(Position.Left) .Items( F.Tree() .ShowBorder(false) .ShowHeader(false) .ID("treeMenu") .Nodes( F.TreeNode() .Text("預設分類") .Expanded(true) .Nodes( F.TreeNode() .Text("開始頁面") .NavigateUrl("~/Home/Hello"), F.TreeNode() .Text("登入頁面") .NavigateUrl("~/Home/Login") ) ) ), F.Region() .ID("mainRegion") .ShowHeader(false) .Layout(LayoutType.Fit) .RegionPosition(Position.Center) .Items( F.TabStrip() .ID("mainTabStrip") .EnableTabCloseMenu(true) .ShowBorder(false) .Tabs( F.Tab() .ID("Tab1") .Title("首頁") .BodyPadding(10) .Layout(LayoutType.Fit) .Icon(Icon.House) .ContentEl("#maincontent") ) ) ) )
最外層是一個RegionPanel控制元件,並設定了填充整個瀏覽器視窗(IsViewPort=true),上半部分的Region控制元件通過ContentEl屬性來指定一個id=header的HTML片段;左側Region裡面放置了一個硬編碼的Tree控制元件,通過指定Region的Layout=Fit來讓Tree控制元件填充整個左側區域;右側Region裡面放置了TabStrip控制元件,TabStrip放置了一個初始選項卡Tab,並通過ContentEl來指向一個id=maincontent的HTML片段。
這個分析過程也很容易理解,如果你之前使用過FineUI(開源版),對比應該並不陌生,只不過把之前的ASPX語法換成了Rezor語法而已。
左側樹控制元件和右側選項卡控制元件的互動是由JavaScript程式碼控制的,這個常見互動FineUIMvc進行了封裝:
@section script { <script> // 頁面控制元件初始化完畢後,會呼叫使用者自定義的onReady函式 F.ready(function () { // 初始化主框架中的樹和選項卡互動,以及位址列的更新 // treeMenu: 主框架中的樹控制元件例項 // mainTabStrip: 選項卡例項 // updateHash: 切換Tab時,是否更新位址列Hash值(預設值:true) // refreshWhenExist: 新增選項卡時,如果選項卡已經存在,是否重新整理內部IFrame(預設值:false) // refreshWhenTabChange: 切換選項卡時,是否重新整理內部IFrame(預設值:false) // maxTabCount: 最大允許開啟的選項卡數量 // maxTabMessage: 超過最大允許開啟選項卡數量時的提示資訊 F.initTreeTabStrip(F.ui.treeMenu, F.ui.mainTabStrip, { maxTabCount: 10, maxTabMessage: '請先關閉一些選項卡(最多允許開啟 10 個)!' }); }); </script> }
對這裡出現JavaScript程式碼的簡單描述:
1.F.ready:在控制元件初始化完畢後執行,類似於jQuery的$.ready,只不過F.ready是在DomReady之後執行的。
2.F.initTreeTabStrip:設定樹控制元件和選項卡控制元件的互動,點選樹控制元件節點新建一個啟用IFrame的選項卡控制元件。裡面有很多引數可以控制互動行為,在上面的程式碼中都有註釋,就不一一解釋了。
3.F.ui.treeMenu:引用左側樹控制元件例項。FineUIMvc會在F.ui命名控制元件下儲存所有頁面上初始化的FineUIMvc控制元件,因此可以方便的通過控制元件的名稱來引用。
主題選擇框
這個頁面還有一個需要JavaScript指令碼的互動過程,那就是頁面右上角的[主體倉庫]按鈕,點選此按鈕時會彈出一個啟用IFrame的Window控制元件,預設這個Window控制元件是隱藏的:
@(F.Window()
.Hidden(true)
.EnableResize(true)
.EnableMaximize(true)
.EnableClose(true)
.Height(600)
.Width(1020)
.IsModal(true)
.ClearIFrameAfterClose(false)
.IFrameUrl(Url.Content("~/Home/Themes"))
.EnableIFrame(true)
.Title("主題倉庫")
.ID("windowThemeRoller")
)
按鈕的程式碼放置在id=header的HTML片段中:
@(F.Button()
.EnableDefaultCorner(false)
.EnableDefaultState(false)
.IconFont(IconFont.Bank)
.IconAlign(IconAlign.Top)
.Text("主題倉庫")
.ID("btnThemeSelect")
.CssClass("icontopaction themes")
.Listener("click", "onThemeSelectClick")
)
通過Button的Listener屬性來指定點選按鈕時需要執行的JavaScript指令碼:
// 點選主題倉庫 function onThemeSelectClick(event) { F.ui.windowThemeRoller.show(); }
選擇某個主題後的邏輯也很簡單,把使用者選擇的值儲存到Cookie中然後重新整理頁面,這個邏輯不難,請自行檢視原始碼。
頁面重新整理後,如何從Cookie中讀取值並設定所需的主題呢?這個邏輯其實是在佈局檢視中完成的,下面來看下完整的佈局檢視:
@{ var F = Html.F(); } <!DOCTYPE html> <html> <head> <title>@ViewBag.Title - FineUIMvc 空專案</title> @F.RenderCss() <link href="~/res/css/common.css" rel="stylesheet" type="text/css" /> @RenderSection("head", false) </head> <body> @Html.AntiForgeryToken() @{ var pm = F.PageManager; // 主題 HttpCookie themeCookie = Request.Cookies["Theme_Mvc"]; if (themeCookie != null) { string themeValue = themeCookie.Value; Theme theme; if (Enum.TryParse<Theme>(themeValue, true, out theme)) { pm.CustomTheme(String.Empty); pm.Theme(theme); } else { pm.CustomTheme(themeValue); } } } @F.PageManager @RenderSection("body", true) @F.RenderScript() @RenderSection("script", false) </body> </html>
這個邏輯不難,首先從請求的HTTP引數中讀取需要的Cookie值,然後設定PageManager的Theme屬性,這裡之所以有個Enum.TryParse的邏輯,是因為Cookie中儲存的也可能是使用者自定義主題,兩者的處理不同。
登入頁面
1.訪問學生列表頁面,會要求使用者先登入,如果直接在瀏覽器位址列輸入:
http://localhost:64475/Students
2.頁面會被重定向到:http://localhost:64475/Login?ReturnUrl=%2fStudents
3.登入成功後,頁面會重定向到首頁:
4.此時點選使用者頭像的下拉選單項[安全退出],就會重定向到登入頁面。
這一系列過程是通過表單身份驗證完整的,先來看下登入頁面的檢視程式碼:
@{ ViewBag.Title = "Login"; var F = @Html.F(); } @section body { @(F.Window() .Width(350) .WindowPosition(WindowPosition.GoldenSection) .EnableClose(false) .IsModal(false) .Title("登入表單") .ID("Window1") .Items( F.SimpleForm() .ShowHeader(false) .LabelWidth(80) .BodyPadding(10) .ShowBorder(false) .ID("SimpleForm1") .Items( F.TextBox() .ShowRedStar(true) .Required(true) .Label("使用者名稱") .ID("tbxUserName"), F.TextBox() .ShowRedStar(true) .Required(true) .TextMode(TextMode.Password) .Label("密碼") .ID("tbxPassword") ) ) .Toolbars( F.Toolbar() .Position(ToolbarPosition.Bottom) .ToolbarAlign(ToolbarAlign.Right) .ID("Toolbar1") .Items( F.Button() .OnClick(Url.Action("btnLogin_Click"), "SimpleForm1") .ValidateTarget(Target.Top) .ValidateForms("SimpleForm1") .Type(ButtonType.Submit) .Text("登入") .ID("btnLogin"), F.Button() .Type(ButtonType.Reset) .Text("重置") .ID("btnReset") ) ) ) }
這個頁面是由幾個FineUIMvc控制元件組成的:
1.Window控制元件:預設彈出的Window控制元件位於頁面的中央位置。
2.Toolbar控制元件:Window控制元件的底部工具欄。
3.Button控制元件:工具欄中的[登入]和[重置]按鈕。
4.TextBox控制元件:[使用者名稱]和[密碼]文字輸入框。
點選[登入]按鈕,發起一個HTTP POST請求,這個請求對應於控制器Login的btnLogin_Click方法,第二個引數SimpleForm1用於指定用於傳入控制器方法的表單引數。
這個過程對於WebForms開發者來說應該很面熟,如果用WebForms的術語我可以這麼來說:點選[登入]按鈕,回發當前頁面,觸發後臺的btnLogin_Click事件。這裡我特地保留了WebForms中後臺事件的命名方法,其實換湯不換藥,百變不離其宗,瞭解HTTP協議的大概工作原理,就不難理解。
後臺的btnLogin_Click方法:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult btnLogin_Click(string tbxUserName, string tbxPassword) { if (tbxUserName == "admin" && tbxPassword == "admin") { FormsAuthentication.RedirectFromLoginPage(tbxUserName, false); } else { ShowNotify("使用者名稱或密碼錯誤!", MessageBoxIcon.Error); } return UIHelper.Result(); }
退出操作:
[HttpPost] [ValidateAntiForgeryToken] public ActionResult onSignOut_Click() { FormsAuthentication.SignOut(); return RedirectToAction("Index", "Login"); }
學生列表頁面(CRUD)
FineUIMvc中所有去往伺服器的POST請求都是AJAX,所以我們可以毫不費力的製作單頁應用程式。配合FineUIMvc內建的IFrame支援,可以將一些邏輯獨立成一個頁面,不僅有助於程式碼的解耦,而且頁面效果也比較統一。
學生列表首頁:
在這個頁面中,我們可以進行如下操作:
1.新增使用者:彈出一個啟用IFrame的Window控制元件,在新頁面中進行新增使用者操作,操作結束後,需要重新繫結表格資料(因為資料已經改變了)。
2.編輯使用者:和新增是類似的邏輯。
3.刪除單個使用者:在表格行中集成了刪除按鈕,刪除之前會有確認提示框。
4.刪除多個使用者:點選表格工具欄中的[刪除選中記錄],可以一次刪除多條記錄,同樣刪除之前會有確認提示框。
表格頁面
我們先來看下錶格的檢視程式碼:
@(F.Grid() .IsViewPort(true) .ShowHeader(false) .ShowBorder(false) .ID("Grid1") .DataIDField("ID") .DataTextField("Name") .EnableCheckBoxSelect(true) .Toolbars( F.Toolbar() .Items( F.Button() .ID("btnDeleteSelected") .Icon(Icon.Delete) .Text("刪除選中記錄") .Listener("click", "onDeleteSelectedClick"), F.ToolbarFill(), F.Button() .ID("btnCreate") .Icon(Icon.Add) .Text("新增使用者") .Listener("click", "onCreateClick") ) ) .Columns( F.RowNumberField(), F.RenderField() .HeaderText("姓名") .DataField("Name") .Width(100), F.RenderField() .HeaderText("性別") .DataField("Gender") .FieldType(FieldType.Int) .RendererFunction("renderGender") .Width(80), F.RenderField() .HeaderText("所學專業") .DataField("Major") .ExpandUnusedSpace(true), F.RenderField() .HeaderText("入學日期") .DataField("EntranceDate") .FieldType(FieldType.Date) .Renderer(Renderer.Date) .RendererArgument("yyyy-MM-dd") .Width(100), F.RenderField() .HeaderText("") .Width(60) .RendererFunction("renderEditField") .TextAlign(TextAlign.Center) .EnableHeaderMenu(false), F.RenderField() .HeaderText("") .Width(60) .RendererFunction("renderDeleteField") .TextAlign(TextAlign.Center) .EnableHeaderMenu(false) ) .DataSource(Model) )
這裡除了對錶格列的定義之外,就是表格頂部工具欄的定義,裡面包含圖示的兩個操作按鈕。
表格資料是通過DataSource方法指定的,傳入的Model引數型別在頁面頭部有定義:
@model IEnumerable<FineUIMvc.QuickStart.Models.Student>
行渲染函式RendererFunction
同時注意性別列和表格最後的兩個操作列,都定義了RendererFunction方法,它用來指定列的客戶端渲染指令碼。對於性別列,我們知道其資料型別為int,所以需要通過客戶端渲染函式來轉換為字串:
function renderGender(value, params) { return value == 1 ? '男' : '女'; }
而兩個操作列,則需要返回包含編輯和刪除圖示的HTML片段:
function renderDeleteField(value, params) { return '<a href="javascript:;" class="deletefield"> <img class="f-grid-cell-icon" src="@Url.Content("~/res/icon/delete.png")"></a>'; } function renderEditField(value, params) { return '<a href="javascript:;" class="editfield"> <img class="f-grid-cell-icon" src="@Url.Content("~/res/icon/pencil.png")"></a>'; }
行中刪除圖示的點選事件
下面看下如何在指令碼中處理行中的編輯圖示和刪除圖示的點選事件:
F.ready(function () { var grid1 = F.ui.Grid1; grid1.el.on('click', 'a.deletefield', function (event) { var rowEl = $(this).closest('.f-grid-row'); var rowData = grid1.getRowData(rowEl); F.confirm({ message: '你確定要刪除選中的行資料嗎?', target: '_top', ok: function () { deleteSelectedRows([rowData.id]); } }); }); });
首先通過F.ui.Grid1來獲取頁面上表格的例項,而F.ui.Grid1.el則是一個標準的jQuery物件,表示此表格在頁面上的DOM元素。然後通過jQuery的on函式來註冊編輯圖示和刪除圖示的點選事件。
在刪除事件中,通過jQuery的closest函式獲取編輯圖示所在的表格行,然後呼叫表格的getRowData方法獲取行資料,刪除時需要知道本行的行ID。然後呼叫F.confirm彈出確認對話方塊,在使用者點選確認對話方塊的確認按鈕時,執行刪除操作(deleteSelectedRows函式)。
之所以將刪除邏輯放到deleteSelectedRows中,是因為在批量刪除時也需要用到,所以提取為公共方法:
function deleteSelectedRows(selectedRows) { // 觸發後臺事件 F.doPostBack('@Url.Action("Grid1_Delete")', { 'selectedRows': selectedRows, 'Grid1_fields': F.ui.Grid1.fields }); }
這裡呼叫了FineUIMvc封裝好的AJAX POST方法F.doPostBack(類似於WebForms中的__doPostBack的命名),第一個引數指定了請求的URL,第二個引數指定請求中附加的表單引數。
行中編輯圖示的點選事件
由於需要在Window控制元件中彈出新增使用者頁面和編輯使用者頁面,所以我們還需要一個隱藏的Window控制元件:
@(F.Window() .ID("Window1") .Width(600) .Height(300) .IsModal(true) .Hidden(true) .Target(Target.Top) .EnableResize(true) .EnableMaximize(true) .EnableIFrame(true) .IFrameUrl(Url.Content("about:blank")) .OnClose(Url.Action("Window1_Close"), "Grid1") )
注意:我們為Window控制設定Target=Top屬性,表明在頂層頁面中彈出這個窗體,而不是侷限在當前頁面內,這個是FineUIMvc內建的特性,並且僅對於啟用IFrame的Window窗體有效(EnableIFrame)。
在F.ready函式中註冊編輯圖示的點選事件:
grid1.el.on('click', 'a.editfield', function (event) { var rowEl = $(this).closest('.f-grid-row'); var rowData = grid1.getRowData(rowEl); F.ui.Window1.show('@Url.Content("~/Students/Edit/")?studentId=' + rowData.id, '編輯使用者'); });
在編輯事件中,同樣先取得當前行資料,然後呼叫F.ui.Window1.show來在Window控制元件中顯示編輯頁面,第二個引數[編輯使用者]指定Window控制元件的標題欄文字。
編輯窗體中的[儲存後關閉]按鈕邏輯
先來看下Edit檢視程式碼:
@{ ViewBag.Title = "Edit"; var F = @Html.F(); } @model FineUIMvc.QuickStart.Models.Student @section body { @(F.Panel() .ID("Panel1") .ShowBorder(false) .ShowHeader(false) .BodyPadding(10) .AutoScroll(true) .IsViewPort(true) .Toolbars( F.Toolbar() .Items( F.Button() .Icon(Icon.SystemClose) .Text("關閉") .Listener("click", "F.activeWindow.hide();"), F.ToolbarSeparator(), F.Button() .ValidateForms("SimpleForm1") .Icon(Icon.SystemSaveClose) .OnClick(Url.Action("btnEdit_Click"), "SimpleForm1") .Text("儲存後關閉") ) ) .Items( F.SimpleForm() .ID("SimpleForm1") .ShowBorder(false) .ShowHeader(false) .Items( F.HiddenFieldFor(m => m.ID), F.TextBoxFor(m => m.Name), F.RadioButtonListFor(m => m.Gender) .Items( F.RadioItem() .Text("男") .Value("1"), F.RadioItem() .Text("女") .Value("0") ), F.TextBoxFor(m => m.Major), F.DatePickerFor(m => m.EntranceDate) .EnableEdit(false) ) ) ) }
讓我們把關注