.NET Magazine國際中文電子雜誌
作 者:許薰尹
審 稿:張智凱
文章編號: N220223402
出刊日期: 2022/2/16
俗稱「工欲善其事,必先利其器」,在程式開發的過程中有了好的工具輔助能夠加快產能,在這篇文章中我們將介紹Visual Studio 2022的Scaffold功能,在ASP.NET Core MVC網站中快速設計一個控制器,透過Entity Framework Core存取資料庫,顯示圖書資料。而資料的排序的功能在網站程式中的其它控制器可能都會需要用到,我們希望修改將控制器的程式,加上排序資料功能,並將程式轉換成程式產生器的公版,以便後續利用程式碼產生器來自動產生控制器程式碼,讓產生出來的新控制器程式預設都有排序的功能。
使用Visual Studio 2022建立專案
從Visual Studio 2022「開始」視窗選取「Create a new project」選項,請參考下圖所示:
![clip_image002 clip_image002]()
圖 1:「Create a new project」選項。
從開發工具「Create a new project」對話盒中,選取 使用C# 語法的「ASP.NET Core Web App(Model –View - Controller)」項目,然後按一下「Next」按鈕,請參考下圖所示:
![clip_image004 clip_image004]()
圖 2:選取「ASP.NET Core Web App(Model –View - Controller)」項目。
在「Configure your new project」視窗中設定專案名稱、專案存放路徑,然後按下「Next」按鈕,請參考下圖所示:
![clip_image006 clip_image006]()
圖 3:「Configure your new project」視窗。
在「Additional information」視窗,確認左上方的「Target Framework」清單選取「.NET 6.0 」,確定「Authentication Type」項目設定為「None」,勾選右下方的「Configure for HTTPS」核取方塊,清除勾選下方的「Enable Docker」、「Enable Razor runtime compilation」核取方塊,然後按下「Create」按鈕建立專案,請參考下圖所示:
![clip_image008 clip_image008]()
圖 4:「Additional information」視窗。
加入模型類別
下一個步驟是加入「Book」模型描述圖書資料,從「Solution Explorer」視窗 -「Models」資料夾上方,按滑鼠右鍵,從快捷選單選擇「Add」- 「Class」新增一個「Book」類別,請參考下圖所示:
![clip_image010 clip_image010]()
圖 5:加入一個「Book」類別。
在「Book」類別之中加入以下屬性,描述圖書資料:
namespace CustomScaffold.Models
{
public class Book
{
public int Id { get; set; }
public string? Title { get; set; }
public int Price { get; set; }
public DateTime PublishDate { get; set; }
public bool InStock { get; set; }
public string? Description { get; set; }
}
}
使用Scaffold功能產生程式碼
接著透過Visual Studio Scaffold功能產生使用Entity Framework Core 存取資料庫的控制器與檢視的程式碼,從「Solution Explorer」視窗「Controllers」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Scaffolded Item」項目,請參考下圖所示:
![clip_image012 clip_image012]()
圖 6:加入控制器。
在「Add New Scaffolded Item」對話盒中選取「MVC Controller with Views, using Entity Framework」,然後按下「Add 」按鈕,請參考下圖所示:
![clip_image014 clip_image014]()
圖 7:加入控制器。
在下一個畫面,設定「Model Class」為「Book」類別,按下「Data context class」後方的「+」按鈕新增一個Data context class,請參考下圖所示:
![clip_image016 clip_image016]()
圖 8:新增一個data context class。
將「New data context type」設定為「BooksContext」,再按下「Add」按鈕,請參考下圖所示:
![clip_image018 clip_image018]()
圖 9:設定Data Context名稱。
回到前一個畫面,設定控制器名稱,然後按下「Add 」按鈕,請參考下圖所示:
![clip_image020 clip_image020]()
圖 10:新增控制器。
接著Visual Studio 2022會自動在「Controllers」資料夾產生「BooksController」的程式碼。同時Visual Studio也會在Views\Books資料夾中產生多個檢視的檔案。此外,「appsettings.json」檔案之中則記錄了資料庫連接字串的資訊,預設會將資料放在LocalDb資料庫:
\CustomScaffold\appsettings.json
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"BooksContext": "Server=(localdb)\\mssqllocaldb;Database=BooksContext-e9e53531-6eb4-4577-b8d7-8ea28faac5fd;Trusted_Connection=True;MultipleActiveResultSets=true"
}
}
Visual Studio 2022工具也會幫我們在「Program.cs」檔案中產生註冊Entity Framework Core服務的程式碼:
\CustomScaffold\Program.cs
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using CustomScaffold.Data;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDbContext<BooksContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("BooksContext")));
// Add services to the container.
builder.Services.AddControllersWithViews();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
app.UseAuthorization();
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
app.Run();
使用EF Core移轉建立資料庫
這個步驟將使用EF Core移轉(Entity Framework Core Migration)功能建立資料庫。從Visual Studio 2022開發工具「Tools」-「NuGet Package Manager」-「Package Manager Console」開啟「Package Manager Console」視窗,然後在命令提示字元中輸入「add-migration」指令,取一個名稱如「initial」:
Add-Migration initial
接著Visual Studio 2022會在專案中建立一個「Migrations」資料夾,裏頭包含多個C#檔案,包含變更資料庫結構的程式碼。在命令提示字元中輸入「update-database」指令,以更新資料庫:
Update-Database
得到的執行結果如下圖所示:
![clip_image002[4] clip_image002[4]]()
圖 11:使用EF移轉更新資料庫。
為了方便測試我們在「_Layout.cshtml」檔案中加入「Books」與「Create」兩個選單項目,以連結到「Books」控制器的「Index」與「Create」動作:
\CustomScaffold\Views\Shared\_Layout.cshtml
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - CustomScaffold</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" asp-append-version="true" />
<link rel="stylesheet" href="~/CustomScaffold.styles.css" asp-append-version="true" />
</head>
<body>
<header>
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container-fluid">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">CustomScaffold</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="Home" asp-action="Index">Home</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Books" asp-action="Index">Books</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Books" asp-action="Create">Create</a>
</li>
<li class="nav-item">
<a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>
<footer class="border-top footer text-muted">
<div class="container">
© 2021 - CustomScaffold - <a asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
</div>
</footer>
<script src="~/lib/jquery/dist/jquery.min.js"></script>
<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>
<script src="~/js/site.js" asp-append-version="true"></script>
@await RenderSectionAsync("Scripts", required: false)
</body>
</html>
在Visual Studio開發工具,按CTRL+F5執行網站,執行結果參考如下,我們可以透過目前的網站程式碼進行圖書資料的新、刪、查、改動作:
![clip_image004[4] clip_image004[4]]()
圖 12:資料新增。
以下則是圖書清單畫面:
![clip_image006[4] clip_image006[4]]()
圖 13:資料清單。
設計資料排序程式
修改工具產生出來的「BooksController」類別「Index」方法程式碼,傳入一個「sortOrder」參數來決定排序的欄位:
\CustomScaffold\Controllers\BooksController.cs
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using CustomScaffold.Data;
using CustomScaffold.Models;
namespace CustomScaffold.Controllers
{
public class BooksController : Controller
{
private readonly BooksContext _context;
public BooksController(BooksContext context)
{
_context = context;
}
// GET: Books
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["TitleSortParm"] = String.IsNullOrEmpty(sortOrder) ? "title_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var books = from s in _context.Book
select s;
switch (sortOrder)
{
case "title_desc":
books = books.OrderByDescending(b => b.Title);
break;
case "Date":
books = books.OrderBy(b => b.PublishDate);
break;
case "date_desc":
books = books.OrderByDescending(s => s.PublishDate);
break;
default:
books = books.OrderBy(s => s.Title);
break;
}
return View(await books.AsNoTracking().ToListAsync());
}
// GET: Books/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var book = await _context.Book
.FirstOrDefaultAsync(m => m.Id == id);
if (book == null)
{
return NotFound();
}
return View(book);
}
// GET: Books/Create
public IActionResult Create()
{
return View();
}
// POST: Books/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,Price,PublishDate,InStock,Description")] Book book)
{
if (ModelState.IsValid)
{
_context.Add(book);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(book);
}
// GET: Books/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var book = await _context.Book.FindAsync(id);
if (book == null)
{
return NotFound();
}
return View(book);
}
// POST: Books/Edit/5
// 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> Edit(int id, [Bind("Id,Title,Price,PublishDate,InStock,Description")] Book book)
{
if (id != book.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(book);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(book.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(book);
}
// GET: Books/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var book = await _context.Book
.FirstOrDefaultAsync(m => m.Id == id);
if (book == null)
{
return NotFound();
}
return View(book);
}
// POST: Books/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var book = await _context.Book.FindAsync(id);
_context.Book.Remove(book);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
private bool BookExists(int id)
{
return _context.Book.Any(e => e.Id == id);
}
}
}
修改「Views\Books\Index.cshtml」檢視的程式碼,在顯示資料的Table表頭使用超連結以切換排序方向性:
\CustomScaffold\Views\Books\Index.cshtml
@model IEnumerable<CustomScaffold.Models.Book>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["TitleSortParm"]">@Html.DisplayNameFor(model => model.Title)</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">@Html.DisplayNameFor(model => model.PublishDate)</a>
</th>
<th>
@Html.DisplayNameFor(model => model.InStock)
</th>
<th>
@Html.DisplayNameFor(model => model.Description)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.PublishDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.InStock)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</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>
執行這個網站,現在圖書資料可以根據「Title」與「PublishDate」進行排序動作,請參考下圖所示:
![clip_image008[4] clip_image008[4]]()
圖 14:圖書資料排序。
客製化程式碼產生器樣板
接下來讓我們來談談客製化動作,目地是後續透過Visual Studio Scaffold功能產生出的控制器程式都可以根據「Title」與「PublishDate」進行排序。
首先要安裝程式碼產生器套件,從「Solution Explorer」視窗 –> 專案名稱上方,按滑鼠右鍵,從快捷選單選擇「Manage NuGet Packages」項目,從對話盒「Browse」分頁上方文字方塊中,輸入查詢關鍵字,找到「Microsoft.VisualStudio.Web.CodeGeneration.Design」套件後,點選「Install」按鈕進行安裝,請參考下圖所示請參考下圖所示:
![clip_image010[4] clip_image010[4]]()
圖 15:安裝「Microsoft.VisualStudio.Web.CodeGeneration.Design」套件。
從以下資料夾複製「Templates」資料夾中想客製化的檔案到專案,這個資料夾會在你進行文章上述的Scaffold動作時自動建立:
C:\Users\使用者帳號\.nuget\packages\microsoft.visualstudio.web.codegenerators.mvc\6.0.1\Templates
以本文範例而言只會使用到「Templates\ControllerGenerator」與「Templates\ViewGenerator」兩個資料夾的程式碼,目前資料夾看起來如下圖所示:
![clip_image012[4] clip_image012[4]]()
圖 16:加入樣板。
修改「MvcControllerWithContext.cshtml」檔案產生「Index」方法的程式,約在行號84開始的程式碼,如下:
\CustomScaffold\Templates\ControllerGenerator\MvcControllerWithContext.cshtml
@inherits Microsoft.VisualStudio.Web.CodeGeneration.Templating.RazorTemplateBase
@{
if (@Model.NullableEnabled)
{
@:#nullable disable
}
}
@using System.Collections.Generic;
@using System.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
@{
foreach (var namespaceName in Model.RequiredNamespaces)
{
@:using @namespaceName;
}
}
namespace @Model.ControllerNamespace
{
@{
string routePrefix;
if (String.IsNullOrEmpty(Model.AreaName))
{
routePrefix = Model.ControllerRootName;
}
else
{
routePrefix = Model.AreaName + "/" + Model.ControllerRootName;
}
var modelProperties = new List<string>();
foreach (var property in Model.ModelMetadata.Properties)
{
if (property.Scaffold)
{
modelProperties.Add(property.PropertyName);
}
}
var bindString = string.Join(",", modelProperties);
var contextTypeName = Model.ContextTypeName;
var entitySetName = Model.ModelMetadata.EntitySetName;
var entitySetVar = Model.EntitySetVariable ??
(String.IsNullOrEmpty(entitySetName)
? entitySetName
: (entitySetName.Substring(0, length: 1).ToLowerInvariant() + entitySetName.Substring(1)));
var primaryKeyName = Model.ModelMetadata.PrimaryKeys[0].PropertyName;
var primaryKeyShortTypeName = Model.ModelMetadata.PrimaryKeys[0].ShortTypeName;
var primaryKeyType = Model.ModelMetadata.PrimaryKeys[0].TypeName;
var primaryKeyNullableTypeName = GetNullableTypeName(primaryKeyType, primaryKeyShortTypeName);
var lambdaVar = Model.ModelVariable[0];
var relatedProperties = new Dictionary<string, dynamic>();
foreach (var nav in Model.ModelMetadata.Navigations)
{
relatedProperties.Add(nav.AssociationPropertyName, nav);
}
var inlineIncludes = "";
foreach (var property in relatedProperties.Values)
{
inlineIncludes += string.Format("{0} .Include({1} => {1}.{2})", Environment.NewLine, lambdaVar, property.AssociationPropertyName);
}
if (!string.IsNullOrEmpty(Model.AreaName))
{
@:@string.Format("[Area(\"{0}\")]", Model.AreaName)
}
}
public class @Model.ControllerName : Controller
{
private readonly @Model.ContextTypeName _context;
public @(Model.ControllerName)(@Model.ContextTypeName context)
{
_context = context;
}
// GET: @routePrefix
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["TitleSortParm"] = String.IsNullOrEmpty(sortOrder) ? "title_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var @Model.ModelVariable = from s in _context.@(entitySetName) select s;
switch (sortOrder)
{
case "title_desc":
@(Model.ModelVariable) = @(Model.ModelVariable).OrderByDescending(b => b.Title);
break;
case "Date":
@(Model.ModelVariable) = @(Model.ModelVariable).OrderBy(b => b.PublishDate);
break;
case "date_desc":
@(Model.ModelVariable) = @(Model.ModelVariable).OrderByDescending(b => b.PublishDate);
break;
default:
@(Model.ModelVariable) = @(Model.ModelVariable).OrderBy(b => b.Title);
break;
}
return View(await @(Model.ModelVariable).AsNoTracking().ToListAsync());
}
// GET: @routePrefix/Details/5
public async Task<IActionResult> Details(@primaryKeyNullableTypeName id)
{
if (id == null)
{
return NotFound();
}
var @Model.ModelVariable = await _context.@(entitySetName)@inlineIncludes
.FirstOrDefaultAsync(m => m.@primaryKeyName == id);
if (@Model.ModelVariable == null)
{
return NotFound();
}
return View(@Model.ModelVariable);
}
// GET: @routePrefix/Create
public IActionResult Create()
{
@{
foreach (var property in relatedProperties.Values)
{
@:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName");
}
} return View();
}
// POST: @routePrefix/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("@bindString")] @Model.ModelTypeName @Model.ModelVariable)
{
if (ModelState.IsValid)
{
@{
if (!string.IsNullOrEmpty(primaryKeyType) && IsGuid(primaryKeyType))
{
@:@(Model.ModelVariable).@primaryKeyName = Guid.NewGuid();
}
@:_context.Add(@Model.ModelVariable);
@:await _context.SaveChangesAsync();
} return RedirectToAction(nameof(Index));
}
@{
foreach (var property in relatedProperties.Values)
{
@:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName", @(Model.ModelVariable).@property.ForeignKeyPropertyNames[0]);
}
}
return View(@Model.ModelVariable);
}
// GET: @routePrefix/Edit/5
public async Task<IActionResult> Edit(@primaryKeyNullableTypeName id)
{
if (id == null)
{
return NotFound();
}
var @Model.ModelVariable = await _context.@(entitySetName).FindAsync(id);
if (@Model.ModelVariable == null)
{
return NotFound();
}
@{
foreach (var property in relatedProperties.Values)
{
@:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName", @(Model.ModelVariable).@property.ForeignKeyPropertyNames[0]);
}
}
return View(@Model.ModelVariable);
}
// POST: @routePrefix/Edit/5
// 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> Edit(@primaryKeyShortTypeName id, [Bind("@bindString")] @Model.ModelTypeName @Model.ModelVariable)
{
if (id != @Model.ModelVariable.@primaryKeyName)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(@Model.ModelVariable);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!@(Model.ModelTypeName)Exists(@Model.ModelVariable.@primaryKeyName))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
@{
foreach (var property in relatedProperties.Values)
{
@:ViewData["@(property.ForeignKeyPropertyNames[0])"] = new SelectList(_context.@property.EntitySetName, "@property.PrimaryKeyNames[0]", "@property.DisplayPropertyName", @(Model.ModelVariable).@property.ForeignKeyPropertyNames[0]);
}
}
return View(@Model.ModelVariable);
}
// GET: @routePrefix/Delete/5
public async Task<IActionResult> Delete(@primaryKeyNullableTypeName id)
{
if (id == null)
{
return NotFound();
}
var @Model.ModelVariable = await _context.@(entitySetName)@inlineIncludes
.FirstOrDefaultAsync(m => m.@primaryKeyName == id);
if (@Model.ModelVariable == null)
{
return NotFound();
}
return View(@Model.ModelVariable);
}
// POST: @routePrefix/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(@primaryKeyShortTypeName id)
{
var @Model.ModelVariable = await _context.@(entitySetName).FindAsync(id);
_context.@(entitySetName).Remove(@Model.ModelVariable);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
private bool @(Model.ModelTypeName)Exists(@primaryKeyShortTypeName id)
{
return _context.@(entitySetName).Any(e => e.@primaryKeyName == id);
}
}
}
@functions
{
// This function converts the primary key short type name to its nullable equivalent when possible. This is required to make
// sure that an HTTP 400 error is thrown when the user tries to access the edit, delete, or details action with null values.
string GetNullableTypeName(string typeName, string shortTypeName)
{
// The exceptions are caught because if for any reason the type is user defined, then the short type name will be used.
// In that case the user will receive a server error if null is passed to the edit, delete, or details actions.
Type primaryKeyType = null;
try
{
primaryKeyType = Type.GetType(typeName);
}
catch
{
}
if (primaryKeyType != null && (!Microsoft.VisualStudio.Web.CodeGeneration.Templating.TypeUtilities.IsNullable(primaryKeyType) || IsGuid(typeName)))
{
return shortTypeName + "?";
}
return shortTypeName;
}
bool IsGuid(string typeName) {
return String.Equals("System.Guid", typeName, StringComparison.OrdinalIgnoreCase);
}
}
修改「Templates\ViewGenerator\List.cshtml」檔案的程式碼,約在行號54開始的程式碼:
\CustomScaffold\Templates\ViewGenerator\List.cshtml
@inherits Microsoft.VisualStudio.Web.CodeGeneration.Templating.RazorTemplateBase
@using Microsoft.VisualStudio.Web.CodeGeneration.EntityFrameworkCore
@using System.Collections.Generic
@using System.Linq
@@model @GetEnumerableTypeExpression(Model.ViewDataTypeName)
@{
if (Model.IsPartialView)
{
}
else if (Model.IsLayoutPageSelected)
{
@:@@{
@:ViewData["Title"] = "@Model.ViewName";
if (!string.IsNullOrEmpty(Model.LayoutPageFile))
{
@:Layout = "@Model.LayoutPageFile";
}
@:}
@:
@:<h1>@Model.ViewName</h1>
@:
}
else
{
@:@@{
@:Layout = null;
@:}
@:
@:<!DOCTYPE html>
@:
@:<html>
@:<head>
@:<meta name="viewport" content="width=device-width" />
@:<title>@Model.ViewName</title>
@:</head>
@:<body>
// PushIndent(" ");
}
@:<p>
@:<a asp-action="Create">Create New</a>
@:</p>
@:<table class="table">
@:<thead>
@:<tr>
Dictionary<string, IPropertyMetadata> propertyLookup = ((IModelMetadata)Model.ModelMetadata).Properties.ToDictionary(x => x.PropertyName, x => x);
Dictionary<string, INavigationMetadata> navigationLookup = ((IModelMetadata)Model.ModelMetadata).Navigations.ToDictionary(x => x.AssociationPropertyName, x => x);
foreach (var item in Model.ModelMetadata.ModelType.GetProperties())
{
if (propertyLookup.TryGetValue(item.Name, out IPropertyMetadata property)
&& property.Scaffold && !property.IsForeignKey && !property.IsPrimaryKey)
{
if (item.Name == "Title")
{
<th>
<a asp-action="Index" asp-route-sortOrder="@@ViewData["TitleSortParm"]">
@@Html.DisplayNameFor(model => model.Title)
</a>
</th>
}
else if(item.Name == "PublishDate")
{
<th>
<a asp-action="Index" asp-route-sortOrder="@@ViewData["DateSortParm"]">
@@Html.DisplayNameFor(model => model.PublishDate)
</a>
</th>
}
else
{
<th>
@@Html.DisplayNameFor(model => model.@GetValueExpression(property))
</th>
}
}
else if (navigationLookup.TryGetValue(item.Name, out INavigationMetadata navigation))
{
<th>
@@Html.DisplayNameFor(model => model.@GetValueExpression(navigation))
</th>
}
}
@:<th></th>
@:</tr>
@:</thead>
@:<tbody>
@:@@foreach (var item in Model) {
@:<tr>
foreach (var item in Model.ModelMetadata.ModelType.GetProperties())
{
if (propertyLookup.TryGetValue(item.Name, out IPropertyMetadata property)
&& property.Scaffold && !property.IsForeignKey && !property.IsPrimaryKey)
{
<td>
@@Html.DisplayFor(modelItem => item.@GetValueExpression(property))
</td>
}
else if (navigationLookup.TryGetValue(item.Name, out INavigationMetadata navigation))
{
<td>
@@Html.DisplayFor(modelItem => item.@GetValueExpression(navigation).@navigation.DisplayPropertyName)
</td>
}
}
string pkName = GetPrimaryKeyName();
if (pkName != null)
{
@:<td>
@:<a asp-action="Edit" asp-route-id="@@item.@pkName">Edit</a> |
@:<a asp-action="Details" asp-route-id="@@item.@pkName">Details</a> |
@:<a asp-action="Delete" asp-route-id="@@item.@pkName">Delete</a>
@:</td>
}
else
{
<td>
@@Html.ActionLink("Edit", "Edit", new { /* id=item.PrimaryKey */ }) |
@@Html.ActionLink("Details", "Details", new { /* id=item.PrimaryKey */ }) |
@@Html.ActionLink("Delete", "Delete", new { /* id=item.PrimaryKey */ })
</td>
}
@:</tr>
@:}
@:</tbody>
@:</table>
if(!Model.IsPartialView && !Model.IsLayoutPageSelected)
{
//ClearIndent();
@:</body>
@:</html>
}
}
@functions
{
string GetPrimaryKeyName()
{
return (Model.ModelMetadata.PrimaryKeys != null && Model.ModelMetadata.PrimaryKeys.Length == 1)
? Model.ModelMetadata.PrimaryKeys[0].PropertyName
: null;
}
string GetValueExpression(IPropertyMetadata property)
{
return property.PropertyName;
}
string GetValueExpression(INavigationMetadata navigation)
{
return navigation.AssociationPropertyName;
}
string GetEnumerableTypeExpression(string typeName)
{
return "IEnumerable<" + typeName + ">";
}
}
好了,現在可以來測試我們客製化的程式碼產生器樣板,刪除「BooksController.cs」檔案,重複前述文章的步驟,透過Visual Studio Scaffold功能重新產生程式碼。
從「Solution Explorer」視窗「Controllers」資料夾上方按滑鼠右鍵,從快捷選單選擇「Add」- 「New Scaffolded Item」項目。在「Add New Scaffolded Item」對話盒中選取「MVC Controller with Views, using Entity Framework」,然後按下「Add 」按鈕。
在下一個畫面,設定Model Class為「Book」類別,並設定控制器名稱,請參考下圖所示:
![clip_image014[4] clip_image014[4]]()
圖 17:使用Scaffold功能產生程式碼。
若一切順利的話,便可重新執行程式碼,這個範例應該都能正常運作。產生出來的「BooksController」程式碼如下:
\CustomScaffold\Controllers\BooksController.cs
#nullable disable
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;
using CustomScaffold.Data;
using CustomScaffold.Models;
namespace CustomScaffold.Controllers
{
public class BooksController : Controller
{
private readonly BooksContext _context;
public BooksController(BooksContext context)
{
_context = context;
}
// GET: Books
public async Task<IActionResult> Index(string sortOrder)
{
ViewData["TitleSortParm"] = String.IsNullOrEmpty(sortOrder) ? "title_desc" : "";
ViewData["DateSortParm"] = sortOrder == "Date" ? "date_desc" : "Date";
var book = from s in _context.Book select s;
switch (sortOrder)
{
case "title_desc":
book = book.OrderByDescending(b => b.Title);
break;
case "Date":
book = book.OrderBy(b => b.PublishDate);
break;
case "date_desc":
book = book.OrderByDescending(b => b.PublishDate);
break;
default:
book = book.OrderBy(b => b.Title);
break;
}
return View(await book.AsNoTracking().ToListAsync());
}
// GET: Books/Details/5
public async Task<IActionResult> Details(int? id)
{
if (id == null)
{
return NotFound();
}
var book = await _context.Book
.FirstOrDefaultAsync(m => m.Id == id);
if (book == null)
{
return NotFound();
}
return View(book);
}
// GET: Books/Create
public IActionResult Create()
{
return View();
}
// POST: Books/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,Price,PublishDate,InStock,Description")] Book book)
{
if (ModelState.IsValid)
{
_context.Add(book);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(book);
}
// GET: Books/Edit/5
public async Task<IActionResult> Edit(int? id)
{
if (id == null)
{
return NotFound();
}
var book = await _context.Book.FindAsync(id);
if (book == null)
{
return NotFound();
}
return View(book);
}
// POST: Books/Edit/5
// 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> Edit(int id, [Bind("Id,Title,Price,PublishDate,InStock,Description")] Book book)
{
if (id != book.Id)
{
return NotFound();
}
if (ModelState.IsValid)
{
try
{
_context.Update(book);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!BookExists(book.Id))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));
}
return View(book);
}
// GET: Books/Delete/5
public async Task<IActionResult> Delete(int? id)
{
if (id == null)
{
return NotFound();
}
var book = await _context.Book
.FirstOrDefaultAsync(m => m.Id == id);
if (book == null)
{
return NotFound();
}
return View(book);
}
// POST: Books/Delete/5
[HttpPost, ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(int id)
{
var book = await _context.Book.FindAsync(id);
_context.Book.Remove(book);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
private bool BookExists(int id)
{
return _context.Book.Any(e => e.Id == id);
}
}
}
「Index.cshtml」檔案是根據「List.cshtml」樣板來產生程式碼,產生出來的「Index.cshtml」檔案程式如下:
\CustomScaffold\Views\Books\Index.cshtml
@model IEnumerable<CustomScaffold.Models.Book>
@{
Layout = null;
}
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width" />
<title>Index</title>
</head>
<body>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
<thead>
<tr>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["TitleSortParm"]">
@Html.DisplayNameFor(model => model.Title)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.Price)
</th>
<th>
<a asp-action="Index" asp-route-sortOrder="@ViewData["DateSortParm"]">
@Html.DisplayNameFor(model => model.PublishDate)
</a>
</th>
<th>
@Html.DisplayNameFor(model => model.InStock)
</th>
<th>
@Html.DisplayNameFor(model => model.Description)
</th>
<th></th>
</tr>
</thead>
<tbody>
@foreach (var item in Model) {
<tr>
<td>
@Html.DisplayFor(modelItem => item.Title)
</td>
<td>
@Html.DisplayFor(modelItem => item.Price)
</td>
<td>
@Html.DisplayFor(modelItem => item.PublishDate)
</td>
<td>
@Html.DisplayFor(modelItem => item.InStock)
</td>
<td>
@Html.DisplayFor(modelItem => item.Description)
</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>
</body>
</html>
使用aspnet-codegenerator
在開發過程中重複使用Visual Studio 2022 選單的「Add」>「 New Scaffolded Item」功能來測試程式碼產生器產生的程式碼有一點煩雜。我們可以利用指令來加快這個開發流程。若要重複產生多個控制器與檢視程式碼,直接執行「aspnet-codegenerator」會比手動操作Visual Studio快很多,將之儲存成批次檔以方便重複執行。
例如我們可以在專案根目錄下加入一個「run.bat」批次檔,然後加入以下指令,先透過「del」刪除先前產生的「BooksController.cs」檔案,然後使用「dotnet」執行「aspnet-codegenerator」指定以下參數:「controller」表示要產生控制器程式碼;「-name」指定控制器名稱為「BooksController」;「-m」指定模型類別為「Book」;「-dc」指定DbContext類別為「BooksContext 」;「-outDir」指定要將檔案放在「Controllers」資料夾;「-f」則是在檔案存在時,強迫覆寫檔案:
\CustomScaffold\run.bat
del "Controllers\BooksController.cs" 2>NUL
dotnet aspnet-codegenerator controller -name BooksController -m Book -dc BooksContext -outDir Controllers -f
我們可以在「Package Manager Console」執行這個批次檔,這個命令執行結果,請參考下圖所示:
![clip_image016[4] clip_image016[4]]()
圖 18:使用批次檔產生程式。
若要套用指定的版面配置頁,可以修改批次檔,設定「--layout」參數:
del "Controllers\BooksController.cs" 2>NUL
dotnet aspnet-codegenerator controller -name BooksController -m Book -dc BooksContext -outDir Controllers -f --layout _Layout.cshtml
產生的「Index」檢視程式如下,程式上方會加入「Layout」屬性的設定:
@model IEnumerable<CustomScaffold.Models.Book>
@{
ViewData["Title"] = "Index";
Layout = "_Layout.cshtml";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table">
//以下略
若要套用預設的版面配置頁,可以修改批次檔,設定「-- useDefaultLayout」參數:
del "Controllers\BooksController.cs" 2>NUL
dotnet aspnet-codegenerator controller -name BooksController -m Book -dc BooksContext -outDir Controllers -f --useDefaultLayout
產生的Index檢視程式如下,會移除<Html>、<Body>等HTML標籤:
@model IEnumerable<CustomScaffold.Models.Book>
@{
ViewData["Title"] = "Index";
}
<h1>Index</h1>
<p>
<a asp-action="Create">Create New</a>
</p>
<table class="table"
//以下略
更多aspnet-codegenerator參數請參考官方文件:
https://docs.microsoft.com/en-us/aspnet/core/fundamentals/tools/dotnet-aspnet-codegenerator?view=aspnetcore-6.0