官方教學網址: https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/start-mvc 環境: Windows 10, .NET 6.0.301,使用 VScode 開發 ## Step 0: 初始化專案 ``` > dotnet new mvc -o MvcMovie > cd MvcMovie > dotnet dev-certs https --trust > dotnet tool install --global dotnet-ef ``` - `dotnet new mvc -o MvcMovie`: 創建一個名為 MvcMovie 的專案。 - `dotnet dev-certs https --trust`: 啟用 https 自簽憑證。 - `dotnet tool install --global dotnet-ef`: 下載 Entity Framework 工具,用來初始化、移轉資料庫。 ``` > dotnet add package Microsoft.EntityFrameworkCore.SQLite > dotnet add package Microsoft.EntityFrameworkCore.Design > dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design ``` - 下載相容於 Entity Framework 的 SQLite 套件 - 下載 Entity Framework 設計工具 - CRUD 程式碼產生工具 ## Step 1: 加入 View 與 Controller 新增 `Views/HelloWorld/Index.cshtml` ```csharp @{ ViewData["Title"] = "Index"; } <h2>Index</h2> <p>Hello from view template!</p> ``` 新增 `Controllers/HelloWorldController.cs` ```csharp using Microsoft.AspNetCore.Mvc; namespace MvcMovie.Controllers; public class HelloWorldController : Controller { public IActionResult Index() { return View(); } public string Welcome() { return "this is the welcome action method"; } } ``` 此時連線到 `localhost:port/HelloWorld` 或 `localhost:port/HelloWorld/Welcome` 就會看到自己創建的頁面。 ![](https://i.imgur.com/l4yAFZo.png) 關於 .NET MVC 如何定位 Controller 可以參考[ASP.NET MVC 4源码分析之如何定位控制器](https://www.cnblogs.com/yaozhenfa/p/asp_net_mvc_source_code_how_to_search_controller.html)。 關於 .NET MVC 如何找到對應的 View 可以參考 [asp.net mvc 源码分析 - ActionResult 篇 FindView](https://www.cnblogs.com/majiang/archive/2012/11/11/2764976.html)。 ## Step 2: 透過 ViewData 把資料從 Controller 傳到 View 中 改寫 `Views/HelloWorld/Index.cshtml` ```csharp @{ ViewData["Title"] = "Index"; } <ul> @for (int i = 0; i < (int)ViewData["NumTimes"]!; i++) { <li>@ViewData["Message"]</li> } </ul> ``` 改寫 `Controllers/HelloWorldController.cs` ```csharp using Microsoft.AspNetCore.Mvc; namespace MvcMovie.Controllers; public class HelloWorldController : Controller { public IActionResult Index(string name, int numTimes) { ViewData["Message"] = "Hello " + name; ViewData["NumTimes"] = numTimes; return View(); } public string Welcome() { return "this is the welcome action method"; } } ``` 此時連線到 `localhost:port/HelloWorld?name=Rick&numtimes=4` ![](https://i.imgur.com/veQZNZ5.png) ## Step 3: 加入 Movie、透過 SQLite 儲存資料、展示所有 Movie ### 定義 Movie 類別、串接 Entity Framework 新增 `Models/Movie.cs`,定義每個電影需要包含哪些資訊。 ```csharp using System.ComponentModel.DataAnnotations; namespace MvcMovie.Models 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; } } ``` 接下來官方教學會使用 `dotnet-aspnet-codegenerator`,根據 `Movie.cs` 自動產生 `MovieController.cs`、`MvcMovieContext.cs` 和 View 來做出對應的 CRUD 頁面,為了理解程式碼的邏輯,我這次不使用 code generator,而是手動加入這些檔案。 首先新增 `Data/MvcMovieContext.cs`,`MvcMovieContext` 繼承 `DbContext`,用來與資料庫溝通。 ```csharp using Microsoft.EntityFrameworkCore; using MvcMovie.Models; public class MvcMovieContext : DbContext { public MvcMovieContext (DbContextOptions<MvcMovieContext> options) : base(options) { } public DbSet<Movie> Movie { get; set; } = default!; } ``` 在 `appsettings.json` 中加入資料庫連線的字串 ```diff { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning" } }, + "AllowedHosts": "*", + "ConnectionStrings": { + "MvcMovieContext": "Data Source=.db" + } } ``` 在 `Program.cs` 中註冊 `MvcMovieContext`,每當有 Controller 需要使用 `MvcMovieContext` 就會經由 Dependency Injection 得到一個實例。透過 `GetConnectionString` 從 `appsettings.json` 讀取連線字串。 ```csharp using Microsoft.EntityFrameworkCore; using MvcMovie.Models; var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext<MvcMovieContext>(options => { options.UseSqlite( builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.") ); }); // ... ``` 如此一來便完成與 SQLite 串接的程式碼,但是我們還沒初始化資料庫、放入範例資料。 ### 初始化 SQLite 資料庫 我對 Entity Framework 了解很粗淺,所以這邊還是按照官方教學,透過 `dotnet ef migrations` 自動產生初始化資料庫的程式碼。 ``` > dotnet ef migrations add InitialCreate ``` 執行指令後,會產生 `Migrations` 目錄,其中的 `InitialCreate` 是自定義類別名稱,裡面的 `Up` 方法會建立一個新的 Schema。 ``` > dotnet ef database update ``` 執行前面所述的 `Up` 方法將初始資料移轉到 SQLite 中,此時專案目錄出現一個 `.db` 檔案,裡面有名為 `Movie` 的 Table。 ### 新增 Movie 的 Controller 和 View 新增 `Controller/MoviesController.cs`,定義每個電影需要包含哪些資訊。 ```csharp using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace MvcMovie.Controllers; public class MoviesController : Controller { private readonly MvcMovieContext _context; public MoviesController(MvcMovieContext context) { _context = context; } public async Task<IActionResult> Index() { return _context.Movie != null ? View(await _context.Movie.ToListAsync()) : Problem("Entity set 'MvcMovieContext.Movie' is null."); } } ``` 由上方程式碼可以看到 `_context` 會被注入 `MvcMovieContext` 的實例。`Index()` 則會將 `_context.Movie` 的參考傳入 View 中。 接下來新增 `Views/Movies/Index.cshtml`,展示資料庫中的所有電影。 ```csharp @model IEnumerable<MvcMovie.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> ``` 觀察上方程式碼,`@model` directive 指定 View 所期望被傳入的物件類別,此範例所指定的為 `IEnumerable<MvcMovie.Models.Movie>`,這個 View 所接收的物件為 `_context.Movie.ToListAsync()`。 `@Html` 是 HTML Helper,`@Html.DisplayNameFor` 和 `@Html.DisplayFor` 會渲染和 Lambda 回傳的類別匹配的 DisplayTemplate,如果類別沒有預設的 DisplayTemplate,則會改為渲染該類別的 `.ToString()`。 `asp-action`、`asp-route-id` 是 Anchor Tag Helper,用途是指定 `href` 的 url,詳見 [Anchor Tag Helper in ASP.NET Core](https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/built-in/anchor-tag-helper?view=aspnetcore-6.0)。 現在連線到 `localhost:port/Movies` 就換看到展示電影的頁面了,但是因為資料庫目前是空的,所以是一片空白,Create New 頁面也還沒寫,所以點進去會 404 ERROR。 ![](https://i.imgur.com/10A3k9m.png) ### 新增種子資料 新增 `Models/SeedData.cs`,這個類別會提供初始資料。 ```csharp using Microsoft.EntityFrameworkCore; namespace MvcMovie.Models; public static class SeedData { 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(); } } } ``` 修改 `Program.cs`,把初始資料寫入 SQLite 資料庫中。 ```csharp // ... var app = builder.Build(); using (var scope = app.Services.CreateScope()) { var services = scope.ServiceProvider; SeedData.Initialize(services); } // ... ``` 現在連線到 `localhost:port/Movies`,成功列出電影資料了。 ![](https://i.imgur.com/YdU2ZHw.png) ## Step 4: 新增 CRUD 的頁面 ### Create 修改 `Controller/MoviesController.cs`,增加 GET 的 `Create` Action 和 POST 的 `Create` Action。 ```csharp public class MoviesController : Controller { // ... // GET: Movies/Create public IActionResult Create() { return View(); } // POST: Movies/Create // To protect from overposting attacks, enable the specific properties you want to bind to. // For more details, see http://go.microsoft.com/fwlink/?LinkId=317598. [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create([Bind("Id,Title,ReleaseDate,Genre,Price")] Movie movie) { if (ModelState.IsValid) { _context.Add(movie); await _context.SaveChangesAsync(); return RedirectToAction(nameof(Index)); } return View(movie); } } ``` - `Bind` 屬性會新增一個 `Movie` 的實例,將 POST 的表單的值放入對應的 `Movie` 的屬性,然後這個 `movie` 會作為參數被傳入 `Create` 函式。 新增 `Views/Movies/Create.cshtml`。 ```csharp @model MvcMovie.Models.Movie @{ ViewData["Title"] = "Create"; } <h1>Create</h1> <h4>Movie</h4> <hr /> <div class="row"> <div class="col-md-4"> <form asp-action="Create"> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <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="Create" class="btn btn-primary" /> </div> </form> </div> </div> <div> <a asp-action="Index">Back to List</a> </div> @section Scripts { @{await Html.RenderPartialAsync("_ValidationScriptsPartial");} } ``` - `asp-for` 在 `label` 中會轉為 `for` 與 `label` 中間的文字,例如 `<label asp-for="Title">` 會被轉換為 `<label for="Title">Title</label>`。 - `asp-for` 在 `<input>` 中為 Input Tag Helper,會將 `asp-for` 中的表達式轉為 `id` 和 `name`,例如 `<input asp-for="Title" class="form-control" />` 會被轉換成 `<input class="form-control" id="Title" name="Title">`。 - `asp-validation-for` 會被轉換為 `data-valmsg-for` 和 `data-valmsg-replace`,在瀏覽器中透過 jQuery 檢查使用者輸入是否有問題。例如 `<span asp-validation-for="Title"></span>` 會變成 `<span class="field-validation-valid" data-valmsg-for="Title" data-valmsg-replace="true"></span>` 現在連線到 `localhost:port/Movies/Create`,會出現新增電影的頁面。 ![](https://i.imgur.com/mQjumN3.png)