譯文,個人原創,轉載請注明出處(C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(上)),不對的地方歡迎指出與交流。
章節出自《Professional C# 6 and .NET Core 1.0》。水平有限,各位閱讀時仔細分辨,唯望莫誤人子弟。
附英文版原文:Professional C# 6 and .NET Core 1.0 - Chapter 41 ASP.NET MVC
C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(中)
C# 6 與 .NET Core 1.0 高級編程 - 41 ASP.NET MVC(下)
------------------------------------
本章內容
- ASP.NET MVC 6的特性
- 路由
- 創建控制器
- 創建視圖
- 驗證用戶輸入
- 使用過濾器
- 使用HTML和標簽助手
- 創建數據驅動的Web應用程序
- 實現認證和授權
Wrox.com網站下載本章代碼
wrox.com中關於本章的代碼下載位於 http://www.wrox.com/go/professionalcsharp6 的“下載代碼”選項卡上。本章的代碼主要有以下主要示例:
- MVC Sample App
- Menu Planner
設置 ASP.NET MVC 6 服務
第40章“ASP.NET Core”展示了ASP.NET MVC的基礎:ASP.NET Core 1.0 第40章展示了中間件以及依賴注入如何與ASP.NET結合使用。本章通過注入ASP.NET MVC服務來使用依賴注入。
ASP.NET MVC是基於MVC(模型 - 視圖 - 控制器)模式。如 圖41.1所示,這種標准模式(一個文檔中描述的設計模式:由Gang of Four [Addison-Wesley Professional,1994]的可重復使用的面向對象軟件的元素)定義了一個實現數據實體和數據訪問的模型,向用戶展示信息的視圖,以及利用模型向視圖發送數據的控制器。控制器從瀏覽器接收請求並返回響應。要創建響應,控制器可以使用模型提供一些數據,以及一個視圖來定義返回的HTML。
圖41.1
ASP.NET MVC 的控制器和模型通常使用運行服務器端的C#和.NET代碼創建。視圖是帶有JavaScript的HTML代碼,視圖中有時也會有少量用於訪問服務器端信息的C#代碼。
MVC模式中的這種分離的最大優點是,可以使用單元測試輕松測試功能。控制器只包含具有參數和返回值的方法,這些參數和返回值可以通過單元測試輕松遍歷。
現在開始為ASP.NET MVC 6設置服務。ASP.NET Core 1.0的依賴注入是深入集成的,如第40章中所示。可以選擇ASP.NET Core 1.0模板“Web應用程序”創建一個ASP.NET MVC 6項目。該模板已經包括ASP.NET MVC 6所需的NuGet包以及幫助組織應用程序的目錄結構。但是,這里將從空模板開始(類似於第40章),所以你可以看到創建一個ASP.NET MVC 6項目所需要的東西,不會有項目可能不需要的額外的東西。
創建的第一個項目名為 MVCSampleApp。要在 Web應用程序MVCSampleApp 使用ASP.NET MVC,需要添加NuGet包 Microsoft.AspNet.Mvc 。該包准備就緒后,通過在ConfigureServices方法中調用擴展方法AddMvc來添加MVC服務(代碼文件MVCSampleApp/Startup.cs):
using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; // etc.
namespace MVCSampleApp { public class Startup { // etc.
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); // etc.
} // etc.
public static void Main(string[] args) { var host = new WebHostBuilder() .UseDefaultConfiguration(args) .UseStartup<Startup>() .Build(); host.Run(); } } }
AddMvc 擴展方法添加和配置幾個 ASP.NET MVC核心服務,如配置功能(IConfigureOptions與MvcOptions和RouteOptions);控制器工廠和控制器觸發器(IControllerFactory,IControllerActivator)、動作方法選擇器,調用者和約束提供者(IActionSelector,IActionInvokerFactory,IActionConstraintProvider)、參數綁定器和模型驗證器(IControllerActionArgumentBinder,IObjectModelValidator)、和過濾器提供程序(IFilterProvider)。
它除了增加核心服務,AddMvc方法還添加ASP.NET MVC服務以支持授權,CORS,數據注釋,視圖,Razor視圖引擎等。
定義路由
第40章解釋了 IapplicationBuilder 的 Map 擴展方法如何定義一個簡單的路由。本章展示了 ASP.NET MVC 路由如何基於映射提供了一種靈活的路由機制,用於將URL映射到控制器和操作方法。
控制器是基於路由選擇的。創建默認路由的一種簡單方法是在啟動類中調用UseMvcWithDefaultRoute方法(代碼文件MVCSampleApp/Startup.cs):
public void Configure(IApplicationBuilder app) { // etc.
app.UseIISPlatformHandler(); app.UseStaticFiles(); app.UseMvcWithDefaultRoute(); // etc.
}
注意 擴展方法 UseStaticFiles 已在第40章討論。此方法需要添加 Microsoft.AspNet.StaticFiles NuGet包。
使用默認路由后,控制器類型的名稱(不帶Controller后綴)和方法名稱組成路由,例如 http://server[:port]/controller/action 。還可以使用名為id的可選參數,例如: http://server[:port]/controller/action/id 。控制器的默認名稱為 Home ; action方法的默認名稱為Index。
以下代碼段顯示了指定相同默認路由的另一種方法。 UseMvc方法可以接收類型 Action<IRouteBuilder> 的參數。此IRouteBuilder接口包含映射的路由列表。使用 MapRoute 擴展方法定義路由:
app.UseMvc(routes =< with => routes.MapRoute( name:"default", template:"{controller}/{action}/{id?}", defaults: new {controller ="Home", action ="Index"} ));
此路由定義與默認路由定義相同。template 參數定義URL,id? 定義了該參數是可選的; defaults 參數定義URL的控制器和動作部分的默認值。
讓我們看看這個URL:
http://localhost:[port]/UseAService/GetSampleStrings
這個URL 中 UseAService 映射到控制器的名稱,因為 Controller 后綴是自動添加的,類型名稱為UseAServiceController,GetSampleStrings 是操作,它表示 UseAServiceController 類中的一個方法。
添加路由
添加或更改路由有幾個原因。例如,可以修改路由去使用鏈接操作,將Home定義為默認控制器,向鏈接添加實體或使用多個參數。
可以定義路由讓用戶使用鏈接(例如 http://<server>/About )在Home控制器中尋址“關於”操作方法,而不傳遞控制器名稱,如以下代碼段所示。請注意,控制器名稱不在URL中。 controller關鍵字對於路由是必需的,但可以為其提供默認值:
app.UseMvc(routes => routes.MapRoute( name:"default", template:"{action}/{id?}", defaults: new {controller ="Home", action ="Index"} ));
更改路由的另一個方案如以下代碼段所示。在此代碼段中,向路徑中添加變量 language 。該變量設置為URL中位於服務器名稱之后並放置在控制器之前,例如 http://server/en/Home/About 。可以使用它來指定語言:
app.UseMvc(routes => routes.MapRoute( name:"default", template:"{controller}/{action}/{id?}", defaults: new {controller ="Home", action ="Index"} ).MapRoute( name:"language", template:"{language}/{controller}/{action}/{id?}", defaults: new {controller ="Home", action ="Index"} );
如果一個路由匹配並且找到了控制器和動作方法,則采取路由,否則選擇下一條路由,直到找到一條路由匹配。
使用路由約束
映射路由時,可以指定約束。這種情況下,不符合約束定義的URL 是不能被解析的。以下約束定義 language 參數使用正則表達式(en)|(de)來指定只能是 en或de。例如 只有 http://<server>/en/Home/About 或 http://<server>/de/Home/About 的URL有效:
app.UseMvc(routes => routes.MapRoute( name:"language", template:"{language}/{controller}/{action}/{id?}", defaults: new {controller ="Home", action ="Index"}, constraints: new {language = @"(en)|(de)"} ));
如果鏈接只應啟用數字(例如,要訪問具有產品編號的產品),正則表達式 \d+ 匹配任意數量的數字,但必須至少匹配一個數字:
app.UseMvc(routes => routes.MapRoute( name:"products", template:"{controller}/{action}/{productId?}", defaults: new {controller ="Home", action ="Index"}, constraints: new {productId = @"\d+"} ));
現在已經看到了路由如何指定使用的控制器和控制器的動作。下一節“創建控制器”介紹了控制器的詳細信息。
創建控制器
控制器對來自用戶的請求做出反應並發送響應。本節所講述的內容,可以不需要視圖。
使用ASP.NET MVC時有一些約定要優於配置。控制器也會看到一些約定。可以在目錄 Controllers 中找到控制器,並且控制器類的名稱必須以名稱 Controller 為后綴。
在創建第一個控制器之前,先創建Controllers目錄。然后,可以通過在解決方案資源管理器中選擇此目錄來創建控制器,從上下文菜單中選擇 Add -> New Item,並選擇MVC控制器類項目模板。 HomeController是為指定的路由創建的。
生成的代碼包含從基類Controller派生的HomeController類。此類還包含與 Index 操作對應的 Index 方法。當請求由路由定義的操作時,將調用控制器中的方法(代碼文件MVCSampleApp/Controllers/HomeController.cs):
public class HomeController : Controller { public IActionResult Index() => View(); }
了解操作方法
控制器包含操作(譯者注: action,也譯作 動作)方法。以下代碼片段有一個簡單的動作 Hello 方法(代碼文件MVCSampleApp/Controllers/HomeController.cs):
public string Hello() =>"Hello, ASP.NET MVC 6";
可以使用鏈接 http://localhost:5000/Home/Hello 在Home控制器中調用Hello操作。當然,端口號取決於設置,可以使用項目設置中的網絡屬性進行配置。從瀏覽器打開此鏈接時,控制器只返回字符串“Hello, ASP.NET MVC 6”,沒有HTML - 只是一個字符串。瀏覽器可以顯示字符串。
動作可以返回任何內容,例如,圖像,視頻,XML或JSON數據的字節,當然,也可以是HTML。視圖對於返回HTML有很大的幫助。
使用參數
可以使用參數聲明動作方法,如下面的代碼片段(代碼文件MVCSampleApp/Controllers/HomeController.cs):
public string Greeting(string name) => HtmlEncoder.Default.Encode($"Hello, {name}");
注意 HtmlEncoder需要NuGet包System.Text.Encodings.Web。
有了這個聲明,可以調用Greeting操作方法去請求以下的URL,在URL中傳遞帶有name參數的值:http://localhost:18770/Home/Greeting?name=Stephanie。
要使用更容易記住的鏈接,可以使用路由信息來指定參數。 Greeting2操作方法指定名為id的參數。
public string Greeting2(string id) => HtmlEncoder.Default.Encode($"Hello, {id}");
這與默認路由{controller}/{action}/{id?}匹配,其中id被指定為可選參數。現在使用下面的鏈接,id參數包含字符串Matthias:http://localhost:5000/Home/Greeting2/Matthias。
還可以使用任意數量的參數聲明操作方法。例如,可以使用兩個參數將“Add”操作方法添加到Home控制器,如下所示:
public int Add(int x, int y) => x + y;
可以使用URL http://localhost:18770/Home/Add?x=4&y=5 調用此操作以填充x和y參數。
使用多個參數,還可以定義一個路由以使用不同的鏈接傳遞值。以下代碼片段顯示了在路由表中定義的一個附加路由,以指定填充變量x和y的多個參數(代碼文件MVCSampleApp/Startup.cs):
app.UseMvc(routes =< routes.MapRoute( name:"default", template:"{controller}/{action}/{id?}", defaults: new {controller ="Home", action ="Index"} ).MapRoute( name:"multipleparameters", template:"{controller}/{action}/{x}/{y}", defaults: new {controller ="Home", action ="Add"}, constraints: new {x = @"\d", y = @"\d"} ));
現在,可以使用此URL調用與之前相同的操作:http://localhost:18770/Home/Add/7/2。
注意 本章后面的“將數據傳遞到視圖”部分中,將看到可以使用自定義類型的參數,以及客戶端的數據如何映射到屬性。
返回數據
目前為止,只從控制器返回字符串值。通常,返回實現接口 IActionResult 的對象。
以下是 ResultController 類的幾個示例。第一個代碼片段使用 ContentResult 類返回簡單的文本內容。可以使用來自基類Controller 的方法來返回 ActionResults,而不是創建 ContentResult 類的實例並返回該實例。在以下示例中,方法 Content 用於返回文本內容。 Content方法允許指定內容,MIME類型和編碼(代碼文件MVCSampleApp/Controllers/ResultController.cs):
public IActionResult ContentDemo() => Content("Hello World","text/plain");
要返回JSON格式的數據,可以使用Json方法。以下示例代碼創建一個Menu對象:
public IActionResult JsonDemo() { var m = new Menu { Id = 3, Text ="Grilled sausage with sauerkraut and potatoes", Price = 12.90, Date = new DateTime(2016, 3, 31), Category ="Main" }; return Json(m); }
Menu類在Models目錄中定義,它定義了一個簡單的僅有一些屬性的POCO 類(代碼文件MVCSampleApp/Models/Menu.cs):
public class Menu { public int Id {get; set;} public string Text {get; set;} public double Price {get; set;} public DateTime Date {get; set;} public string Category {get; set;} }
客戶端在響應正文中看到此JSON數據。 JSON數據可以輕松地作為JavaScript對象使用:
{"Id":3,"Text":"Grilled sausage with sauerkraut and potatoes",
"Price":12.9,"Date":"2016-03-31T00:00:00","Category":"Main"}
使用Controller類的Redirect方法,客戶端接收HTTP重定向請求。在接收到重定向請求后,瀏覽器請求它接收的鏈接。 Redirect方法返回一個RedirectResult(代碼文件MVCSampleApp/Controllers/ResultController.cs):
public IActionResult RedirectDemo() => Redirect("http://www.cninnovation.com");
還可以通過指定重定向到另一個控制器和操作來為客戶端生成重定向請求。 RedirectToRoute 返回一個 RedirectToRouteResult,用於指定路由名稱,控制器,操作和參數。這將構建一個使用HTTP重定向請求返回到客戶端的鏈接:
public IActionResult RedirectRouteDemo() => RedirectToRoute(new {controller ="Home", action="Hello"});
Controller基類的File方法定義了返回不同類型的不同重載。此方法可以返回FileContentResult,FileStreamResult和VirtualFileResult。不同的返回類型取決於所使用的參數 - 例如,VirtualFileResult的字符串,FileStreamResult的Stream和FileContentResult的字節數組。
下一個代碼段返回一個圖像。創建 Images 文件夾並添加一個JPG文件。要使下一個代碼段工作,請在 wwwroot 目錄中創建一個Images文件夾,並添加文件Matthias.jpg。示例代碼返回一個VirtualFileResult,它在第一個參數指定文件名,第二個參數指定MIME類型為image/jpeg的contentType參數:
public IActionResult FileDemo() => File("~/images/Matthias.jpg","image/jpeg");
下一節顯示如何返回不同的ViewResult。
使用控制器基類和POCO控制器
到目前為止,所有創建的控制器都是從基類 Controller 派生的。 ASP.NET MVC 6還支持不從這個基類派生的控制器 - 被稱為POCO(普通舊CLR對象)控制器。這樣,可以用自定義的基類來定義控制器類型層次結構。
從Controller基類中得到什么?這個基類能使控制器直接訪問基類的屬性。下表描述了這些屬性及其功能。
| 屬性 | 說明 |
| ActionContext | 此屬性包裝其他的屬性。這里可以獲取有關操作描述符的信息,其中包含操作的名稱,控制器,過濾器和方法信息; HttpContext可以從Context屬性直接訪問;可以從ModelState屬性直接訪問的模型的狀態以及可以從RouteData屬性直接訪問路由信息。 |
| Context | 此屬性返回HttpContext。通過它可以訪問 ServiceProvider 以訪問注冊了依賴注入(ApplicationServices屬性)的服務,身份驗證和用戶信息,請求和響應信息 都可以從 Request 和 Response屬性直接訪問的,以及Web套接字(如果它們正在使用)。 |
| BindingContext | 此屬性可以訪問將接收到的數據綁定到操作方法的參數的綁定器。將綁定請求信息綁定到自定義類型將在本章后面的“從客戶端提交數據”一節中討論。 |
| MetadataProvider | 使用綁定器綁定參數。綁定器可以使用與模型相關聯的元數據。MetadataProvider屬性可以訪問有關配置為處理元數據信息的提供程序的信息。 |
| ModelState | ModelState屬性可以知道模型綁定是成功還是有錯誤。如果出現錯誤,可以閱讀有關導致錯誤的屬性的信息。 |
| Request | 此屬性可以訪問有關 HTTP請求的所有信息:標題和正文信息,查詢字符串,表單數據和Cookie。 頭信息包含一個User-Agent字符串,提供有關瀏覽器和客戶端平台的信息。 |
| Response | 此屬性保存返回給客戶端的信息。可以發送cookie,更改標題信息,並直接寫入正文。 在本章前面的章節“啟動”中,已經看到了如何使用Response屬性將一個簡單的字符串返回給客戶端。 |
| Resolver | Resolver屬性返回ServiceProvider,可以在其中訪問注冊了依賴注入的服務。 |
| RouteData | RouteData屬性提供有關在啟動代碼中注冊的完整路由表的信息。 |
| ViewBag | 可以使用這些屬性向視圖發送信息。這將在稍后的“將數據傳遞到視圖”部分中解釋。 |
| ViewData | |
| TempData | 此屬性寫入在多個請求之間共享的用戶狀態(而寫入ViewBag和ViewData的數據可以寫入以在單個請求中共享視圖和控制器之間的信息)。默認情況下,TempData將信息寫入會話狀態。 |
| User | User屬性返回有關已驗證用戶的信息,包括身份和聲明。 |
POCO控制器沒有Controller基類,但訪問這些信息仍然很重要。以下代碼片段定義了從對象基類派生的POCO控制器(當然您可以使用自定義類型作為基類)。要使用POCO類創建一個ActionContext,可以創建此類型的屬性。 POCO Controller類使用ActionContext 作為此屬性的名稱,這類似於Controller類的方式。但是,有一個屬性不會自動設置。需要應用ActionContext屬性。使用此屬性注入實際的ActionContext。 Context屬性直接從ActionContext訪問HttpContext屬性。 Context屬性用於從UserAgentInfo 操作方法訪問並返回請求中的User-Agent頭信息(代碼文件MVCSampleApp/Controllers/POCOController.cs):
public class POCOController { public string Index() => "this is a POCO controller"; [ActionContext] public ActionContext ActionContext {get; set;} public HttpContext Context => ActionContext.HttpContext; public ModelStateDictionary ModelState => ActionContext.ModelState; public string UserAgentInfo() { if (Context.Request.Headers.ContainsKey("User-Agent")) { return Context.Request.Headers["User-Agent"]; } return"No user-agent information"; } }
創建視圖
返回到客戶端的HTML代碼最好使用視圖來指定。本節中的示例將創建 ViewsDemoController 。視圖都在Views文件夾中定義。 ViewsDemo 控制器的視圖需要一個 ViewsDemo子目錄。這是視圖的約定(代碼文件MVCSampleApp/Controllers/ViewsDemoController.cs):
public ActionResult Index() => View();
注意 另一個搜索視圖的地方是 Shared 目錄。可以將多個控制器(以及多個視圖使用的特殊部分視圖)要使用的視圖放入 Shared 目錄。
在Views目錄中創建 ViewsDemo 目錄之后,可以使用Add -> New Item並選擇 “MVC View Page” 項目模板來創建視圖。因為action方法具有名稱Index,所以視圖文件被命名為Index.cshtml。
操作方法Index使用沒有參數的View方法,因此視圖引擎將在ViewsDemo目錄中搜索與操作名稱相同名稱的視圖文件。在控制器中使用的View方法有允許傳遞不同視圖名稱的重載。在這種情況下,視圖引擎將查找傳遞給View方法的名稱的視圖。
視圖包含混合了一些服務器端代碼的HTML代碼,如下面的代碼段(代碼文件 VCSampleApp/Views/ViewsDemo/Index.cshtml)所示:
@{ Layout = null; } <!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Index</title> </head> <body> <div> </div> </body> </html>
服務器端代碼使用 @符號 寫入,它會啟動 Razor 語法,本章后面將討論。在介紹Razor語法的詳細信息之前,下一節將介紹如何將數據從控制器傳遞到視圖。
傳遞數據到視圖
控制器和視圖在同一進程中運行。視圖直接從控制器內創建。這使得將數據從控制器傳遞到視圖變得容易。為了傳遞數據,可以使用ViewDataDictionary。此字典將鍵存儲為字符串,並啟用對象值。可以將 ViewDataDictionary 與 Controller 類的 ViewData 屬性一起使用 - 例如,可以將字符串傳遞到使用鍵值 MyData 的字典: ViewData[“MyData”] =“Hello” 。一個更簡單的語法是使用ViewBag屬性。 ViewBag 是一個動態類型,可以分配任何屬性名稱來傳遞數據到視圖(代碼文件MVCSampleApp/Controllers/SubmitDataController.cs):
public IActionResult PassingData() { ViewBag.MyData ="Hello from the controller"; return View(); }
注意 使用動態類型的優點是沒有從視圖到控制器的直接依賴。動態類型在第16章“反射,元數據和動態編程”中有詳細說明。
在視圖中,可以用類似於控制器的方式訪問從控制器傳遞來的數據。視圖的基類(WebViewPage)定義了一個ViewBag屬性(代碼文件MVCSampleApp/Views/ViewsDemo/PassingData.cshtml):
<div> <div>@ViewBag.MyData</div> </div>
了解 Razor 語法
如上所述的視圖,視圖包含HTML和服務器端代碼。ASP.NET MVC 中可以使用Razor語法在視圖中編寫C#代碼。 Razor使用@字符 作為轉換字符。從@后面就是C#代碼。
使用Razor需要區分返回值的語句和不返回值的方法。可以直接使用返回的值。例如,ViewBag.MyData 返回一個字符串。該字符串直接放在HTML div標簽之間,如下所示:
<div>@ViewBag.MyData</div>
當調用返回 void 的方法或指定一些不返回值的其他語句時,需要一個Razor代碼塊。以下代碼塊定義了一個字符串變量:
@{
string name ="Angela";
}
現在可以使用簡單語法的變量,只需使用轉換字符@訪問變量:
<div>@name</div>
使用Razor語法,引擎會在找到HTML元素時自動檢測C#代碼的結束。在某些情況下可能無法自動檢測到C#代碼的結束。可以使用括號來解決此問題,如以下示例所示,以標記變量,然后正常文本繼續:
<div>@(name), Stephanie</div>
另一種啟動Razor代碼塊的方法是使用foreach語句:
@foreach(var item in list) { <li>The item name is @item.</li> }
注意 通常 Razor 自動檢測文本內容,例如,Razor檢測到打開的尖括號或帶有變量的括號。也有一些情況下不起作用。這里可以明確使用 @: 來定義文本的開始。
創建強類型視圖
將數據傳遞給視圖,我們已經看到了操作中的ViewBag。有另一種方法來傳遞數據到視圖 - 傳遞模型到視圖。使用模型允許您創建強類型視圖。
現在 ViewsDemoController 使用動作方法PassingAModel擴展。以下示例創建一個新的菜單項列表,並將此列表作為模型傳遞給Controller基類的View方法(代碼文件MVCSampleApp/Controllers/ViewsDemoController.cs):
public IActionResult PassingAModel() { var menus = new List<Menu> { new Menu { Id=1, Text="Schweinsbraten mit Knödel und Sauerkraut", Price=6.9, Category="Main" }, new Menu { Id=2, Text="Erdäpfelgulasch mit Tofu und Gebäck", Price=6.9, Category="Vegetarian" }, new Menu { Id=3, Text="Tiroler Bauerngröst'l mit Spiegelei und Krautsalat", Price=6.9, Category="Main" } }; return View(menus); }
當模型信息從動作方法傳遞到視圖時,可以創建強類型視圖。強類型視圖使用 model 關鍵字聲明。傳遞給視圖的模型類型必須與模型指令的聲明相匹配。在下面的代碼片段中,強類型視圖聲明類型 IEnumerable<Menu> ,它與模型類型匹配。因為 Menu 類在命名空間 MVCSampleApp.Models 中定義,所以這個命名空間使用 using 關鍵字打開。
從.cshtml文件創建的視圖的基類派生自基類 RazorPage 。有了一個模型,基類是 RazorPage<TModel> 類型;以下代碼片段基類是 RazorPage<IEnumerable<Menu>> 。此通用參數依次定義類型為 IEnumerable<Menu> 的Model屬性。代碼片段基類的Model屬性用於通過@foreach 遍歷 Menu 項,並顯示每個菜單的列表項(代碼文件MVCSampleApp/ViewsDemo/PassingAModel.cshtml):
@using MVCSampleApp.Models @model IEnumerable<Menu> @{ Layout = null; } <!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>PassingAModel</title> </head> <body> <div> <ul> @foreach (var item in Model) { <li>@item.Text</li> } </ul> </div> </body> </html>
可以將任何對象作為模型傳遞 - 任何視圖需要的都可以。例如,當編輯單個Menu對象時,可以使用類型為Menu的模型。顯示或編輯列表時,可以使用 IEnumerable<Menu> 。
運行應用程序顯示定義視圖時,會在瀏覽器中看到菜單列表,如圖41.2所示。

圖41.2
定義布局
通常,許多網頁應用程序共享相同的內容,例如版權信息、徽標和主導航結構。目前為止,所有視圖都包含完整的HTML內容,但是有一個更簡單的方法來管理共享的內容。這是布局頁面發揮作用的地方。
要定義布局,要設置視圖的 Layout 屬性。要定義所有視圖的默認屬性,可以創建視圖起始頁。將此文件放入Views文件夾,可以使用項目模板 MVC View Start Page 來創建它。創建文件_ViewStart.cshtml(代碼文件MVCSampleApp/Views/_ViewStart.cshtml):
@{
Layout ="_Layout";
}
對於不需要布局的所有視圖,可以將Layout屬性設置為null:
@{
Layout = null;
}
使用默認布局頁
可以使用項目模板 MVC View Layout Page 創建默認布局頁面。在共享文件夾中創建此頁面,以便它可用於來自不同控制器的所有視圖。項目模板 MVC View Layout Page 創建以下代碼:
<!DOCTYPE html> <html> <head> <meta name="viewport" content="width=device-width" /> <title>@ViewBag.Title</title> </head> <body> <div> @RenderBody() </div> </body> </html>
布局頁面包含使用此布局頁面的所有頁面通用的HTML內容(例如,頁眉,頁腳和導航)。您已經了解了視圖和控制器如何用ViewBag通信。布局頁面可以使用相同的機制。在內容頁面中定義 ViewBag.Title 的值,在布局頁面,上述代碼段中它顯示在HTML標題元素中。基類 RazorPage 的 RenderBody 方法呈現內容頁面的內容,從而定義內容應該放置的位置。
以下代碼段中,生成的布局頁面將更新為引用樣式表,並向每個頁面添加頁眉,頁腳和導航部分。environment,asp-controller和asp-action 是創建HTML元素的標簽助手。標簽助手將在本章后面的“助手”部分中討論(代碼文件MVCSampleApp/Views/Shared/_Layout.cshtml):
<!DOCTYPE html> <html> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <environment names="Development"> <link rel="stylesheet" href="~/css/site.css" /> </environment> <environment names="Staging,Production"> <link rel="stylesheet" href="~/css/site.min.css" asp-append-version="true" /> </environment> <title>@ViewBag.Title - My ASP.NET Application</title> </head> <body> <div class="container"> <header> <h1>ASP.NET MVC Sample App</h1> </header> <nav> <ul> <li><a asp-controller="ViewsDemo" asp-action="LayoutSample"> Layout Sample</a></li> <li><a asp-controller="ViewsDemo" asp-action="LayoutUsingSections"> Layout Using Sections</a></li> </ul> </nav> <div> @RenderBody() </div> <hr /> <footer> <p> <div>Sample Code for Professional C#</div> © @DateTime.Now.Year - My ASP.NET Application </p> </footer> </div> </body> </html>
為LayoutSample 操作 創建視圖(代碼文件MVCSampleApp/Views/ViewsDemo/LayoutSample.cshtml)。該視圖不設置Layout屬性,因此使用默認布局。以下代碼段設置ViewBag.Title,它在布局中的HTML標題元素中使用:
@{ ViewBag.Title ="Layout Sample"; } <h2>LayoutSample</h2> <p> This content is merged with the layout page </p>
運行應用程序時,將合並布局和視圖中的內容,如圖41.3所示。

圖41.3
使用部件
渲染正文和使用ViewBag不是在布局和視圖之間交換數據僅有的方法。部件區域可以定義命名內容應放置在視圖中的位置。以下代碼片段使用名為PageNavigation的部件。默認情況下,這些部件是必需的,如果未定義部件,則加載視圖將失敗。當required參數設置為false時,該部件變為可選(代碼文件MVCSampleApp/Views/Shared/_Layout.cshtml):
<!-- etc. --> <div> @RenderSection("PageNavigation", required: false) </div> <div> @RenderBody() </div> <!-- etc. -->
在視圖頁面中,section關鍵字定義了該部件。放置該部件的位置完全獨立於其他內容。視圖不定義頁面中的位置,這是由布局定義的(代碼文件MVCSampleApp/Views/ViewsDemo/LayoutUsingSections.cshtml):
@{ ViewBag.Title ="Layout Using Sections"; } <h2>Layout Using Sections</h2> Main content here @section PageNavigation { <div>Navigation defined from the view</div> <ul> <li>Nav1</li> <li>Nav2</li> </ul> }
運行應用程序時,視圖和布局中的內容將根據布局定義的位置合並,如圖41.4所示。

圖41.4
注意 部件不僅用於在HTML頁面的正文中放置某些內容,它們也可用於視圖在頭部中放置某些東西 - 例如,來自頁面的元數據。
部分視圖定義內容
布局為Web應用程序中的多個頁面提供了一個整體定義,也可以使用部分視圖來定義視圖中的內容。部分視圖沒有布局。
除此之外,部分視圖類似於正常視圖。部分視圖使用與普通視圖相同的基類,它們都有一個模型。
以下是部分視圖的示例。這里首先需要一個模型,該模型包含由 EventsAndMenusContext 類定義的獨立集合,事件和菜單的屬性(代碼文件MVCSampleApp/Models/EventsAndMenusContext.cs):
public class EventsAndMenusContext { private IEnumerable<Event> events = null; public IEnumerable<Event> Events { get { return events ?? (events = new List<Event>() { new Event { Id=1, Text="Formula 1 G.P. Australia, Melbourne", Day=new DateTime(2016, 4, 3) }, new Event { Id=2, Text="Formula 1 G.P. China, Shanghai", Day = new DateTime(2016, 4, 10) }, new Event { Id=3, Text="Formula 1 G.P. Bahrain, Sakhir", Day = new DateTime(2016, 4, 24) }, new Event { Id=4, Text="Formula 1 G.P. Russia, Socchi", Day = new DateTime(2016, 5, 1) } }); } } private List<Menu> menus = null; public IEnumerable<Menu> Menus { get { return menus ?? (menus = new List<Menu>() { new Menu { Id=1, Text="Baby Back Barbecue Ribs", Price=16.9, Category="Main" }, new Menu { Id=2, Text="Chicken and Brown Rice Piaf", Price=12.9, Category="Main" }, new Menu { Id=3, Text="Chicken Miso Soup with Shiitake Mushrooms", Price=6.9, Category="Soup" } }); } } }
上下文類已注冊到依賴注入啟動代碼,以使用控制器構造函數注入類型(代碼文件MVCSampleApp/Startup.cs):
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddScoped<EventsAndMenusContext>(); }
現在該模型將在以下代碼中用於部分視圖示例,從服務器端代碼加載的部分視圖以及使用客戶端上的JavaScript代碼請求的視圖。
使用來自服務器端代碼的部分視圖
在 ViewsDemoController 類中,構造函數被修改為注入 EventsAndMenusContext 類型(代碼文件MVCSampleApp/Controllers/ViewsDemoController.cs):
public class ViewsDemoController : Controller { private EventsAndMenusContext _context; public ViewsDemoController(EventsAndMenusContext context) { _context = context; } // etc.
UseAPartialView1動作方法將 EventsAndMenus的 一個實例傳遞給視圖(代碼文件MVCSampleApp/Controllers/ViewsDemoController.cs):
public IActionResult UseAPartialView1() => View(_context);
視圖頁面被定義為使用 EventsAndMenusContext 類型的模型。可以使用HTML Helper 的方法 Html.PartialAsync 顯示部分視圖,該方法返回 Task<HtmlString>。后面的示例代碼中,該字符串使用 Razor 語法作為 div 元素的內容寫入。 PartialAsync方法的第一個參數接受部分視圖的名稱,第二個參數可以傳遞模型。如果沒有傳遞模型,則部分視圖可以訪問與視圖相同的模型。這里視圖使用類型 EventsAndMenusContext 的模型,部分視圖只是使用類型 IEnumerable<Event>的一部分(代碼文件MVCSampleApp/Views/ViewsDemo/UseAPartialView1.cshtml):
@model MVCSampleApp.Models.EventsAndMenusContext @{ ViewBag.Title ="Use a Partial View"; ViewBag.EventsTitle ="Live Events"; } <h2>Use a Partial View</h2> <div>this is the main view</div> <div> @await Html.PartialAsync("ShowEvents", Model.Events) </div>
如果不使用異步方法,可以使用同步變量Html.Partial。這是一個返回 HtmlString 的擴展方法。
在視圖中渲染局部視圖的另一種方法是使用 HTML Helper 方法Html.RenderPartialAsync,它被定義為返回 Task 。此方法直接將部分視圖內容寫入響應流。這樣一可以在Razor代碼塊中使用 RenderPartialAsync 。
用創建正常視圖的方式去創建局部視圖即可,可以訪問模型以及通過使用ViewBag屬性訪問的字典。部分視圖接收字典的副本以接收可以使用的相同字典數據(代碼文件MVCSampleApp/Views/ViewsDemo/ShowEvents.cshtml):
@using MVCSampleApp.Models @model IEnumerable<Event> <h2> @ViewBag.EventsTitle </h2> <table> @foreach (var item in Model) { <tr> <td>@item.Day.ToShortDateString()</td> <td>@item.Text</td> </tr> } </table>
運行應用程序時,將渲染視圖,局部視圖和布局,如圖41.5所示。

圖41.5
從控制器返回部分視圖
目前為止部分視圖都是直接加載,而im有與控制器交互,但也可以使用控制器返回部分視圖。
以下代碼片段中,在 ViewsDemoController 類中定義了兩個操作方法。第一個動作方法 UsePartialView2 返回正常視圖,第二個動作方法 ShowEvents 使用基類方法PartialView返回部分視圖。局部視圖 ShowEvents 之前已經創建和使用,並在此處使用。 PartialView方法將包含事件列表的模型將傳遞到部分視圖(代碼文件 MVCSampleApp/Controllers/ViewDemoController.cs):
public ActionResult UseAPartialView2() => View(); public ActionResult ShowEvents() { ViewBag.EventsTitle ="Live Events"; return PartialView(_context.Events); }
從控制器提供部分視圖時,可以直接從客戶端代碼調用部分視圖。以下代碼片段使用jQuery:事件處理程序鏈接到按鈕的 click 事件。在事件處理程序內,使用jQuery load 函數向服務器發出GET請求,以請求 /ViewsDemo/ShowEvents 。該請求返回部分視圖,部分視圖的結果放在名為 events 的 div 元素中(代碼文件MVCSampleApp/Views/ViewsDemo/UseAPartialView2.cshtml):
@model MVCSampleApp.Models.EventsAndMenusContext @{ ViewBag.Title ="Use a Partial View"; } <script src="~/lib/jquery/dist/jquery.js"></script> <script> $(function () { $("#getEvents").click(function () { $("#events").load("/ViewsDemo/ShowEvents"); }); }); </script> <h2>Use a Partial View</h2> <div>this is the main view</div> <button id="FileName_getEvents">Get Events</button> <div id="FileName_events"> </div>
使用視圖組件
ASP.NET MVC 6 提供了一個新的替代部分視圖的方式:視圖組件。視圖組件與局部視圖非常類似,主要區別是視圖組件與控制器無關。這使得它們易於與多個控制器一起使用。視圖組件非常有用的示例是菜單的動態導航,登錄面板或博客中的側邊欄內容。這些場景獨立於單個控制器是非常有用的。
與控制器和視圖一樣,視圖組件有兩個部分。視圖組件中控制器功能由從ViewComponent(或具有ViewComponent屬性的POCO類)派生的類接管。用戶界面類似於視圖定義,但是調用視圖組件的方法不同。
以下代碼片段定義了從基類 ViewComponent 派生的視圖組件。該類使用先前在 Startup 類中注冊的 EventsAndMenusContext 類型,以便可用於依賴注入。這與構造函數注入的控制器類似。 InvokeAsync 方法定義為從顯示視圖組件的視圖中調用。該方法可以有任意數量和類型的參數,因為由 IViewComponentHelper 接口定義的方法使用 params關鍵字定義了靈活數量的參數。不要使用異步方法實現,而是同步實現此方法返回 IViewComponentResult 而不是 Task<IViewComponentResult> 。然而,通常異步方式是最好的使用,例如,用於訪問數據庫。視圖組件需要存儲在 ViewComponents 目錄中。這個目錄本身可以放置在項目中的任何地方(代碼文件MVCSampleApp/ViewComponents/EventListViewComponent.cs):
public class EventListViewComponent : ViewComponent { private readonly EventsAndMenusContext _context; public EventListViewComponent(EventsAndMenusContext context) { _context = context; } public Task<IViewComponentResult> InvokeAsync(DateTime from, DateTime to) { return Task.FromResult<IViewComponentResult>( View(EventsByDateRange(from, to))); } private IEnumerable<Event> EventsByDateRange(DateTime from, DateTime to) { return _context.Events.Where(e => e.Day >= from && e.Day <= to); } }
視圖組件的用戶界面在以下代碼段中定義。可以使用項目模板 MVC View Page 創建視圖組件的視圖,它使用相同的 Razor 語法。具體來說,它必須放在 Components/[viewcomponent] 文件夾中,例如 Components/EventList 。要使視圖組件可用於所有控件,需要在視圖的 Shared 文件夾中創建Components文件夾。僅使用來自一個特定控制器的視圖組件時,可以將其放入視圖控制器文件夾中。這個視圖與眾不同的地方是它需要被命名為default.cshtml。也可以創建其他視圖名稱,需要使用從InvokeAsync方法返回的View方法的參數指定這些視圖(代碼文件MVCSampleApp/Views/Shared/Components/EventList/default.cshtml):
@using MVCSampleApp.Models; @model IEnumerable<Event> <h3>Formula 1 Calendar</h3> <ul> @foreach (var ev in Model) { <li><div>@ev.Day.ToString("D")</div><div>@ev.Text</div></li> } </ul>
視圖組件完成后,可以通過調用InvokeAsync方法顯示它。Component是視圖動態創建的屬性,返回實現IViewComponentHelper的對象。 IviewComponentHelper允許您調用同步或異步方法,如Invoke,InvokeAsync,RenderInvoke和RenderInvokeAsync。當然,只能調用由視圖組件實現的這些方法,並且要使用相應的參數(代碼文件MVCSampleApp/Views/ViewsDemo/UseViewComponent.cshtml):
@{ ViewBag.Title ="View Components Sample"; } <h2>@ViewBag.Title</h2> <p> @await Component.InvokeAsync("EventList", new DateTime(2016, 4, 10), new DateTime(2016, 4, 24)) </p>
運行應用程序,您可以看到呈現的視圖組件,如圖41.6所示。

圖41.6
在視圖中使用依賴注入
如果直接從視圖中需要服務,可以使用 inject 關鍵字注入它:
@using MVCSampleApp.Services @inject ISampleService sampleService <p> @string.Join("*", sampleService.GetSampleStrings()) </p>
要這樣做時,最好使用AddScoped方法注冊服務。如上所述,以這種方式注冊服務意味着它只對一個HTTP請求實例化一次。使用AddScoped,在控制器和視圖中注入相同的服務,它只為請求實例化一次。
導入多個視圖的命名空間
所有以前的視圖示例都使用了using關鍵字打開所有需要的命名空間。其實可以使用Visual Studio項目模板 MVC View Imports Page 來創建文件(_ViewImports.cshml)定義所有使用聲明,而不是在每個視圖打開命名空間(代碼文件MVCSampleApp/Views/_ViewImports.cshtml):
@using MVCSampleApp.Models
@using MVCSampleApp.Services
有了這個文件,不需要在所有視圖中都添加 using 關鍵字。
---------------------(未完待續)
