官方教學網址: https://docs.microsoft.com/en-us/aspnet/core/tutorials/first-mvc-app/start-mvc
環境: Windows 10, .NET 6.0.301,使用 VScode 開發
> 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
新增 Views/HelloWorld/Index.cshtml
@{
ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>Hello from view template!</p>
新增 Controllers/HelloWorldController.cs
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源码分析之如何定位控制器。
關於 .NET MVC 如何找到對應的 View 可以參考 asp.net mvc 源码分析 - ActionResult 篇 FindView。
改寫 Views/HelloWorld/Index.cshtml
@{
ViewData["Title"] = "Index";
}
<ul>
@for (int i = 0; i < (int)ViewData["NumTimes"]!; i++) {
<li>@ViewData["Message"]</li>
}
</ul>
改寫 Controllers/HelloWorldController.cs
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
新增 Models/Movie.cs
,定義每個電影需要包含哪些資訊。
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
,用來與資料庫溝通。
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
中加入資料庫連線的字串
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
+ "AllowedHosts": "*",
+ "ConnectionStrings": {
+ "MvcMovieContext": "Data Source=.db"
+ }
}
在 Program.cs
中註冊 MvcMovieContext
,每當有 Controller 需要使用 MvcMovieContext
就會經由 Dependency Injection 得到一個實例。透過 GetConnectionString
從 appsettings.json
讀取連線字串。
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 串接的程式碼,但是我們還沒初始化資料庫、放入範例資料。
我對 Entity Framework 了解很粗淺,所以這邊還是按照官方教學,透過 dotnet ef migrations
自動產生初始化資料庫的程式碼。
> dotnet ef migrations add InitialCreate
執行指令後,會產生 Migrations
目錄,其中的 InitialCreate
是自定義類別名稱,裡面的 Up
方法會建立一個新的 Schema。
> dotnet ef database update
執行前面所述的 Up
方法將初始資料移轉到 SQLite 中,此時專案目錄出現一個 .db
檔案,裡面有名為 Movie
的 Table。
新增 Controller/MoviesController.cs
,定義每個電影需要包含哪些資訊。
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
,展示資料庫中的所有電影。
@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。
現在連線到 localhost:port/Movies
就換看到展示電影的頁面了,但是因為資料庫目前是空的,所以是一片空白,Create New 頁面也還沒寫,所以點進去會 404 ERROR。
新增 Models/SeedData.cs
,這個類別會提供初始資料。
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 資料庫中。
// ...
var app = builder.Build();
using (var scope = app.Services.CreateScope()) {
var services = scope.ServiceProvider;
SeedData.Initialize(services);
}
// ...
現在連線到 localhost:port/Movies
,成功列出電影資料了。
修改 Controller/MoviesController.cs
,增加 GET 的 Create
Action 和 POST 的 Create
Action。
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
。
@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
,會出現新增電影的頁面。