ASP.NET Core MVC 使用者入門示範教學課程 - CRUD Code First

ASP.NET Core MVC 入門教學


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

那這邊有三種方式可以進行修正

  1. 讓 Entity Framework 自動卸除資料庫,並重新依據新的模型類別結構描述來建立資料庫。 在開發週期早期,當您在測試資料庫上進行開發時,這個方法會很方便;其可讓您一併調整模型和資料庫結構描述,更加快速。 不過,它的缺點是您會遺失資料庫中現有的資料 — 因此您不會想在實際執行的資料庫上使用這種方法! 使用初始設定式將測試資料自動植入資料庫,通常是開發應用程式的有效方式。 這是早期開發和使用 SQLite 時的好方法。
  2. 您可明確修改現有資料庫的結構描述,使其符合模型類別。 這種方法的優點是可以保留您的資料。 您可以手動方式或藉由建立資料庫變更指令碼來進行這項變更。
  3. 使用 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; }
    }
}

可以看到每個欄位上加了很多東西,這一些就是驗證的規則,以下節錄官方文件的說明

驗證屬性會指定您想要對套用目標模型屬性強制執行的行為:

RequiredMinimumLength 屬性 (attribute) 指出屬性 (property) 必須是值;但無法防止使用者輸入空格以滿足此驗證。

RegularExpression 屬性則用來限制可輸入的字元。 在上述程式碼中,"Genre":

  • 必須指使用字母。
  • 第一個字母必須是大寫。 允許空白字元,而數位則不允許使用特殊字元。

RegularExpression "Rating":

  • 第一個字元必須為大寫字母。
  • 允許後續空格中的特殊字元和數位。 "PG-13" 對分級而言有效,但不適用於 "Genre"。

Range 屬性會將值限制在指定的範圍內。

StringLength 屬性可讓您設定字串屬性的最大長度,並選擇性設定其最小長度。

實值型別 (如decimalintfloatDateTime) 原本就是必要項目,而且不需要 [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




Copyright © 凱哥寫程式 2022 | Powered by TalllKai ❤