一. MVC介紹 MVC架構模式有助於實現關註點分離。視圖和控制器均依賴於模型。 但是,模型既不依賴於視圖,也不依賴於控制器。 這是分離的一個關鍵優勢。 這種分離允許模型獨立於可視化展示進行構建和測試。ASP.NET Core MVC 包括以下功能: 路由、模型綁定、模型驗證、依賴關係註入、篩選器、 ...
一. MVC介紹
MVC架構模式有助於實現關註點分離。視圖和控制器均依賴於模型。 但是,模型既不依賴於視圖,也不依賴於控制器。 這是分離的一個關鍵優勢。 這種分離允許模型獨立於可視化展示進行構建和測試。ASP.NET Core MVC 包括以下功能:
路由、模型綁定、模型驗證、依賴關係註入、篩選器、區域、Web API、可測試性、Razor 視圖引擎、強類型視圖、標記幫助程式、 視圖組件。
(1) 路由
ASP.NET Core MVC 建立在 ASP.NET Core 的路由之上,是一個功能強大的 URL 映射組件,可用於生成具有易於理解和可搜索 URL 的應用程式。關於路由知識,請查看asp.net core 系列第5,6章。
(2) 模型綁定(Model)
ASP.NET Core MVC 模型綁定將客戶端請求數據(窗體值(form)、路由數據、查詢字元串參數、HTTP 頭)轉換到控制器(Controller)可以處理的對象中。 因此,控制器邏輯不必找出傳入的請求數據;它只需具備作為其Action方法的參數的數據。下麵的LoginViewModel就是一個模型類。
public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null)
(3) 模型驗證
ASP.NET Core MVC 通過使用數據註釋驗證屬性。 驗證屬性在值發送到服務端前,在客戶端上進行檢查。併在調用控制器action前在服務端上進行檢查。
using System.ComponentModel.DataAnnotations; public class LoginViewModel { [Required] [EmailAddress] public string Email { get; set; } [Required] [DataType(DataType.Password)] public string Password { get; set; } [Display(Name = "Remember me?")] public bool RememberMe { get; set; } } //服務端控制器action驗證 public async Task<IActionResult> Login(LoginViewModel model, string returnUrl = null) { //驗證模型 if (ModelState.IsValid) { // work with the model } return View(model); }
(4) 依賴註入
依賴關係註入除了在控制器上通過構造函數請求所需服務,還可以使用@inject 指令,應用在視圖文件上。下麵是視圖頁面上通過依賴註入獲取服務對象。
@inject SomeService ServiceName <!DOCTYPE html> <html lang="en"> <head> <title>@ServiceName.GetTitle</title> </head> <body> <h1>@ServiceName.GetTitle</h1> </body> </html>
(5) 篩選器
篩選器幫助開發者封裝,橫切關註點,例如異常處理或授權。篩選器允許action方法運行自定義預處理和後處理邏輯,並且可以配置為在給定請求的執行管道內的特定點上運行。篩選器可以作為屬性應用於控制器或Action(也可以全局運行)。例如MVC 授權篩選器。
[Authorize] public class AccountController : Controller
(6) 區域
區域用在大型Web開發上, 是功能分組的方法。區域是應用程式內的一個 MVC 結構。 例如,具有多個業務單位(如結賬、計費、搜索等)的電子商務應用。每個單位都有自己的邏輯組件視圖、控制器和模型。
(7) Web API
除了作為生成網站的強大平臺,ASP.NET Core MVC 還對生成 Web API 提供強大的支持。 可以生成可連接大量客戶端(包括瀏覽器和移動設備)的服務,前面章節有講過。
(8) 可測試性
框架對界面和依賴項註入的使用非常適用於單元測試,並且該框架還包括使得集成測試快速輕鬆的功能(例如 TestHost 和實體框架的 InMemory 提供程式)
(9) Razor 視圖引擎
ASP.NET Core MVC 視圖使用 Razor 視圖引擎呈現視圖。 Razor 是一種緊湊、富有表現力且流暢的模板標記語言,用於使用嵌入式 C# 代碼定義視圖。 Razor 用於在伺服器上動態生成 Web 內容。 可以完全混合伺服器代碼與客戶端內容和代碼。例如下麵嵌入 C#代碼,迴圈輸出5組li標記
<ul> @for (int i = 0; i < 5; i++) { <li>List item @i</li> } </ul>
(10) 強類型視圖
可以基於模型強類型化 MVC 中的 Razor 視圖。 控制器可以將強類型化的模型傳遞給視圖,使視圖具備類型檢查和 IntelliSense 支持。例如,以下視圖呈現類型為 IEnumerable<Product>
的模型:
@model IEnumerable<Product> <ul> @foreach (Product p in Model) { <li>@p.Name</li> } </ul>
(11) 標記幫助程式
標記幫助程式使伺服器端代碼可以在 Razor 文件中參與創建和呈現 HTML 元素。 例如,內置 LinkTagHelper 可以用來創建指向 AccountsController
控制器中
Login
的方法鏈接
<p> Thank you for confirming your email. Please <a asp-controller="Account" asp-action="Login">Click here to Log in</a>. </p>
(12) 視圖組件
通過視圖組件可以包裝呈現邏輯併在整個應用程式中重用它。 這些組件類似於分部視圖,但具有關聯邏輯。
二. 完整示例介紹(項目StudyMVCDemo)
2.1 安裝EF數據提供程式
這裡使用記憶體資料庫Microsoft.EntityFrameworkCore.InMemory,Entity Framework Core 和記憶體資料庫一起使用, 這對測試非常有用。
PM> Install-Package Microsoft.EntityFrameworkCore.InMemory
2.2 新建數據模型類(POCO )和EF上下文類
public class MvcMovieContext : DbContext { public MvcMovieContext(DbContextOptions options) : base(options) { } public DbSet<Movie> Movie { get; set; } }
public class Movie { public int Id { get; set; } public string Title { get; set; } [DataType(DataType.Date)] public DateTime ReleaseDate { get; set; } public string Genre { get; set; } public decimal Price { get; set; } }
2.3 初始化數據
public static void Main(string[] args) { var host = CreateWebHostBuilder(args).Build(); using (var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; try { //var context = services.GetRequiredService<MvcMovieContext>(); //程式運行時,使用EF遷移生成數據,用在關係型資料庫 //context.Database.Migrate();
SeedData.Initialize(services); } catch (Exception ex) { var logger = services.GetRequiredService<ILogger<Program>>(); logger.LogError(ex, "An error occurred seeding the DB."); } } host.Run(); }
public static class SeedData { /// <summary> /// 初始化數據 /// </summary> /// <param name="serviceProvider"></param> public static void Initialize(IServiceProvider serviceProvider) { using (var context = new MvcMovieContext( serviceProvider.GetRequiredService<DbContextOptions<MvcMovieContext>>())) { // 如果有數據返回 if (context.Movie.Any()) { return; // DB has been seeded } context.Movie.AddRange( new Movie { Title = "When Harry Met Sally", ReleaseDate = DateTime.Parse("1989-2-12"), Genre = "Romantic Comedy", Price = 7.99M }, new Movie { Title = "Ghostbusters ", ReleaseDate = DateTime.Parse("1984-3-13"), Genre = "Comedy", Price = 8.99M }, new Movie { Title = "Ghostbusters 2", ReleaseDate = DateTime.Parse("1986-2-23"), Genre = "Comedy", Price = 9.99M }, new Movie { Title = "Rio Bravo", ReleaseDate = DateTime.Parse("1959-4-15"), Genre = "Western", Price = 3.99M } ); context.SaveChanges(); } } }View Code
2.4 添加控制器類(MoviesController)
public class MoviesController : Controller { private readonly MvcMovieContext _MvcMovieContext; public MoviesController(MvcMovieContext MvcMovieContext) { this._MvcMovieContext = MvcMovieContext; } }
2.5 列表頁Movies/index.cshtml
// GET: /<controller>/ public IActionResult Index() { var movies = _MvcMovieContext.Movie.ToList(); return View(movies); }
@model IEnumerable<StudyMVCDemo.Models.Movie> @{ ViewData["Title"] = "Index"; } <h1>Index</h1> <p> <a asp-action="Create">Create New</a> </p> <table class="table"> <thead> <tr> <th> @Html.DisplayNameFor(model => model.Title) </th> <th> @Html.DisplayNameFor(model => model.ReleaseDate) </th> <th> @Html.DisplayNameFor(model => model.Genre) </th> <th> @Html.DisplayNameFor(model => model.Price) </th> <th></th> </tr> </thead> <tbody> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Title) </td> <td> @Html.DisplayFor(modelItem => item.ReleaseDate) </td> <td> @Html.DisplayFor(modelItem => item.Genre) </td> <td> @Html.DisplayFor(modelItem => item.Price) </td> <td> <a asp-action="Edit" asp-route-id="@item.Id">Edit</a> | <a asp-action="Details" asp-route-id="@item.Id">Details</a> | <a asp-action="Delete" asp-route-id="@item.Id">Delete</a> </td> </tr> } </tbody> </table>
啟動程式,在瀏覽器中輸入http://localhost:18084/Movies,如下圖所示:
上圖中菜單佈局是在 Views/Shared/_Layout.cshtml 文件中實現的,該_Layout.cshtml頁中@RenderBody()是視圖頁面的占位符。
Views/_ViewStart.cshtml 文件將 Views/Shared/_Layout.cshtml 文件引入到每個視圖中。 可以使用 Layout
屬性設置不同的佈局視圖,或將它設置為 null
,這樣將不會使用任何佈局文件。後面詳細瞭解佈局。
2.6 詳細頁Movies/ Details.cshtml
/// <summary> /// 詳細頁 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<IActionResult> Details(int? id) { if (id == null) { return NotFound(); } var movie = await _MvcMovieContext.Movie .FirstOrDefaultAsync(m => m.Id == id); if (movie == null) { return NotFound(); } return View(movie); }
@model StudyMVCDemo.Models.Movie @{ ViewData["Title"] = "Details"; } <h1>Details</h1> <div> <h4>Movie</h4> <hr /> <dl class="row"> <dt class="col-sm-2"> @Html.DisplayNameFor(model => model.Title) </dt> <dd class="col-sm-10"> @Html.DisplayFor(model => model.Title) </dd> <dt class="col-sm-2"> @Html.DisplayNameFor(model => model.ReleaseDate) </dt> <dd class="col-sm-10"> @Html.DisplayFor(model => model.ReleaseDate) </dd> <dt class="col-sm-2"> @Html.DisplayNameFor(model => model.Genre) </dt> <dd class="col-sm-10"> @Html.DisplayFor(model => model.Genre) </dd> <dt class="col-sm-2"> @Html.DisplayNameFor(model => model.Price) </dt> <dd class="col-sm-10"> @Html.DisplayFor(model => model.Price) </dd> </dl> </div> <div> <a asp-action="Edit" asp-route-id="@Model.Id">Edit</a> | <a asp-action="Index">Back to List</a> </div>
啟動程式,從列表頁的超連接Details點擊進入,如下圖所示:
2.7 編輯頁Movies/ Edit.cshtml
對於編輯頁有二個action, 一個是Get用來提取數據填充到表單,一個是Post用來提交修改的表單數據。
(1) post中的Bind特性是對需要的屬性進行更新。
(2) ValidateAntiForgeryToken特性用於防止請求偽造, 生成的隱藏的 XSRF 標記 Input name="__RequestVerificationToken"。用在Post提交的比如修改和刪除功能等。
(3) 模型驗證asp-validation-for是指表單Post到伺服器之前,客戶端驗證會檢查欄位上的任何驗證規則。 如果有任何驗證錯誤,則將顯示錯誤消息,並且不會Post表單,內部是輸入標記幫助程式使用 DataAnnotations 特性,併在客戶端上生成 jQuery 驗證所需的 HTML 特性。
public async Task<IActionResult> Edit(int? id) { if (id == null) { return NotFound(); } var movie = await _MvcMovieContext.Movie.FindAsync(id); if (movie == null) { return NotFound(); } return View(movie); } [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price")] Movie movie) { if (id != movie.Id) { return NotFound(); } if (ModelState.IsValid) { try { _MvcMovieContext.Update(movie); await _MvcMovieContext.SaveChangesAsync(); } catch (DbUpdateConcurrencyException) { throw; } return RedirectToAction("Index"); } return View(movie); }
@model StudyMVCDemo.Models.Movie @{ ViewData["Title"] = "Edit"; } <h1>Edit</h1> <h4>Movie</h4> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Edit"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <input type="hidden" asp-for="Id" /> <div class="form-group"> <label asp-for="Title" class="control-label"></label> <input asp-for="Title" class="form-control" /> <span asp-validation-for="Title" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="ReleaseDate" class="control-label"></label> <input asp-for="ReleaseDate" class="form-control" /> <span asp-validation-for="ReleaseDate" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Genre" class="control-label"></label> <input asp-for="Genre" class="form-control" /> <span asp-validation-for="Genre" class="text-danger"></span> </div> <div class="form-group"> <label asp-for="Price" class="control-label"></label> <input asp-for="Price" class="form-control" /> <span asp-validation-for="Price" class="text-danger"></span> </div> <div class="form-group"> <input type="submit" value="Save" class="btn btn-primary" /> </div> </form> </div> </div> <div> <a asp-action="Index">Back to List</a> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} }
啟動程式,從列表頁的Edit點擊進入,如下圖所示:
2.8 刪除
// 刪除沒有對應的頁面,從列表頁的Delete點擊進入,下麵是刪除的關鍵代碼 public async Task<IActionResult> DeleteConfirmed(int id) { var movie = await _context.Movie.FindAsync(id); _context.Movie.Remove(movie); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); }
參考文獻