📑 Razor Pages
📌 什麼是 Razor Pages?
Razor Pages 是 ASP.NET Core 提供的一種以頁面為中心的開發模式,比 MVC 更簡單直觀。
想像你在蓋房子:
- MVC 模式 像是找三個不同的工匠——一個畫設計圖(View)、一個管工地(Controller)、一個準備材料(Model),分工合作
- Razor Pages 像是一個全能工匠——每一面牆(頁面)都由一個人負責設計和施工,簡單的案子效率更高
Razor Pages 特別適合以頁面為主的網站(如部落格、表單頁面、報表頁面),不需要 Controller 的額外層級。
🏗️ Razor Pages 基本結構
Pages/ # 所有頁面都放在 Pages 資料夾
├── Index.cshtml # 首頁的 HTML 模板(像牆壁的外觀)
├── Index.cshtml.cs # 首頁的邏輯程式碼(像牆壁的內部結構)
├── About.cshtml # 關於頁面
├── About.cshtml.cs # 關於頁面的邏輯
├── Products/ # 子資料夾 = URL 路徑的一部分
│ ├── Index.cshtml # /Products 頁面
│ ├── Details.cshtml # /Products/Details 頁面
│ └── Create.cshtml # /Products/Create 頁面
└── Shared/ # 共用元件
└── _Layout.cshtml # 版面配置(像房子的骨架)
📄 PageModel 類別
// Pages/Products/Index.cshtml.cs
using Microsoft.AspNetCore.Mvc; // 引用 MVC 命名空間
using Microsoft.AspNetCore.Mvc.RazorPages; // 引用 Razor Pages 命名空間
public class IndexModel : PageModel // 繼承 PageModel 基底類別
{
// 屬性:提供資料給頁面(像準備好的食材放在工作台上)
public List<Product> Products { get; set; } = new(); // 商品清單
public string Message { get; set; } = ""; // 顯示訊息
// OnGet:處理 GET 請求(使用者進入頁面時執行)
public void OnGet() // 當使用者瀏覽此頁面時
{
Message = "歡迎來到商品列表!"; // 設定訊息
Products = new List<Product> // 準備商品資料
{
new Product { Id = 1, Name = "筆電", Price = 30000 }, // 第一個商品
new Product { Id = 2, Name = "手機", Price = 25000 }, // 第二個商品
new Product { Id = 3, Name = "平板", Price = 18000 } // 第三個商品
};
}
// OnPost:處理 POST 請求(使用者提交表單時執行)
public IActionResult OnPost() // 當使用者送出表單時
{
if (!ModelState.IsValid) // 如果資料驗證失敗
{
return Page(); // 回到同一頁,顯示錯誤訊息
}
// 處理表單資料...
return RedirectToPage("./Index"); // 重新導向到清單頁面
}
}
🎨 Razor 頁面模板 (.cshtml)
@* Pages/Products/Index.cshtml *@
@page @* 這行超重要!標記此檔案是 Razor Page *@
@model IndexModel @* 綁定對應的 PageModel 類別 *@
<h1>@Model.Message</h1> @* 顯示 PageModel 中的 Message 屬性 *@
@* 用表格顯示商品清單 *@
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>商品名稱</th>
<th>價格</th>
<th>操作</th>
</tr>
</thead>
<tbody>
@foreach (var product in Model.Products) @* 走訪每個商品 *@
{
<tr>
<td>@product.Id</td> @* 顯示商品 ID *@
<td>@product.Name</td> @* 顯示商品名稱 *@
<td>@product.Price 元</td> @* 顯示商品價格 *@
<td>
@* asp-page 指向另一個 Razor Page *@
<a asp-page="./Details"
asp-route-id="@product.Id">
詳細資料
</a>
</td>
</tr>
}
</tbody>
</table>
@* 新增商品的表單 *@
<form method="post"> @* POST 表單會觸發 OnPost 方法 *@
<div>
<label>商品名稱</label>
<input asp-for="NewProduct.Name" /> @* 綁定到 PageModel 的屬性 *@
</div>
<button type="submit">新增</button> @* 送出按鈕 *@
</form>
🔄 OnGet 與 OnPost 處理器
// Pages/Contact.cshtml.cs - 聯絡表單範例
public class ContactModel : PageModel // 聯絡頁面的 PageModel
{
[BindProperty] // 自動將表單資料綁定到此屬性(像自動拆信取出內容)
public ContactForm ContactForm { get; set; } = new(); // 表單資料
public string SuccessMessage { get; set; } = ""; // 成功訊息
// 處理 GET 請求:顯示空白表單
public void OnGet() // 使用者開啟頁面時
{
// 不需要做什麼,顯示空白表單即可
}
// 處理 POST 請求:接收表單資料
public IActionResult OnPost() // 使用者提交表單時
{
if (!ModelState.IsValid) // 驗證是否通過
{
return Page(); // 驗證失敗,回到同一頁顯示錯誤
}
// 處理表單資料(例如寄送 Email)
SuccessMessage = $"感謝 {ContactForm.Name},我們已收到您的訊息!"; // 設定成功訊息
return Page(); // 回到同一頁顯示成功訊息
}
// 也可以有多個 POST Handler(像一個頁面有多個按鈕)
public IActionResult OnPostDelete(int id) // 當按下「刪除」按鈕時
{
// 刪除指定的資料
Console.WriteLine($"刪除 ID:{id}"); // 記錄刪除操作
return RedirectToPage(); // 重新導向回同一頁
}
// 非同步版本
public async Task<IActionResult> OnPostAsync() // 非同步的 POST 處理器
{
if (!ModelState.IsValid) // 驗證
return Page(); // 失敗就回到頁面
await SaveToDatabase(ContactForm); // 非同步存入資料庫
return RedirectToPage("./ThankYou"); // 導向感謝頁面
}
}
🏷️ @page 指令與 asp-page Tag Helper
// @page 指令可以自訂路由模板
// Pages/Products/Details.cshtml
@page "{id:int}" // URL 為 /Products/Details/5,id 必須是整數
@model DetailsModel // 綁定 PageModel
// Pages/Products/Details.cshtml.cs
public class DetailsModel : PageModel // 商品詳情的 PageModel
{
public Product Product { get; set; } = new(); // 商品資料
public void OnGet(int id) // id 會自動從路由參數取得
{
// 根據 id 查詢商品
Product = GetProductById(id); // 從資料庫取得商品
}
}
@* 使用 asp-page 在頁面之間導航(像建築物裡的門,連接不同房間) *@
@* 連結到同一層的頁面 *@
<a asp-page="./Create">新增商品</a> @* 連到 /Products/Create *@
@* 帶路由參數的連結 *@
<a asp-page="./Details"
asp-route-id="5">查看商品 5</a> @* 連到 /Products/Details/5 *@
@* 連結到根層的頁面 *@
<a asp-page="/Index">回首頁</a> @* 連到 / *@
@* 帶查詢字串的連結 *@
<a asp-page="./Index"
asp-route-search="手機">搜尋手機</a> @* 連到 /Products?search=手機 *@
📊 Razor Pages vs MVC:何時用哪個?
| 情境 | 推薦方式 | 原因 |
|---|---|---|
| 簡單的表單頁面 | Razor Pages | 一個檔案搞定,不需要 Controller |
| 部落格或內容網站 | Razor Pages | 以頁面為中心,結構直觀 |
| 複雜的 Web API | MVC (Controller) | Controller 更適合 API 設計 |
| 大型企業應用 | MVC | 更好的關注點分離 |
| 學習 ASP.NET | Razor Pages | 入門更簡單 |
| CRUD 表單 | Razor Pages | 內建 Handler 模式很適合 |
🤔 我這樣寫為什麼會錯?
❌ 錯誤 1:忘記加 @page 指令
@* ❌ 錯誤寫法:忘記 @page 指令 *@
@model IndexModel @* 只寫了 model,沒有 @page *@
<h1>我的頁面</h1>
@* 這個頁面無法被路由到!因為缺少 @page 指令 *@
@* ASP.NET 會把它當成一般的 View,而不是 Razor Page *@
@* ✅ 正確寫法:第一行就要有 @page *@
@page @* 告訴 ASP.NET 這是一個 Razor Page *@
@model IndexModel @* 綁定 PageModel *@
<h1>我的頁面</h1>
@* 現在可以透過 URL 正確存取了 *@
解釋: @page 就像門牌號碼——沒有門牌,郵差(路由系統)就找不到你家(頁面)。忘記加 @page 是 Razor Pages 最常見的錯誤,頁面會變成一般的 View,只能被 Controller 引用,無法直接透過 URL 存取。
❌ 錯誤 2:沒有使用 [BindProperty]
// ❌ 錯誤寫法:POST 表單的資料沒有綁定
public class CreateModel : PageModel // 新增頁面
{
public Product Product { get; set; } = new(); // 沒有加 [BindProperty]
public IActionResult OnPost() // 接收表單
{
// Product 永遠是空的!因為沒有 [BindProperty]
Console.WriteLine(Product.Name); // 永遠是 ""(空字串)
return Page(); // 回到頁面
}
}
// ✅ 正確寫法:用 [BindProperty] 標記要綁定的屬性
public class CreateModel : PageModel // 新增頁面
{
[BindProperty] // 告訴 ASP.NET 自動將表單資料填入這個屬性
public Product Product { get; set; } = new(); // 會自動接收表單資料
public IActionResult OnPost() // 接收表單
{
// Product 已經自動填入表單的資料了
Console.WriteLine(Product.Name); // 正確取得使用者輸入的名稱
return Page(); // 回到頁面
}
}
解釋: [BindProperty] 就像自動拆信機——沒有它,信(表單資料)送到了但沒人拆開來看,所以你的程式什麼都收不到。對於 POST 表單來說,[BindProperty] 是必要的。
❌ 錯誤 3:GET 請求用 [BindProperty] 卻沒設定 SupportsGet
// ❌ 錯誤寫法:想在 GET 請求中取得查詢字串參數
public class SearchModel : PageModel // 搜尋頁面
{
[BindProperty] // 預設只在 POST 時綁定
public string Keyword { get; set; } = ""; // GET 請求時永遠是空的
public void OnGet() // GET /Search?Keyword=手機
{
// Keyword 仍然是空字串![BindProperty] 預設不支援 GET
}
}
// ✅ 正確寫法:加上 SupportsGet = true
public class SearchModel : PageModel // 搜尋頁面
{
[BindProperty(SupportsGet = true)] // 明確允許 GET 請求的繫結
public string Keyword { get; set; } = ""; // 現在 GET 時也能取得了
public void OnGet() // GET /Search?Keyword=手機
{
// Keyword = "手機",正確取得查詢字串參數!
}
}
解釋: 預設情況下,[BindProperty] 只處理 POST 請求。如果你想在 GET 請求中也能自動綁定查詢字串參數,必須加上 SupportsGet = true。這是一個安全設計,避免 GET 請求意外修改了資料。