檔案架構長這樣,有什麼 controller 就要有什麼 view 下的資料夾,
有什麼 view,controller 就要有什麼函式
pattern 是網址路徑,controller 名字 / view 名字 / ID,有等於的代表胡果是這個就可以省略
圖中 controller = Home 代表 contrler 名字是 Home 就可以省略,後面 view 一樣意思
ID 的問號代表可有可無,沒有打上 ID,網址一樣可用,ID 是錯的也可用
下面兩張圖是一樣的網頁(注意網址)
使用套件:
- Microsoft.EntityFrameworkCore.SqlServer
- Microsoft.EntityFrameworkCore.Tools
把已經建好的資料庫弄到專案裡的 Model
在套件管理主控台輸入以下指令以連接至資料庫
! 注意 ! 程式碼要確定沒有錯誤,不然指令輸入後會出錯,他也不會告訴你錯在哪
Scaffold-DbContext "Server=伺服器位置;Database=資料庫;User ID=帳號;Password=密碼;TrustServerCertificate=true" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models -NoOnConfiguring -UseDatabaseNames -NoPluralize -Force
TrustServerCertificate=true:如果出現 "此憑證鏈結是由不受信任的授權單位發出的" 可以在連線字串裡加這個參數
-OutputDir Models:指說要將相關檔案產生在這個資料夾底下
-NoOnConfiguring:DbContext不要產生OnConfiguring片段,現在還不會用到
-UseDatabaseNames:使用跟資料庫一樣的大小寫命名,不然可能大小寫會被改成別的風格
-NoPluralize:不要加複數s,不然會幫你在命名結尾上加上s
-Force:是如果此位置已有相同檔案時覆蓋,就算沒檔案也可以多這個參數
把專案裡打好的 Model,弄到資料庫
在 Modle 資料夾建立類別
打上欄位
namespace Kcg.Models;
public partial class TOPMenu
{
public Guid TOPMenuId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string Icon { get; set; }
public int Orders { get; set; }
}
再來一樣在 Model 建立一個類別,名稱為 資料庫名Context
public partial class KcgContext : DbContext //繼承 DbContext
{
public KcgContext(DbContextOptions<KcgContext> options) : base(options)
{
}
public DbSet<TOPMenu> TOPMenu { get; set; } //建立資料表
}
在 appsettings.json (參數檔)打上連線字串
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"ConnectionStrings": {
"KcgDatabase": "Server=伺服器位置;Database=資料庫名稱;User ID=使用者名稱;Password=密碼;TrustServerCertificate=true"
}
}
在 Program.cs 加入資料庫物件的DI注入
UseSqlServer() 裡的一整串代表去 appsettings 抓連線字串(Server=伺服器位置;Database=資料庫名稱;User ID=使用者名稱;Password=密碼;TrustServerCertificate=true)
builder.Services.AddDbContext<KcgContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("KcgDatabase")));
在套件管理主控台輸入以下指令,建立記錄檔(InitialCreate 是自己取的名字)
Add-Migration InitialCreate
在套件管理主控台輸入以下指令,更新資料庫,這時資料庫才會有剛剛打的資料表
Update-Database
新增資料表特性
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TOPMenu>(entity =>
{
entity.Property(e => e.TOPMenuId).HasDefaultValueSql("(newid())"); //預設值或繫結
entity.Property(e => e.Icon).IsRequired().HasMaxLength(50);
entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
entity.Property(e => e.Url).IsRequired().HasMaxLength(50);
entity.Property(e => e.Orders).IsRequired();
});
}
再來要建立記錄檔,表示資料表有更新(一樣 TOPMenuUp 是自己取的名字)
Add-Migration TOPMenuUp
最後再 update 資料庫
Update-Database
新增資料到資料表
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<TOPMenu>().HasData(
new TOPMenu { Name = "網站導覽", Url = "a1", Icon = "Icon1", Orders = 1 },
new TOPMenu { Name = "回首頁", Url = "a2", Icon = "Icon2", Orders = 2 },
new TOPMenu { Name = "English", Url = "a3", Icon = "Icon3", Orders = 3 },
new TOPMenu { Name = "雙語詞彙", Url = "a4", Icon = "Icon4", Orders = 4 },
new TOPMenu { Name = "市長信箱", Url = "a5", Icon = "Icon5", Orders = 5 },
new TOPMenu { Name = "洽公導覽", Url = "a6", Icon = "Icon6", Orders = 6 });
modelBuilder.Entity<TOPMenu>(entity =>
{
entity.Property(e => e.TOPMenuId).HasDefaultValueSql("(newid())");
entity.Property(e => e.Icon).IsRequired().HasMaxLength(50);
entity.Property(e => e.Name).IsRequired().HasMaxLength(50);
entity.Property(e => e.Url).IsRequired().HasMaxLength(50);
entity.Property(e => e.Orders).IsRequired();
});
}
更新資料庫
Add-Migration AddData
Update-Database
先在 controller 的全域宣告一個資料庫物件
private readonly KcgContext _kcgContext; //先在全域宣告資料庫物件
public HomeController(KcgContext kcgContext) //這邊是依賴注入,使用剛剛在 code first 的 Program.cs 設定好的資料庫物件的寫法
{
_kcgContext = kcgContext; //這樣後面都可以藉由 _kcgContext 來取資料庫資料
}
例如我要回傳 TOPMenu 資料表的第一筆資料的 Name
public string Index()
{
return _kcgContext.TOPMenu.FirstOrDefault().Name;
}
controller
public IActionResult index()
{
ViewData["要傳的變數"] = 要傳得值;
return View();
}
view\controllerName\index
@ViewData["要傳的變數"] //Razor語法,想要在這用c#語法或變數時要加@
controller
public IActionResult index()
{
ViewBag.要傳的變數 = 要傳得值;
return View();
}
view\controllerName\index
@ViewBag.要傳的變數 //Razor語法,想要在這用c#語法或變數時要加@
controller
public IActionResult index()
{
ViewData["title"] = "這是 ViewDate 設定的值";
ViewBag.Title = "這是 ViewBag 設定的值"; //這裡 T 我用大寫
return View();
}
view\controllerName\index
@ViewData["title"]
<br/>
@ViewBag.Title
ViewData 跟 ViewBag 不用事先宣告,可以直接使用,但會造成程式碼雜亂不好維護,所以要少用,或用在簡單的地方,例如<title/>
! 注意 ! ViewData 稍微嚴謹一點
public IActionResult Index()
{
ViewData["number1"] = 10;
int ViewDataNum = 10 + (int)ViewData["number1"]; //ViewData 比較嚴謹,要給型別
ViewBag.number2 = 10;
int ViewBagNum = 10 + ViewBag.number2;
return View();
}
兩個的生命週期只有一個 request,下一個 request 又是新的變數,例如我在另一個 view 使用 @ViewData["number1"],雖然我有先載入 index 頁面,但另一個 view 就是另一個 request 了,所以另一個 view 印不出 ViewData["number1"] 的值
用起來跟 ViewData 差不多
public IActionResult index()
{
TempData["Temp"] = "這是 Temp 設定的值";
return View();
}
他預設的生命週期是使用過一次才會消失,例如我在另一個 view 使用 @TempData["Temp"],然後先載入 index 頁面(index 的 view 沒有印 TempData),在載入另一個 view,TempData就出來了,但我重新整理後又不見了,因為 TempData 使用過一次就會消失,除非再載入 index 一次
先去 Home 再去 Privacy
重新整理
但只要加上 TempData.Keep() 就能讓他再存在一次
public IActionResult Privacy()
{
TempData.Keep("Temp");
return View();
}
先去 Home 再去 Privacy,不管重新整理幾次他都還在,可是這時候如果我讓另一個頁面也使用 Temp,而且沒有 TempData.Keep(),他就只會出現一次,重新整理或回到 Privacy 都不會出現 Temp 了,除非回到 Home 重新設定
@TempData["Temp"]
request > Controller > View > Layout(主版型)
設定主板型(版型放在View\Shared) View_ViewStart
@{
var controller = ViewContext.RouteData.Values["controller"].ToString();
if(controller == "Demo")
{
Layout = "_DemoLayout";
}
else
{
Layout = "_Layout";
}
}
View\Shared_DemoLayout
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">Demo</a> //讓左上角標題變Demo
也可以再放一個 _ViewStart 在要改版型的 controller 的 view 資料夾裡,這樣會先執行外面的 _ViewStart,再執行裡面的 _ViewStart,裡面蓋掉外面的,所以最後 Demo 的版型會以裡面的為主
不想用 _ViewStart,只想要某個 view 用其他版型,可以直接在 index 裡使用
View\Demo\index
@{
Layout = "_DemoLayout"; //Layout = null; 代表不套版型
}
@{
ViewData["Title"] = "Home Page";
}
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://learn.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
@{
string title = "在 @{ } 中可以使用 c# 語法";
int number = 5;
}
@title : @number
@if(number > 0)
{
//在這邊還是寫程式碼區塊
<div>如果今天要在這裡面顯示文字到網頁上,就寫在html標籤裡面即可</div>
<text>如果怕破壞格式不想用html標籤,可以用text,則不會留下痕跡</text>
}
else
{
<div>number 小於等於 0</div>
}
<div>@@</div>//這樣會顯示一個@
@{
string 小老鼠 = "@"; //可以用中文當變數,超酷
}
<!-- 想要在@後面加文字 -->
<div>@(小老鼠)大老鼠</div>

藉著範例檔能使用新增、查詢、修改和刪除的功能
首先在 Controller 建立新的控制器
使用 Kcg 資料庫的 News 資料表
然後他會幫我們新增 Views 下的 News 資料夾,裡面直接就有新增、刪除、細節、編輯及顯示五個檔案
為了更方便檢視畫面,在上面標提列新增 NewsIndex
結果會跟第一張圖一樣,新增、刪除及編輯都會跟資料庫同步
可以發現 Contents 欄位的文字特別長,所以我們讓他只在 Details 顯示
把這邊刪除只是讓他不顯示,但仍然有耗費資源效能抓資料
Controller 的這個寫法代表把 News 所有資料取下來
改成一欄一欄篩選
var result = from a in _context.News
select new News
{
Click = a.Click,
DepartmentId = a.DepartmentId,
Enable = a.Enable,
EndDateTime = a.EndDateTime,
InsertDateTime = a.InsertDateTime,
InsertEmployeeId = a.InsertEmployeeId,
NewsId = a.NewsId,
StartDateTime = a.StartDateTime,
Title = a.Title,
UpdateDateTime = a.UpdateDateTime,
UpdateEmployeeId = a.UpdateEmployeeId
};
return View(await result.ToListAsync());
現在畫面上的 DepartmentId、InsertEmployeeId 跟 UpdateEmployeeId 欄位都只是代號,所以現在要 join 其他資料表的資料近來(先 DepartmentId 跟 UpdateEmployeeId),把上面的程式碼改成下面這樣
var result = from a in _context.News
join b in _context.Department on a.DepartmentId equals b.DepartmentId //join Department 資料表,且 DepartmentId 欄位要跟 a.DepartmentId 欄位一樣
join c in _context.Employee on a.UpdateEmployeeId equals c.EmployeeId
select new //去掉 News 型別,因為裡面不只有 News 資料表的資料
{
Click = a.Click,
DepartmentId = a.DepartmentId,
Enable = a.Enable,
EndDateTime = a.EndDateTime,
InsertDateTime = a.InsertDateTime,
InsertEmployeeId = a.InsertEmployeeId,
NewsId = a.NewsId,
StartDateTime = a.StartDateTime,
Title = a.Title,
UpdateDateTime = a.UpdateDateTime,
UpdateEmployeeId = a.UpdateEmployeeId,
UpdateEmployeeName = c.Name,
DepartmentName = b.Name
};
去掉 News 型別後,他就變無型別,這樣不好維護,所以使用DTO寫法,讓他加上強型別
namespace Kcg.Dtos
{
public class NewsDto
{
//DTO 也可以做到減少不必要的欄位
public Guid NewsId { get; set; }
public string Title { get; set; }
public string DepartmentName { get; set; }
public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
public DateTime UpdateDateTime { get; set; }
public string UpdateEmployeeName { get; set; }
public int Click { get; set; }
public bool Enable { get; set; }
}
}
回到 Controller,可以加上型別 NewsDto,把不必要的欄位刪掉
! 注意 ! 要 using 剛剛的 Dtos
using Kcg.Dtos
Views/Index 的 model 要改成 NewsDto
會需要 using Dtos,放在 Views/Shared/_ViewImports.cshtml 可以省去一直 using 的麻煩,這邊打了,Views 底下其他檔案都有效
@using Kcg.Dtos
回到 Index 把欄位名改成剛剛 controller 設定的,以及把該刪的欄位刪掉
結果圖
範例的 create 頁面
他在 controller 有兩個區塊
在 index 點 create new 用的是上面的,在 create 頁面點 create 是下面的
下面的 HttpPost 意思是方法走 Post,要知道是走 Psot 要去網頁的 html 看
Bind 裡面有太多欄位,不好維護
//create 一個 news,裡面 Bind 會收到的資料
public async Task<IActionResult> Create([Bind("NewsId,Title,Contents,DepartmentId,StartDateTime,EndDateTime,InsertDateTime,InsertEmployeeId,UpdateDateTime,UpdateEmployeeId,Click,Enable")] News news)
{
if (ModelState.IsValid)
{
news.NewsId = Guid.NewGuid();
_context.Add(news); //加資料
await _context.SaveChangesAsync(); //存到資料庫
return RedirectToAction(nameof(Index)); //返回 Index
}
return View(news);
}
可以用 Dto 方法替代,先建 dto 檔,一樣有刪掉一些不必要的欄位
public class NewsCreateDto
{
public string Title { get; set; }
public string Contents { get; set; }
public int DepartmentId { get; set; }
public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
}
然後剛剛 controller 裡 Bind 一大串就跟裡面改成這樣
public async Task<IActionResult> Create(NewsCreateDto news)
{
if (ModelState.IsValid) //欄位值要是有效的
{
News insert = new News()
{
Title = news.Title,
Contents = news.Contents,
DepartmentId = news.DepartmentId,
StartDateTime = news.StartDateTime,
EndDateTime = news.EndDateTime,
Click = 0, //這4個欄位直接給定
Enable = true,
InsertEmployeeId = 1,
UpdateEmployeeId = 1
};
_context.News.Add(insert);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(news); //有欄位無效就繼續在 Create 頁面,且剛剛填的資料還保存著
}
! 注意 ! Views/Create.cshtml
@model NewsCreateDto
改好後下面有欄位需要刪,這邊體現 dto 的好處,他會有智慧提示錯誤在哪裡,如果是 Bind 就沒有
在有空格的情況下點 Create 按鈕,發現根本沒有 request,沒有進到 controller 的下面區塊,因為前端就擋掉了,在 Views/Create.cshtml 就擋掉了
試著建一筆資料
! 注意 ! DepartmentId 欄位的值要有對上 Department 資料表的代號,不然抓不到資料,也就不會顯示有新增一筆資料了(雖然資料庫還是有新增資料,但網頁抓不到)
範例 edit 頁面
在 controller 一樣有兩塊,點 edit 是走上面 get,點 save 是走下面 post
get 是透過主鍵(那個流水號)只到要的資料,網址上有寫他抓的流水號,因為之前在講路由時就有提到網址最後面試可有可無的 id,就是這個id
// GET: News/Edit/5
public async Task<IActionResult> Edit(Guid? id) //NewsId (流水號)型別是 Guid
{
if (id == null)
{
return NotFound();
}
var news = await _context.News.FindAsync(id); //透過 id 找這筆資料
if (news == null)
{
return NotFound();
}
return View(news); //傳給 Views/News/Edit.cshtml
}
namespace Kcg.Dtos
{
public class NewsEditDto
{
public Guid NewsId { get; set; }
public string Title { get; set; }
public string Contents { get; set; }
public int DepartmentId { get; set; }
public DateTime StartDateTime { get; set; }
public DateTime EndDateTime { get; set; }
public Boolean Enable { get; set; }
}
}
回到 controller,我們不用取全部資料,所以改成這個
// GET: News/Edit/5
public async Task<IActionResult> Edit(Guid? id)
{
if (id == null)
{
return NotFound();
}
//var news = await _context.News.FindAsync(id); //改掉
var news = await (from a in _context.News //改成這個
where a.NewsId == id
select new NewsEditDto
{
EndDateTime = a.EndDateTime,
NewsId = a.NewsId,
StartDateTime = a.StartDateTime,
Title = a.Title,
Contents = a.Contents,
DepartmentId = a.DepartmentId
}).SingleOrDefaultAsync();
if (news == null)
{
return NotFound();
}
return View(news);
}
! 注意 ! 因為使用 DTO,所以 Views/News/Edit.cshtml 的 model 要改
@model NewsEditDto
現在 DepartmentId 只是代號,想要他到 Department 資料表找對應的資料,可是現在 controller 已經接收 DTO 的資料了,這時可以用 ViewModels (controller 傳到 view 的 model)
不用之前的 join 是因為 ViewModels 比較好,重要資料最好用這個
namespace Kcg.ViewModels
{
public class NewsEditViewModel
{
public NewsEditDto News { get; set; } //抓 DTO(News) 資料
public List<Department> Departments { get; set; } //抓 Department 資料
}
}
回 controller,抓資料的程式碼要改
! 注意 ! 要 using Kcg.ViewModels
// GET: News/Edit/5
public async Task<IActionResult> Edit(Guid? id)
{
if (id == null)
{
return NotFound();
}
var NewsEditViewModel = new NewsEditViewModel(); //使用 NewsEditViewModel
NewsEditViewModel.News = await (from a in _context.News //取 News 資料
where a.NewsId == id
select new NewsEditDto
{
EndDateTime = a.EndDateTime,
NewsId = a.NewsId,
StartDateTime = a.StartDateTime,
Title = a.Title,
Contents = a.Contents,
DepartmentId = a.DepartmentId,
Enable = a.Enable
}).SingleOrDefaultAsync();
NewsEditViewModel.Departments = await _context.Department.ToListAsync(); //取 Departments 資料
if (NewsEditViewModel.News == null)
{
return NotFound();
}
return View(NewsEditViewModel);
}
! 注意 ! Views/News/Edit.cshtml 的 model 從 NewsEditDto 改成 NewsEditViewModel
! 再注意 ! Views/_ViewImpots.cshtml 要加 NewsEditViewModel
@model NewsEditViewModel
@using Kcg.ViewModels
Edit.cshtml 的 model 改好後下面欄位要改,都加上 News.,兩層
最後是下拉式選單(html),寫在 Edit.cshtml
<div class="form-group">
<label asp-for="News.DepartmentId" class="control-label"></label>
<select name="News.DepartmentId" class="form-select"> //選單
@foreach(var temp in Model.Departments) //Departments 資料表有幾筆資料就有幾個選項
{
<option value="@temp.DepartmentId">@temp.Name</option> //使用者看到的是 Departments.Name,系統回傳的值是 Departments.DepartmentId
}
</select>
<span asp-validation-for="News.DepartmentId" class="text-danger"></span>
</div>
剛剛動的都是 Get(上面區塊),現在要改 Post(下面區塊)
首先改 Bind,他有一長串,不好的地方上面有講過
public async Task<IActionResult> Edit(Guid id, NewsEditDto news)
{
}
public async Task<IActionResult> Edit(Guid id, NewsEditDto news)
{
if (id != news.NewsId)
{
return NotFound();
}
if (ModelState.IsValid) //edit 欄位有效
{
var update = _context.News.Find(news.NewsId); //利用 NewsId 找到這筆資料
if (update != null) //有沒有這筆資料
{
//更新欄位
update.Title = news.Title;
update.Contents = news.Contents;
update.DepartmentId = news.DepartmentId;
update.StartDateTime = news.StartDateTime;
update.EndDateTime = news.EndDateTime;
update.Enable = news.Enable;
//內部給定
update.UpdateEmployeeId = 1;
update.UpdateDateTime = DateTime.Now;
await _context.SaveChangesAsync(); //儲存修改
return RedirectToAction(nameof(Index)); //返回 News/Index.cshtml 頁面
}
/*try 上面的 update 已經排除沒有這筆資料的狀況了,所以這 try catch 可以刪掉
{
_context.Update(news);
await _context.SaveChangesAsync();
}
catch (DbUpdateConcurrencyException)
{
if (!NewsExists(news.NewsId))
{
return NotFound();
}
else
{
throw;
}
}
return RedirectToAction(nameof(Index));*/
}
return View(news); //欄位無效就繼續待在這,資料也還在
}