這篇是參考官方文件-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