Try   HackMD

官方教學網址: 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

@{
    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/HelloWorldlocalhost:port/HelloWorld/Welcome 就會看到自己創建的頁面。

關於 .NET MVC 如何定位 Controller 可以參考ASP.NET MVC 4源码分析之如何定位控制器

關於 .NET MVC 如何找到對應的 View 可以參考 asp.net mvc 源码分析 - ActionResult 篇 FindView

Step 2: 透過 ViewData 把資料從 Controller 傳到 View 中

改寫 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

Step 3: 加入 Movie、透過 SQLite 儲存資料、展示所有 Movie

定義 Movie 類別、串接 Entity Framework

新增 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.csMvcMovieContext.cs 和 View 來做出對應的 CRUD 頁面,為了理解程式碼的邏輯,我這次不使用 code generator,而是手動加入這些檔案。

首先新增 Data/MvcMovieContext.csMvcMovieContext 繼承 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 得到一個實例。透過 GetConnectionStringappsettings.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 串接的程式碼,但是我們還沒初始化資料庫、放入範例資料。

初始化 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,定義每個電影需要包含哪些資訊。

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-actionasp-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,成功列出電影資料了。

Step 4: 新增 CRUD 的頁面

Create

修改 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-forlabel 中會轉為 forlabel 中間的文字,例如 <label asp-for="Title"> 會被轉換為 <label for="Title">Title</label>
  • asp-for<input> 中為 Input Tag Helper,會將 asp-for 中的表達式轉為 idname,例如 <input asp-for="Title" class="form-control" /> 會被轉換成 <input class="form-control" id="Title" name="Title">
  • asp-validation-for 會被轉換為 data-valmsg-fordata-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,會出現新增電影的頁面。