Skip to content

Wednesday1112/asp.net-core-mvc

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

33 Commits
 
 

Repository files navigation

asp.net core mvc 自學紀錄

參考影片

MVC 架構

image
檔案架構長這樣,有什麼 controller 就要有什麼 view 下的資料夾,
image
有什麼 view,controller 就要有什麼函式
image

Program.cs (路由)

pattern 是網址路徑,controller 名字 / view 名字 / ID,有等於的代表胡果是這個就可以省略
圖中 controller = Home 代表 contrler 名字是 Home 就可以省略,後面 view 一樣意思
ID 的問號代表可有可無,沒有打上 ID,網址一樣可用,ID 是錯的也可用
image
下面兩張圖是一樣的網頁(注意網址)
image
image

Database

使用套件:

  • Microsoft.EntityFrameworkCore.SqlServer
  • Microsoft.EntityFrameworkCore.Tools

Data First

把已經建好的資料庫弄到專案裡的 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:使用跟資料庫一樣的大小寫命名,不然可能大小寫會被改成別的風格
image
-NoPluralize:不要加複數s,不然會幫你在命名結尾上加上s
image
-Force:是如果此位置已有相同檔案時覆蓋,就算沒檔案也可以多這個參數

Code First

把專案裡打好的 Model,弄到資料庫
在 Modle 資料夾建立類別
image
打上欄位

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;
}

結果
image

View

ViewData

controller

public IActionResult index()
{
    ViewData["要傳的變數"] = 要傳得值;

    return View();
}

view\controllerName\index

@ViewData["要傳的變數"] //Razor語法,想要在這用c#語法或變數時要加@

ViewBag

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

結果兩個變數被視為同一個
image

方便性

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"] 的值

TempData

用起來跟 ViewData 差不多

public IActionResult index()
{
    TempData["Temp"] = "這是 Temp 設定的值";

    return View();
}

他預設的生命週期是使用過一次才會消失,例如我在另一個 view 使用 @TempData["Temp"],然後先載入 index 頁面(index 的 view 沒有印 TempData),在載入另一個 view,TempData就出來了,但我重新整理後又不見了,因為 TempData 使用過一次就會消失,除非再載入 index 一次
先去 Home 再去 Privacy
image
重新整理
image
但只要加上 TempData.Keep() 就能讓他再存在一次

public IActionResult Privacy()
{
    TempData.Keep("Temp");

    return View();
}

先去 Home 再去 Privacy,不管重新整理幾次他都還在,可是這時候如果我讓另一個頁面也使用 Temp,而且沒有 TempData.Keep(),他就只會出現一次,重新整理或回到 Privacy 都不會出現 Temp 了,除非回到 Home 重新設定

@TempData["Temp"]

View2 頁面
image
重新整理
image
回到 Privacy 頁面
image

架構的執行順序

request > Controller > View > Layout(主版型)

_ViewStart

設定主板型(版型放在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

image
也可以再放一個 _ViewStart 在要改版型的 controller 的 view 資料夾裡,這樣會先執行外面的 _ViewStart,再執行裡面的 _ViewStart,裡面蓋掉外面的,所以最後 Demo 的版型會以裡面的為主
image
不想用 _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>

Razor 基本語法

@{
    string title = "在 @{ } 中可以使用 c# 語法";
    int number = 5;
}
@title : @number

image
if 判斷式

@if(number > 0)
{
    //在這邊還是寫程式碼區塊
    <div>如果今天要在這裡面顯示文字到網頁上,就寫在html標籤裡面即可</div>
    <text>如果怕破壞格式不想用html標籤,可以用text,則不會留下痕跡</text>
}
else
{
    <div>number 小於等於 0</div>
}

text 沒有留下痕跡
image
如果要在畫面上打小老鼠

<div>@@</div>//這樣會顯示一個@

image
或是

@{
    string 小老鼠 = "@"; //可以用中文當變數,超酷
}
<!-- 想要在@後面加文字 -->
<div>@(小老鼠)大老鼠</div>
image

CRUD

藉著範例檔能使用新增、查詢、修改和刪除的功能
image
首先在 Controller 建立新的控制器
image
使用 Kcg 資料庫的 News 資料表
image
然後他會幫我們新增 Views 下的 News 資料夾,裡面直接就有新增、刪除、細節、編輯及顯示五個檔案
image
為了更方便檢視畫面,在上面標提列新增 NewsIndex
image
結果會跟第一張圖一樣,新增、刪除及編輯都會跟資料庫同步

可以發現 Contents 欄位的文字特別長,所以我們讓他只在 Details 顯示
把這邊刪除只是讓他不顯示,但仍然有耗費資源效能抓資料
image image
Controller 的這個寫法代表把 News 所有資料取下來
image
改成一欄一欄篩選

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());

啟動偵錯可以看到 SQL 語法
image

join

現在畫面上的 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寫法,讓他加上強型別
image

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

image
Views/Index 的 model 要改成 NewsDto
image
會需要 using Dtos,放在 Views/Shared/_ViewImports.cshtml 可以省去一直 using 的麻煩,這邊打了,Views 底下其他檔案都有效

@using Kcg.Dtos

image
回到 Index 把欄位名改成剛剛 controller 設定的,以及把該刪的欄位刪掉
image
image
結果圖
image

範例的 create 功能

範例的 create 頁面
image
image
他在 controller 有兩個區塊
在 index 點 create new 用的是上面的,在 create 頁面點 create 是下面的
image
下面的 HttpPost 意思是方法走 Post,要知道是走 Psot 要去網頁的 html 看
image
image
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 檔,一樣有刪掉一些不必要的欄位
image

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 就沒有
image
在有空格的情況下點 Create 按鈕,發現根本沒有 request,沒有進到 controller 的下面區塊,因為前端就擋掉了,在 Views/Create.cshtml 就擋掉了
image

結果

試著建一筆資料
! 注意 ! DepartmentId 欄位的值要有對上 Department 資料表的代號,不然抓不到資料,也就不會顯示有新增一筆資料了(雖然資料庫還是有新增資料,但網頁抓不到)
image

範例的 edit 功能

範例 edit 頁面
image
image 在 controller 一樣有兩塊,點 edit 是走上面 get,點 save 是走下面 post
image
get 是透過主鍵(那個流水號)只到要的資料,網址上有寫他抓的流水號,因為之前在講路由時就有提到網址最後面試可有可無的 id,就是這個id
image

// 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
}

然後一樣用 DTO 刪掉一些欄位
image

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

改完,下面欄位有的要刪掉
image

DepartmentId 欄位下拉式選單

現在 DepartmentId 只是代號,想要他到 Department 資料表找對應的資料,可是現在 controller 已經接收 DTO 的資料了,這時可以用 ViewModels (controller 傳到 view 的 model)
不用之前的 join 是因為 ViewModels 比較好,重要資料最好用這個
image
image

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.,兩層
image
image
最後是下拉式選單(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>

Post 區塊修改

剛剛動的都是 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); //欄位無效就繼續待在這,資料也還在
}

結果

改這筆上次建的資料
image image image

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published