這篇是參考官方文件-ASP.NET Core MVC 使用者入門來做示範
文章內容重新編排過,去除多餘的部分只留下重點
這內容算是在進入ASP.NET Core MVC之前一個前導教學,如果覺得講的還不錯,可以期待一下我接下來的教學課程~當然是免費的
1.開始使用
首先當然要先安裝Visual Studio 2022,不過這邊就不示範安裝過程了
接著打開Visual Studio 2022,點選建立新的專案,最近這個選項畫面改版好多次,不知道接下來會不又會改

搜尋asp.net core mvc,並選擇ASP.NET Core Web應用程式(Medel-View-Controller),C#的這一個

專案名稱打上MvcMovie,並選擇下一步

接著預設選擇.NET 6.0,不用更改,按下建立

這是初始化面

接著我們按下啟動不偵錯,執行應用程式

出現這個畫面代表你成功的執行了我們新建的MvcMovie專案了

2.新增控制器
先看到右側目錄

這邊有三個資料夾即為最根本的MVC架構
- 模型 (M):代表應用程式資料的類別。 模型類別使用驗證邏輯對該資料強制執行商務規則。 通常,模型物件會在資料庫中擷取並儲存模型狀態。 在本教學課程中,
Movie模型會從資料庫擷取電影資料,將其提供給檢視或更新它。 更新的資料會寫入資料庫。 - 檢視 (V):檢視是顯示應用程式之使用者介面 (UI) 的元件。 一般而言,此 UI 會顯示模型資料。
 - Controllers:類別:
- 處理瀏覽器要求。
 - 擷取模型資料。
 - 呼叫傳迴響應的檢視範本。
 
 
以上是節錄官方文件,但我相信初學者應該是有看沒有懂,沒關係我們就略過,直接看實際運作
接下來我們在Controllers目錄下新增一個控制器

選擇空白後加入

這邊取名為HelloWorldController後按下新增,那通常我們會用類型當檔案結尾名稱,因為這是控制器所以結尾會加個Controller

原始程式長這樣
using Microsoft.AspNetCore.Mvc;
namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}接著我們改一下裡面的程式碼
namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        // 
        // GET: /HelloWorld/
        public string Index()
        {
            return "This is my default action...";
        }
        // 
        // GET: /HelloWorld/Welcome/ 
        public string Welcome()
        {
            return "This is the Welcome action method...";
        }
    }
}這時其實我們已經件好了兩個網頁頁面/HelloWorld/和/HelloWorld/Welcome/
我們可以打開瀏覽器輸入上面兩個網址看看


會發現這兩頁就是我們剛剛在HelloWorldController.cs裡面輸入的字串
那這是MVC專案的一個路由基本架構,相關設定在Program.cs裡的下面這段,這邊當然可以依自己的需求修改
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");那預設的程式碼是什麼意思呢?
就是在網址空白時,會去讀取HomeController裡的Index所以會有我們一開始看到的畫面
那我們的HelloWorldController內容要如何讀取呢,就是依照上面的規則打入網址即可
第一個地方是擺Controller名稱,所以是HelloWorld
第二個地方是擺action名稱,也就是我們寫在HelloWorldController裡面方法,Index和Welcome
那如果action不打,預設會去讀Index
所以/HelloWorld/等於/HelloWorld/Index/,另外一個就是/HelloWorld/Welcome/,共計新增三個網址可找到網頁
接著我們示範要如何取得使用者傳來的資料,並把他再度show到畫面上
我們修改一下Welcome這個內容
public string Welcome(string name, int numTimes = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}");
}這邊numTimes = 1是代表如果沒收到值的話就會自動帶1
那接著會接收到兩個值name和numTimes,然後我們在return確認我們真的有收到
那使用者這邊該如何傳送值呢?就是使用GET傳值方式即可
那GET傳值方式怎麼傳呢?就是在網址後面加上?name=Rick&numtimes=4
整個網址會是https://localhost:7079/HelloWorld/Welcome?name=Rick&numtimes=4

那輸入後,畫面上就會顯示我們傳進去的Rick跟4的值了,代表有成功接收到
那最後再來講講我們路由設定最後一個id的作用,其實也是用來接收值的
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");//這邊這個id那加上?是代表不一定會需要這個值,那如何接收呢?
public string Welcome(string name, int numTimes = 1, int id = 1)
{
    return HtmlEncoder.Default.Encode($"Hello {name}, NumTimes is: {numTimes}, id is: {id}");
}網址的話改輸入:https://localhost:7079/HelloWorld/Welcome/3?name=Rick&numtimes=4

這樣我們就完成了MVC專案基本的新建頁面跟傳接收值的方法了
3.新增檢視
那我們之前是在controller直接回傳字串,那在網頁上就看到相對的字
但實際上我們網站不可能只有單純的字,而是要有豐富的畫面,這時候就需要用html來編輯
所以我們改一下HelloWorldController程式碼
public IActionResult Index()
{
    return View();
}接著在View下建立一個HelloWorld的資料夾

接著在資料夾按右鍵新增項目

新增一個Razor 檢視 - 空白,名字預設為Index.cshtml

這時候我們先編輯該檔案裡面的內容
@{
    ViewData["Title"] = "Index";
}
<h2>Index</h2>
<p>Hello from our View Template!</p>存檔後再到https://localhost:7079/HelloWorld,就會發現裡面是剛剛編輯後的內容了

那其實這邊可以說是剛開始MVC架構最難的地方,因為一下有Controller檔,一下有View檔,兩者之間關係跟運作常讓初學者搞不懂作用
不過在ASP.NET Core中其實已經變得很簡單了,很多設定都幫你設好的,你只要記著寫法規則即可
這邊我們就先來順一下執行流程
首先使用者輸入了"https://localhost:7079/HelloWorld"網址
依照路由設定Controller為HelloWorld,那因為沒有Action所以是找預設的Index
接著我們會進到HelloWorldController.cs找Index方法
那在Index方法中最後回傳了一個View(),所以會去找Views資料夾下的HelloWorld資料夾(跟Controller同名)
接著在HelloWorld資料夾下找Index.cshtml檔(跟Action同名),最後將裡面的內容給秀在網頁上
相當不直覺繁瑣的流程,需要你知道規則後才能往下做,往往這邊就讓初學者卻步,但其實熟了之後也沒什麼大不了的
接著,應該大家都會注意到了,網頁的內容其實還有網址列跟結尾區塊
但我Index.cshtml裡並沒有寫這些東西啊,那這些是寫在哪裡呢
我們可以開啟Views/Shared/_Layout.cshtml檔,就會發現,外框的html都寫在這裡
最上面有個@ViewData["Title"]
<title>@ViewData["Title"] - Movie App</title>這個值得來源會是我們剛剛Views/HelloWorld/Index.cshtml中ViewData["Title"] = “Index”所設定的值,也就是Index
我們試著改一下數值為Movie List
@{
    ViewData["Title"] = "Movie List";
}
<h2>Index</h2>
<p>Hello from our View Template!</p>再看到瀏覽器的標頭就會改變

那再看到Views/_ViewStart.cshtml
@{
    Layout = "_Layout";
}
這檔就是可以指派要出現的Layout是哪個,也就是我們上面的Views/Shared/_Layout.cshtml(路徑預設會抓Views/Shared/下)
接著我們要示範將Controller裡的值傳到View中,這邊我們可以使用ViewData來做傳遞
首先先將我們的Welcome方法做修改,指定了兩個變數給ViewData
namespace MvcMovie.Controllers
{
    public class HelloWorldController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
        
        public IActionResult Welcome(string name, int numTimes = 1)
        {
            //這邊
            ViewData["Message"] = "Hello " + name;
            ViewData["NumTimes"] = numTimes;
            return View();
        }
    }
}
接著我們建立一個View在Views/HelloWorld/Welcome.cshtml
裡面內容為
@{
    ViewData["Title"] = "Welcome";
}
<h2>Welcome</h2>
<ul>
    @for (int i = 0; i < (int)ViewData["NumTimes"]!; i++)
    {
        <li>@ViewData["Message"]</li>
    }
</ul>接著輸入https://localhost:7079/HelloWorld/Welcome?name=Rick&numtimes=4
瀏覽器畫面則會是

就代表說我Controller設定的name跟次數都有正確的帶到View這邊
如此我們就完成了基本的檢視頁面建立跟傳值的功能了
4.新增模型
這邊難度就開始往上提升了
我們要開始建立跟資料庫連線的相關物件了,並且開始存取資料庫,那這邊是用CodeFirst的方式建立
首先我們先在建立一個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; }
    }
}這邊是在程式這邊寫好說,我有一個Movie的資料表,裡面有這些欄位
其中[DataType(DataType.Date)]是說我這欄位只顯示到日期部分
接著打開套件管理器主控台

輸入安裝以下套件
Install-Package Microsoft.EntityFrameworkCore.Design
Install-Package Microsoft.EntityFrameworkCore.SqlServer接著我們直接用Scaffold幫我們建立整套的CRUD頁面功能

選擇使用 Entity Framework執行檢視的MVC控制器後按加入

以下圖進行選擇

接著他會開始安裝套件,跟產生一些檔,還有相關程式碼,如下

那這時候網頁應用程式還無法正確運作,因為沒有對應的資料庫
接著我們下以下兩個指令
Add-Migration InitialCreate
Update-Database執行完後我們到這個網址:https://localhost:7079/Movies
就可以發現網頁應用程式可以正常運作了,你可以試試預設幫我們產生好的新增修改刪除介面

可以正常的CRUD了,所以到這裡結束了嗎?當然還沒,因為這都是自動幫我們產生的,總要知道相關原理吧
不然怎麼改成自己想要的畫面格式,所以接下來會介紹一些基本你應該知道的原理
最後我們把Movies改成預設頁面
Program.cs
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Movies}/{action=Index}/{id?}");修改網址列網址_Layout.cshtml
<div class="container-fluid">
    <a class="navbar-brand" asp-area="" asp-controller="Movies" asp-action="Index">MvcMovie</a>
    <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target=".navbar-collapse" aria-controls="navbarSupportedContent"
            aria-expanded="false" aria-label="Toggle navigation">
        <span class="navbar-toggler-icon"></span>
    </button>
    <div class="navbar-collapse collapse d-sm-inline-flex justify-content-between">
        <ul class="navbar-nav flex-grow-1">
            <li class="nav-item">
                <a class="nav-link text-dark" asp-area="" asp-controller="Movies" asp-action="Index">Home</a>
            </li>
        </ul>
    </div>
</div>
5.使用資料庫
既然剛剛已經能CRUD了,那資料庫在哪裡?
我們可以在Program.cs中看到宣告
builder.Services.AddDbContext<MvcMovieContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("MvcMovieContext") ?? throw new InvalidOperationException("Connection string 'MvcMovieContext' not found.")));
這是一個相依性插入容器註冊資料庫的語法,那至於是什麼意思我們這邊先不管
其中builder.Configuration.GetConnectionString("MvcMovieContext")是讀取我們appsettings.json
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "ConnectionStrings": {
    "MvcMovieContext": "Server=(localdb)\\mssqllocaldb;Database=MvcMovie.Data;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}上面MvcMovieContext後的字串,即為我們資料庫的連線至串,那這邊預設會使用SQL Server Express LocalDB
我們可以打開SQL Server 物件總管

之後就可以在對應的路徑找到我們CodeFirst產生的資料庫

接著我們來做資料庫的資料初始化,要這麼做是因為,可能你在資料庫剛建立時,就會先建立一些基本資料
那我們可以預先寫在程式哩,如果發現資料表是空的就會自動幫你補上要補的資料
在 Models 資料夾中建立名為 SeedData 的新類別。 使用下列程式碼取代產生的程式碼:
using Microsoft.EntityFrameworkCore;
using MvcMovie.Data;
namespace MvcMovie.Models
{
    public static class SeedData
    {
        public static void Initialize(IServiceProvider serviceProvider)
        {
            using (var context = new MvcMovieContext(
                serviceProvider.GetRequiredService<
                    DbContextOptions<MvcMovieContext>>()))
            {
                // Look for any movies.
                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();
            }
        }
    }
}那一開頭的這個
if (context.Movie.Any())
{
    return;   // DB has been seeded
}就代表說,如果Movie的資料表有任何資料的話,則不新增資料
最後在我們的Program.cs把要新增初始資料的動作加上去
using (var scope = app.Services.CreateScope())
{
    var services = scope.ServiceProvider;
    SeedData.Initialize(services);
}
接著我們在去首頁看,就會有資料了,如果你剛剛有自行新增資料的話,就不會有資料,妳可以將資料表中資料都刪掉,在執行一次,就可以自動幫你新增初始資料了

那我們回過頭來看一下Index.cshtml這頁的程式碼
先找到在MoviesController.cs對應的Action程式碼Index()
public async Task<IActionResult> Index()
{
    return View(await _context.Movie.ToListAsync());
}這邊是回傳Movie資料表所有的內容到View
接著回到Views/Movies/Index.cshtml
@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>
}foreach是c#基本語法,這邊就不特別說明了,值得一提的是我們使用@就可以在@Razor頁面使用c#語法
那之中的Model就是我們剛剛Action中傳回的資料
而在Index.cshtml最頂端,其實有先宣告會接收到什麼型態的資料,如下
@model IEnumerable<MvcMovie.Models.Movie>那些著使用@Html.DisplayFor()就可以把資料讀取出來秀在畫面上
如果有學過html的人,一定知道這並非html的格式,沒錯這是@Razor中的Tag helper(標籤協助)程式語法
好處是有智慧提示跟防呆,壞處就是又要多學多記一個東西
6.控制器動作與檢視
這邊開始又更進階一些,文章畢竟比較難描述過程,如果追不上可以看影片操作
接著我們看到表格的標題

都是英文的,相信是不行的,他預設是帶資料表的欄位名稱,對中文語系的我們,必須要另外改成中文
我們先看到Views/Movies/Index.cshtml檔裡標頭的部分
<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>這一樣是@Razor中的Tag helper(標籤協助)程式語法
那這邊的標題我該如何改成中文呢,我們可以到Models/Movie.cs,在上面加上[Display(Name = "")]
using System.ComponentModel.DataAnnotations;
namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        [Display(Name = "片名")]
        public string? Title { get; set; }
        [Display(Name = "發行日")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        
        [Display(Name = "類型")]
        public string? Genre { get; set; }
        
        [Display(Name = "價格")]
        public decimal Price { get; set; }
    }
}回到畫面看就可以發現對應的地方都變中文了

那這還有另外一個好處,就是其他新增、修改和明細頁面都會用到這些欄位名稱,所以統一使用@Html.DisplayNameFor
往後要改名稱的話,只要去Models檔裡改就好
接著我們看到修改、明細和刪除的網址

<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>這邊依然是用標籤協助程式所產生,那好處一樣
那在瀏覽器看到的語法會是
<td>
    <a href="/Movies/Edit/3">Edit</a> |
    <a href="/Movies/Details/3">Details</a> |
    <a href="/Movies/Delete/3">Delete</a>
</td>那這個網址是對應我們路由設定
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");然後去找MoviesController對應的三個Action(Edit、Details、Delete)
id的部分也就是3,是該筆的資料表主鍵欄位
那我們先看到Edit對應的程式碼
public async Task<IActionResult> Edit(int? id)
{
    if (id == null || _context.Movie == null)
    {
        return NotFound();
    }
    var movie = await _context.Movie.FindAsync(id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}上面是撈出要編輯的那筆,之後回傳到View,這樣我們才看的到要編輯的資料
Edit.cshtml
<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>那這邊一樣是用標籤協助程式產生相關欄位,而產生後畫面如下

最後我們按下Save後會用Post方法送出,那對應程式如下
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Edit(int id, [Bind("Id,Title,ReleaseDate,Genre,Price,Rating")] Movie movie)
{
    if (id != movie.Id)
    {
        return NotFound();
    }
    if (ModelState.IsValid)
    {
        try
        {
            _context.Update(movie);
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!MovieExists(movie.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }
        return RedirectToAction(nameof(Index));
    }
    return View(movie);
}我們方法一樣是Edit,不過注意上面多了一個[HttpPost],代表用Post則會走到這一段程式碼中
那裡面就是相關更新的Entity Framework Core程式碼
那在Edit.cshtml中有以下標籤
<span asp-validation-for="Price" class="text-danger"></span>這個是可以自動幫我們產生對應的驗證提示
如我價格輸入了不是數字,畫面則會出現提示The field 價格 must be a number.

那如果有一點寫網頁的經驗,就會知道這是javascript所控制的提示
如果我們停用javascript會怎樣?

一樣會跳出提示訊息,但此時就會變成是從伺服器告訴你的,也就是說
asp-validation-for標籤幫我們實作了前後端兩種驗證
那我常常專案都懶得寫前端驗證,只寫後端驗證,這是因為前端驗證只是"提示",後端驗證才是必須要的
但這邊你只要使用這個標籤,就簡單的完成了前後端驗證,是不是很棒呢
7.新增搜尋
接下來我們要來寫一個關鍵字搜尋的功能
我們找到Controllers/MoviesController.cs中Index部分的程式碼
public async Task<IActionResult> Index(string searchString)
{
    var movies = from m in _context.Movie
                 select m;
    if (!String.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }
    return View(await movies.ToListAsync());
}那上面就是等著Get傳來的searchString字串值,下面則是過濾的程式碼
我們試著在網址輸入https://localhost:7079?searchString=Ghost
就會發現畫面剩下兩筆

那這時候又想到了路由設定的id,是否可以將關鍵字搜尋網址變成這樣呢,https://localhost:7079/Movies/Index/Ghost
app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");答案是不推薦,因為這樣變成每筆不同的關鍵字都是不一樣的網址,相信這樣做鐵定是不好的
接著我們在Index.cshtml加上搜尋的輸入框
<form asp-controller="Movies" asp-action="Index">
    <p>
        Title: <input type="text"  name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>我們在試著使用輸入框搜尋Ghost,會發現結果是對的,但網址怪怪的

網址並沒有用上GET傳值,那是因為form預設是用POST傳值,那我們通常會使用GET傳值來處理,所以我們要指定方式
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        Title: <input type="text"  name="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>增加method="get",再次搜尋就會發現網址上有參數了

接著我們來做類別的搜尋,這邊先新增一個MovieGenreViewModel.cs 類別新增至 Models 資料夾
using Microsoft.AspNetCore.Mvc.Rendering;
namespace MvcMovie.Models
{
    public class MovieGenreViewModel
    {
        public List<Movie>? Movies { get; set; }
        public SelectList? Genres { get; set; }
        public string? MovieGenre { get; set; }
        public string? SearchString { get; set; }
    }
}接下來是Index中的程式碼,但是已經有些小複雜了,我覺得很難用文章講解,有興趣的可以看影片,會做一些講解
public async Task<IActionResult> Index(string movieGenre, string searchString)
{
    // Use LINQ to get list of genres.
    IQueryable<string> genreQuery = from m in _context.Movie
                                    orderby m.Genre
                                    select m.Genre;
    var movies = from m in _context.Movie
                 select m;
    if (!string.IsNullOrEmpty(searchString))
    {
        movies = movies.Where(s => s.Title!.Contains(searchString));
    }
    if (!string.IsNullOrEmpty(movieGenre))
    {
        movies = movies.Where(x => x.Genre == movieGenre);
    }
    var movieGenreVM = new MovieGenreViewModel
    {
        Genres = new SelectList(await genreQuery.Distinct().ToListAsync()),
        Movies = await movies.ToListAsync()
    };
    return View(movieGenreVM);
}最後是Index.cshtml的部分
<form asp-controller="Movies" asp-action="Index" method="get">
    <p>
        <select asp-for="MovieGenre" asp-items="Model.Genres">
            <option value="">All</option>
        </select>
        Title: <input type="text" asp-for="SearchString" />
        <input type="submit" value="Filter" />
    </p>
</form>
<table class="table">
    <thead>
        <tr>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Title)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].ReleaseDate)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Genre)
            </th>
            <th>
                @Html.DisplayNameFor(model => model.Movies![0].Price)
            </th>
            <th></th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model.Movies!)
        {
        <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>如此就能完成雙條件搜尋了

8.新增欄位
接著,我們程式一定會寫一寫,或者是後來需求增加,而需要新增一些欄位
我們只要找到要增加的那個類別檔,如Movie.cs
namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        [Display(Name = "片名")]
        public string? Title { get; set; }
        [Display(Name = "發行日")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        [Display(Name = "類型")]
        public string? Genre { get; set; }
        [Display(Name = "價格")]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
                
        [Display(Name = "等級")]
        public string? Rating { get; set; }
    }
}增加最下面一個Rating欄位
接著我們就分別更新相關的檔案都加上Rating欄位
那都修改完後,此時我們的應用程式是無法執行的,因為目前資料庫沒Rating欄位
那這邊有三種方式可以進行修正
- 讓 Entity Framework 自動卸除資料庫,並重新依據新的模型類別結構描述來建立資料庫。 在開發週期早期,當您在測試資料庫上進行開發時,這個方法會很方便;其可讓您一併調整模型和資料庫結構描述,更加快速。 不過,它的缺點是您會遺失資料庫中現有的資料 — 因此您不會想在實際執行的資料庫上使用這種方法! 使用初始設定式將測試資料自動植入資料庫,通常是開發應用程式的有效方式。 這是早期開發和使用 SQLite 時的好方法。
 - 您可明確修改現有資料庫的結構描述,使其符合模型類別。 這種方法的優點是可以保留您的資料。 您可以手動方式或藉由建立資料庫變更指令碼來進行這項變更。
 - 使用 Code First 移轉來更新資料庫結構描述。
 
那我們這邊會選擇第三種,因此下以下的指令
Add-Migration Rating
Update-Database如此在執行一次應用程式,就可以正常運作了
9.新增驗證
MVC 的設計原則之一是DRY (「不自行重複」)。 ASP.NET Core MVC 鼓勵您只指定一次功能或行為,然後讓它反映到應用程式的所有位置。 這會減少您需要撰寫的程式碼數量,並讓您撰寫的程式碼錯誤較不容易出錯、更容易測試,以及更容易維護。
MVC 和 Entity Framework Core Code First 所提供的驗證支援就是執行 DRY 準則的絶佳範例。 您可以宣告方式在單一位置指定驗證規則 (在模型類別中) ,而規則可在應用程式的任何位置強制執行。
接下來我們就要來增加一些驗證規則,首先我們先修改一下Movie.cs
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace MvcMovie.Models
{
    public class Movie
    {
        public int Id { get; set; }
        [Display(Name = "片名")]
        [StringLength(60, MinimumLength = 3)]
        [Required]
        public string? Title { get; set; }
        [Display(Name = "發行日")]
        [DataType(DataType.Date)]
        public DateTime ReleaseDate { get; set; }
        [Display(Name = "類型")]
        [RegularExpression(@"^[A-Z]+[a-zA-Z\s]*$")]
        [Required]
        [StringLength(30)]
        public string? Genre { get; set; }
        [Display(Name = "價格")]
        [Range(1, 100)]
        [DataType(DataType.Currency)]
        [Column(TypeName = "decimal(18, 2)")]
        public decimal Price { get; set; }
        [Display(Name = "等級")]
        [RegularExpression(@"^[A-Z]+[a-zA-Z0-9""'\s-]*$")]
        [StringLength(5)]
        [Required]
        public string? Rating { get; set; }
    }
}
可以看到每個欄位上加了很多東西,這一些就是驗證的規則,以下節錄官方文件的說明
驗證屬性會指定您想要對套用目標模型屬性強制執行的行為:
Required 和 MinimumLength 屬性 (attribute) 指出屬性 (property) 必須是值;但無法防止使用者輸入空格以滿足此驗證。
RegularExpression 屬性則用來限制可輸入的字元。 在上述程式碼中,"Genre":
- 必須指使用字母。
 - 第一個字母必須是大寫。 允許空白字元,而數位則不允許使用特殊字元。
 
RegularExpression "Rating":
- 第一個字元必須為大寫字母。
 - 允許後續空格中的特殊字元和數位。 "PG-13" 對分級而言有效,但不適用於 "Genre"。
 
Range 屬性會將值限制在指定的範圍內。
StringLength 屬性可讓您設定字串屬性的最大長度,並選擇性設定其最小長度。
實值型別 (如decimal、int、float、DateTime) 原本就是必要項目,而且不需要 [Required] 屬性。
擁有 ASP.NET Core 自動強制執行的驗證規則有助於讓您的應用程式更穩固。 它也確保您不會忘記要驗證某些項目,不小心讓不正確的資料進入資料庫。
修改完之後我們就可以回到網頁的新增頁面看看,此時先直接按Create,就會出現驗證提示

這些效果就是我們剛剛加上去的東西的效果
那我們來看一下Create執行的程式碼片段
[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);
}其實沒寫什麼,就單純的驗證不過,又重回同一頁,驗證通過就儲存並回到列表Index頁
那我們在ASP.NET 中寫驗證真的很簡單,只要在Model的屬性上,放上對應的驗證規則,直接就幫我們做好驗證邏輯
完全不需要自己動手寫驗證邏輯,而在程式的地方,也不會出現驗證的程式碼,程式變得很精簡分工明確
那如果覺得Model中堆得太高了,也可以像下面這樣寫
[Required, StringLength(60, MinimumLength = 3)]
public string? Title { get; set; }10.檢查詳細資料及刪除
這一部分,主要帶大家來看一下程式碼做了什麼事情
明細部分
// GET: Movies/Details/5
public async Task<IActionResult> Details(int? id)
{
    if (id == null)
    {
        return NotFound();
    }
    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}這邊就是當我沒傳id進來時,我就回傳404頁面給使用者
當查不到資料時一樣回傳404頁面給使用者
算是一個基本的防呆處理
再看到刪除的部分
// GET: Movies/Delete/5
public async Task<IActionResult> Delete(int? id)
{
    if (id == null)
    {
        return NotFound();
    }
    var movie = await _context.Movie
        .FirstOrDefaultAsync(m => m.Id == id);
    if (movie == null)
    {
        return NotFound();
    }
    return View(movie);
}
// POST: Movies/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
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));
}這邊有兩個方法,一個是進入delete頁面執行的程式,一個是送出刪除的執行程式
但由於都是只傳int id同樣的參數,那基本上不能有同樣名稱同樣參數的方法存在
所以我們這邊送出刪除的執行程式的方法就改為DeleteConfirmed名稱
但這樣就無法套用路由規則,因為規則會去找Delete方法
所以我們必須在上面再加上個Action別名[HttpPost, ActionName("Delete")]
這樣程式就能正確的運作
那或許也可以自行增加一個無用參數
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Delete(int id, bool notUsed)這樣也可以使用相同名稱不同參數的形式共存
結語
以上就是這整個課程的一個簡單示範教學
那其實裡面的寫法都是最基本的寫法,真的實務上在寫可能會變成不同的世界
不過也算是示範了一個基礎使用ASP.NET Core MVC 開發的感覺了
那如果有興趣的話,可以看我的ASP.NET Core MVC入門教學,裡面會針對每個部分進行講解,並有實務上的範例
一步步的帶你進入ASP.NET Core MVC的世界,不過現在還沒開始錄就是了XD