經過前三節基礎理論的學習,我們在本節中開始我們的MVC實例演練之旅。在VS.NET中創建新的"ASP.NET MVC 3 Web Application"項目,並取個項目名:Miracle.Mvc.PartyInvites。為了簡單起見,分別選擇空模板、Razor視圖引擎並取消"使用html5語義化標簽"選項。項目創建完畢如下圖所示:
從圖中可以看出,項目中包含很多MVC自動生成的文件。特別注意的是,我們注意以下三個文件夾:Models、Views和Controllers,即我們俗稱的MVC。現在我們直接運行剛才的項目,看看有什么結果:瀏覽器會顯示"無法找到資源"的錯誤。根本原因在於:當瀏覽器發送請求(這里是默認請求:http://server:8080/)時,MVC會根據Global.asax中的默認設置(controller = "Home", action = "Index", id = UrlParameter.Optional)映射匹配請求的URL。我們發現項目中缺少Home的控制器和Index的action。
public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); }
於是我們在Controllers中添加HomeController,然后添加Index方法。再次運行項目,將在瀏覽器中顯示"Hello, Miracle"字符串。
public class HomeController : Controller { public string Index() { return "Hello, Miracle"; } }
雖然這段代碼如此簡單,但卻為我們了解MVC結構開了個好頭。MVC利用傳統的ASP.NET的路由機制(Routing),將URL映射為對應的控制器和action。如對我們以上實例而言,以下URL(http://server/,http://server/Home,http://server/Home/Index)都會映射為同一控制器和action,當前我們也可配置Global.asax中默認的路由匹配規則,來自定義配置路由規則。以上僅僅在客戶端生成了一個字符串,我們來做點稍微有意義的:生成html對應的Page。改變一下Index的代碼:
public class HomeController : Controller { public ViewResult Index() { return View(); } }
此時如果直接運行,也將會顯示"未找到視圖或MasterPage"的錯誤。估計是缺少什么吧,不難發現Index的返回值變成了ViewResult,而函數體變成了return View();我們知道,MVC控制器中的action方法除了返回字符串外,還可以返回視圖ViewResult,此時action方法將對應於Views目錄下指定控制器對應的View,我們可以試着在Index方法名或函數體中點擊添加View,選擇Razor視圖引擎並取消"創建強類型視圖"和"使用布局和模板頁"選項。添加完畢后會發現在Views目錄下生成了Views/Home/Index.cshtml。再次運行項目會顯示"Hello, Miracle (from the view)"。我們並沒有指定訪問哪個視圖,那怎么就能正確顯示了呢?原因在於MVC的視圖命名規則(即View視圖名稱與Controller中action的名稱一致)。
@{ Layout = null; } <!DOCTYPE html> <html> <head> <title>Index</title> </head> <body> <div> Hello, Miracle (from the view) </div> </body> </html>
MVC除了能返回ViewResult之外,還能返回RedirectResult,HttpUnautorizedResult等視圖(都繼承於ActionResult)。可能到這里,MVC僅僅輸出一些字符串或html等靜態文本,讀者仍然沒有發現MVC的強大魅力,不過先不着急。
接下來,我們來探討如何在MVC中實現動態輸出,通常而言構建數據是Controller的主要工作,渲染html是View的主要工作,數據將從Controller傳遞到View中。那如何實現數據傳遞的呢?在MVC中依靠ViewBag這個動態對象來完成,有意思的是,這個對象可在Controller中隨意指定屬性,而在View中來接收這個屬性,達到數據傳遞的目的。再次運行項目,將顯示"Good Morning,Miracle (from the view)"。
public ViewResult Index() { var hour = DateTime.Now.Hour; ViewBag.Greeting = hour < 12 ? "Good Morning" : "Good Afternoon";
return View(); }
@{ Layout = null; } <!DOCTYPE html> <html> <head> <title>Index</title> </head> <body> <div> @ViewBag.Greeting, Miracle (from the view) </div> </body> </html>
有了以上對MVC基本框架的了解,我們來深入這個實例。客戶現在要求制作一張聚會賀卡,並讓受邀請的人在網站上進行電子回復(RSVP)。網站應滿足以下幾個特征:
(1).首頁顯示晚會的基本信息;(2).能進行電子回復的頁面(RSVP);(3).驗證回復的信息,並跳轉到感謝頁;(4).RSVP電子郵件回復受邀者。我們來改動以下Index.cshtml:
//... @ViewBag.Greeting, Miracle (from the view) <p>We're going to have an excting party.<br/> (To do: sell it better. Add pictures or something.) </p>
由於需要進行電子回復,我們在主頁中添加鏈接以便客人能進行電子回復。運行之后將會產生一個超鏈接(http://server/Home/RsvpForm),其中對應於兩個參數:第一個是顯示的鏈接名稱,第二個是對應的action名稱。與Asp.Net不同的是,前者是對應與文件夾下的文件,后者對應於Controller下的Action方法。
@ViewBag.Greeting, Miracle (from the view) <p>We're going to have an excting party.<br/> (To do: sell it better. Add pictures or something.) </p> @Html.ActionLink("RSVP Now", "RsvpForm")
其中可能包含姓名、郵箱、電話等信息,這時我們封裝成一個對象(Guest)來完成,也就是MVC中的"M"了。接下來在Models目錄下創建以下類。
public class Guest { public string Name { get; set; } public string Email { get; set; } public string Phone { get; set; } public bool? IsAttend { get; set; } }
接下來創建對應的Action方法和對應的視圖:RsvpForm和Views/Home/RsvpForm.cshtml。由於在電子回復中包含了客戶的相關信息,因此我們創建視圖時選擇"強類型視圖"選項。
@model Miracle.Mvc.PartyInvites.Models.Guest @{ Layout = null; } <!DOCTYPE html> <html> <head> <title>RsvpForm</title> </head> <body> <div> @using (Html.BeginForm()) { <p>Your name: @Html.TextBoxFor(m => m.Name)</p> <p>Your email: @Html.TextBoxFor(m => m.Email)</p> <p>Your phone: @Html.TextBoxFor(m => m.Phone)</p> <p>Will you attend? @Html.DropDownListFor(m => m.IsAttend, new[] { new SelectListItem() { Text="Yes, I'll be there", Value = bool.TrueString }, new SelectListItem() { Text="No, I can't come", Value = bool.FalseString } }, "Choose an option") </p> <input type="submit" value="Submit Rsvp"/> } </div> </body> </html>
從以上代碼中不難發現:@model Miracle.Mvc.PartyInvites.Models.Guest,正是由於強類型視圖,使得我們可以直接利用model來訪問對應的屬性。此外Html.BeginForm是創建form,類似於<form action="/Home/RsvpForm" method="post"></form>。細心的讀者會發現,我們現在的MVC視圖Form是運行在客戶端的,而傳統的Asp.Net Form是運行在服務端的(runat="server")。我們填寫相關的信息,點擊"Submit Rsvp"之后意外發現我們剛才輸入的東西全不見了。這與傳統的Form不太一致,原因是什么呢?傳統的Web Form會保存當前頁面的數據到_ViewState變量中,並賦值給隱藏變量,當頁面回發時再將這些值賦到對應的控件上。而這里的MVC Form由於運行在客戶端,也沒有_ViewState、隱藏變量,更不存在回發的任何機制,而頁面跳轉到本身,相當於重新渲染了一次頁面,因此數據丟失。后續我們將有辦法來解決這個問題。
從上面我們看到,點擊按鈕沒有觸發任何事件,MVC也就不知道如何提交到服務器了。我們知道通常HTML請求都會分為get和post請求。這里試着給讀者將一下兩者的應用,get請求適合於首次訪問頁面(或點擊鏈接)等渲染的頁面(可能是空白頁面,也可能是已經有數據的頁面),post請求是當在此頁面中填入相關數據后保存到服務器時執行提交的動作(form的默認請求方法為post)。我們再次來修改RsvpForm方法:
[HttpGet] public ViewResult RsvpForm() { return View(); } [HttpPost] public ViewResult RsvpForm(Models.Guest guest) { //TODO: send email to response the organizer return View("Thanks", guest); }
重點關注第2個方法,首先是基於HttpPost請求,將guest的相關數據發送到Thanks視圖,從上面的html頁面源代碼中可以看到Guest對象的相關屬性已經映射到控件對應的id和name上,這里的return View("Thanks", guest)將客戶數據從Rsvp Form傳遞到Thanks Form中,也即利用Data Binding完成了View之間數據的傳遞。接下來我們創建Views/Home/Thanks.cshtml視圖。
@model Miracle.Mvc.PartyInvites.Models.Guest @{ Layout = null; } <!DOCTYPE html> <html> <head> <title>Thanks</title> </head> <body> <div> <h1>Thank you, @Model.Name!</h1> @if (Model.IsAttend == true) { @: It's great that you're coming. The drinks are already in the fridge! } else { @: Sorry to hear that you can't make it, but thanks for letting us know. } </div> </body> </html>
OK,大功基本告成。但是有個致命問題是:無論我填不填表單 內容,都可以跳到感謝頁面。我們需要對那些進行認真填寫的客戶才會跳轉到感謝頁面,也就是說要對他們輸入的信息進行驗證。比如姓名必填、郵箱必須滿足規則等。回想以前的Web Form,可以通過驗證控件或JS驗證完成,而且多個地方驗證還要搬家似的到處粘貼,及其不方便。那我們就想是否可以只在一個地方驗證就可以了呢,回頭去看那個Model,只需要添加相關的MVC特性(需引入System.ComponentModel.DataAnnotations)即可完成。
public class Guest { [Required(ErrorMessage = "Please enter your name")] public string Name { get; set; } [Required(ErrorMessage = "Please enter your email")] [RegularExpression(".+\\@.+\\..+", ErrorMessage = "Please enter a valid emmail")] public string Email { get; set; } [Required(ErrorMessage = "Please enter your phone")] public string Phone { get; set; } [Required(ErrorMessage = "Please verify whether you will attend")] public bool? IsAttend { get; set; } }
我們在Controller中實現驗證邏輯,根據ModelState.IsValid來判斷是否通過驗證。
[HttpPost] public ViewResult RsvpForm(Models.Guest guest) { if (ModelState.IsValid) { //TODO: send email to response the organizer return View("Thanks", guest); } else { return View(); } }
同時在View中當不通過驗證時顯示出這些錯誤信息。
@using (Html.BeginForm())
{
@Html.ValidationSummary()
//...
}
再次運行,將發現只有通過驗證才能跳轉到感謝頁。同時請讀者注意,此時的Rsvp Form可以進行數據保存了,點擊按鈕之后頁面數據不再發生丟失了。原因在於頁面不再進行重新渲染,而是接收了上一次HttpPost請求的結果。而HttpPost又保存了Guest對象的相關信息,因此不會丟失(但值得一提的是,打開頁面源代碼沒有任何的_ViewState標記或隱藏變量)。相反添加了很多語義化的屬性:
<div class="validation-summary-errors" data-valmsg-summary="true"> <ul> <li>Please enter your phone</li> </ul> </div> <p> Your name: <input data-val="true" data-val-required="Please enter your name" id="Name" name="Name" type="text" value="Miracle" /></p> <p> Your email: <input data-val="true" data-val-regex="Please enter a valid emmail" data-val-regex-pattern=".+\@.+\..+" data-val-required="Please enter your email" id="Email" name="Email" type="text" value="hmiinyu@sina.com" /></p> <p> Your phone: <input class="input-validation-error" data-val="true" data-val-required="Please enter your phone" id="Phone" name="Phone" type="text" value="" /></p> <p> Will you attend? <select data-val="true" data-val-required="Please verify whether you will attend" id="IsAttend" name="IsAttend"> <option value="">Choose an option</option> <option selected="selected" value="True">Yes, I'll be there</option> <option value="False">No, I can't come</option> </select> </p>
以輸入的姓名為例,當正確輸入時,控件將渲染成:
<input data-val="true" data-val-required="Please enter your name" id="Name" name="Name" type="text" value="Miracle" />
當輸入無效時,控件將渲染成:
<input class="input-validation-error" data-val="true" data-val-required="Please enter your name" id="Name" name="Name" type="text" value="" />
我們需要引入相關的CSS(其實就是Content目錄下的site.css),利用@Href操作符來完成。
<link href="@Href("~/Content/Site.css")" rel="stylesheet" type="text/css" />
最后,我們通過WebMail helper來完成郵件發送動作。
@model Miracle.Mvc.PartyInvites.Models.Guest @{ Layout = null; } <!DOCTYPE html> <html> <head> <title>Thanks</title> </head> <body> @{ try { WebMail.SmtpServer = "smtp.miracle.com"; WebMail.SmtpPort = 8080; WebMail.EnableSsl = true; WebMail.UserName = "Miracle"; WebMail.Password = "Miracle"; WebMail.From = "miracle@rsvp.com"; WebMail.Send("party-host@rsvp.com", "RSVP Notification", Model.Name + " is " + ((Model.IsAttend ?? false) ? "" : "not") + "attending"); } catch { @: <b>Sorry - we couldn't send the email to confirm your RSVP.</b> } } <div> <h1>Thank you, @Model.Name!</h1> @if (Model.IsAttend == true) { @: It's great that you're coming. The drinks are already in the fridge! } else { @: Sorry to hear that you can't make it, but thanks for letting us know. } </div> </body> </html>
到此為止,我們展示了一個簡單的MVC應用,說明是如何應用基礎理論到實踐的過程,讀者可在此下載源代碼進行參考分析。