官方教學網址: 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` 就會看到自己創建的頁面。

關於 .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`

## 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。

### 新增種子資料
新增 `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`,成功列出電影資料了。

## 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`,會出現新增電影的頁面。
