[Day13] C# MVC 驗證與授權,新刪修查按鈕權限 - C#&AspNetCore

在上回 [Day12] C# MVC 驗證與授權,登入與登出 - C#&AspNetCore ,我們介紹了登入與登出。

而這回,將探討的是驗證與授權的細部權限,即所謂的新刪修查權限。
如:某角色是禁止使用特定功能的編輯。

我在之前也有了打篇與這相關的資料表規劃 : 資訊系統的角色權限規劃,含資料表設計

授權

授權是指決定使用者能夠做什麼的處理常式。 例如,系統管理使用者可以建立文件庫、新增檔、編輯檔,以及將它們刪除。

實作

目標,可以讓一個有權限的使用者依照他的權限去限制:檢視、新增、刪除、編輯。

權限代碼:

  • Basic_UserManagement_View
  • Basic_UserManagement_Create
  • Basic_UserManagement_Modify
  • Basic_UserManagement_Delete

使用者擁有的權限代碼:

  • Basic_UserManagement_View
  • Basic_UserManagement_Create
  • Basic_UserManagement_Delete

所以該使用者只能檢視、建立、刪除,並且無法編輯。

在此先列出檔案目錄,以及異動檔案(黃色標記處):

1.首先先建立權限代碼的定義檔案
~/Authorization/Permissions.cs

1
2
3
4
5
6
7
8
9
10
11
12
namespace CookieAuthentication.Authorization
{
// 權限代碼
public static class Permissions
{
// 基本資料管理-人員管理
public const string Basic_UserManagement_View = "Basic_UserManagement_View";
public const string Basic_UserManagement_Create= "Basic_UserManagement_Create";
public const string Basic_UserManagement_Modify = "Basic_UserManagement_Modify";
public const string Basic_UserManagement_Delete = "Basic_UserManagement_Delete";
}
}

說明:
未來如果要新增各種功能代碼都可以從這新增。

2.建立權限的AuthorizationRequirement,要求規格
~/Authorization/PermissionAuthorizationRequirement.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
using Microsoft.AspNetCore.Authorization;
namespace CookieAuthentication.Authorization
{
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
public string[] Permissions { get; set; }

public PermissionAuthorizationRequirement(string[] permissions)
{
Permissions = permissions;
}
}
}

說明:
為了要能夠自訂權限的邏輯,所以需要一個IAuthorizationRequirement幫我們傳遞授權資料的格式。

3.建立權限邏輯的控制器

~/Authorization/PermissionAuthorizationHandler.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
using Microsoft.AspNetCore.Authorization;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CookieAuthentication.Authorization
{
// 權限控制邏輯
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionAuthorizationRequirement requirement)
{
// 如果沒有登入直接返回失敗
var accountClaim = context.User.FindFirst(x => x.Type == "Account");
if (accountClaim == null)
{
context.Fail();
return Task.CompletedTask;
}

// 要求的權限
var requirementPermissions = requirement.Permissions;

// 取得使用者的權限, 這段可以從資料庫撈,在這範例用陣列
List<string> userPermission = new List<string>();
userPermission.Add(Permissions.Basic_UserManagement_View);
userPermission.Add(Permissions.Basic_UserManagement_Delete);
userPermission.Add(Permissions.Basic_UserManagement_Modify);

// 檢查是否使用者具備權限
bool isExist = false;

for (int i = 0; i < userPermission.Count(); i++)
{
for (int j = 0; j < requirementPermissions.Count(); j++)
{
if (userPermission[i] == requirementPermissions[j])
{
isExist = true;
break;
}
}
}
// 如果具備則授予成功
if (isExist)
{
context.Succeed(requirement);
}

return Task.CompletedTask;
}
}
}

說明:
在此繼承了AuthorizationHandler,是為了要撰寫自己的判斷邏輯。
主要在HandleRequirementAsync裡面檢查傳進來的權限與使用者是否匹配,如果是就設定context.Succeed(requirement);不是就設定context.Fail()。並在最後回傳return Task.CompletedTask。
在22~48是關鍵,在這如果連結資料庫就從資料庫抓使用者的權限表來比對即可。
我在24~28先用假資料替代資料庫撈出來的情況。

4.建立過濾器Filter,之後要用來套用在Controller的Action上

~/Authorization/PermissionFilter.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Threading.Tasks;

namespace CookieAuthentication.Authorization
{
public class PermissionFilter:Attribute, IAsyncAuthorizationFilter
{
public string[] permissions { get; set; }

public PermissionFilter(params string[] permissions)
{
this.permissions = permissions;
}

public async Task OnAuthorizationAsync(AuthorizationFilterContext context)
{
var authorizationService = context.HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
var authorizationResult = await authorizationService.AuthorizeAsync(context.HttpContext.User, null, new PermissionAuthorizationRequirement(permissions));
if (!authorizationResult.Succeeded)
{
// 如果授權失敗,設定為未授權
context.Result = new UnauthorizedResult();
}
}
}
}

說明:
重點在21~27,使用authorizationService.AuthorizeAsync去認證權限,就會把資料傳到我們上面建立的PermissionAuthorizationHandler.cs裡面判斷。
如果最後判斷沒權限則設定context.Result = new UnauthorizedResult();表示禁止。

5.增加過濾器服務Startup.cs

~/Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
using CookieAuthentication.Authorization;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
namespace CookieAuthentication
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// 註冊需求和處理常式,套用自訂權限控制器
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

// 從appsettings.json讀取登入逾時設定
double LoginExpireMinute = this.Configuration.GetValue<double>("LoginExpireMinute");

// 建立驗證中介軟體服務
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option =>
{
// 登入逾期設定,如果沒給預設14天
option.ExpireTimeSpan = TimeSpan.FromMinutes(LoginExpireMinute);
// 限制cookie不能延期
option.SlidingExpiration = false;
});

services.AddControllersWithViews(options => {
//CSRF資安有關,這裡就加入全域驗證範圍Filter的話,待會Controller就不必再加上[AutoValidateAntiforgeryToken]屬性
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
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.UseAuthentication(); // 啟用身份驗證
app.UseAuthorization(); // 啟用授權,指的是Controller、Action可加上驗證 [Authorize] 屬性


app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

說明
只有在26行,增加一行,表示套用驗證邏輯的PermissionAuthorizationHandler

1
2
// 註冊需求和處理常式,套用自訂權限控制器
services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();

6.限制頁面上的檢視權限,在Controller增加權限過濾器Filter

~/Controllers/HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[PermissionFilter(Permissions.Basic_UserManagement_View)]
public IActionResult Privacy()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<ul>");

foreach (Claim claim in HttpContext.User.Claims)
{
sb.AppendLine($@"<li> claim.Type:{claim.Type} , claim.Value:{ claim.Value}</li>");
}
sb.AppendLine("</ul>");

ViewBag.msg = sb.ToString();
return View();
}

說明:
添加
[PermissionFilter(Permissions.Basic_UserManagement_View)]
表示允許某權限的代碼進入,在此就是Permissions.Basic_UserManagement_View,表示畫面的檢視權限。

7.限制頁面上的按鈕權限

~/View/Home/Privacy.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@using Microsoft.AspNetCore.Authorization
@using CookieAuthentication.Authorization

@inject IAuthorizationService AuthorizationService
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>

<!-- 按鈕區塊 -->
@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Create }))).Succeeded)
{
<button type="submit">新增</button>
}

@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Modify }))).Succeeded)
{
<button type="submit">編輯</button>
}

@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Delete }))).Succeeded)
{
<button type="submit">刪除</button>
}
<!-- ./按鈕區塊 -->

<!--登出區塊-->
<div>
您的登入資訊↓
</div>
<div>
@Html.Raw(ViewBag.msg)
</div>
<div>
<a href="@Url.Action("Logout","Home")">登出</a>
</div>
<!--./登出區塊-->

說明:
在13~27為限制擁有某些權限的代碼才啟用
舉例:
@if ((await AuthorizationService.AuthorizeAsync(User, null, new PermissionAuthorizationRequirement(new string[] { Permissions.Basic_UserManagement_Modify }))).Succeeded)

8.完成!
該使用者登入後只有:畫面檢視、刪除、新增權限,而沒有編輯權限。
可以見到成功進Privacy頁面,而只有新增、刪除按鈕有顯示,編輯按鈕則因為權限限制沒有被顯示出來。

總結

如此一來權限問題也難不倒了@@
而接下來主要以資料呈現為主題,所以下回是LINQ的資料操作。

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/views?view=aspnetcore-3.1
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/resourcebased?view=aspnetcore-3.1

[Day12] C# MVC 驗證與授權,登入與登出 - C#&AspNetCore

在上回 [Day11] C# MVC傳遞參數的方式 - C#&AspNetCore ,我們介紹了頁面與頁面間的參數傳遞。

而這回,將探討的是驗證與授權,並以實作登入、登出為示範。

此外,對於權限.net core MVC本身就有提供可實作的方法,連結如下:
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/introduction?view=aspnetcore-3.1

驗證與授權

步驟:驗證->授權

  • 驗證 (Authentication):讓系統認得你是誰
    • Cookie-Based驗證
    • Token-Based驗證
    • Idetity 驗證
    • OAuth2 驗證
    • Windows 驗證
  • 授權 (Authorization):讓系統判斷你是否有權限
    • AuthorizeAttribute授權

驗證有很多種沒有好壞之分,以我觀點來說就是:「用官方範例並符合需求的簡單作法」
因為這樣能避免維護問題、要改動也容易些。
在這網頁上是.net core MVC,所以最簡單實作方式就是「Cookie-Based驗證」,未來要使用API時也只要增加「Token-Based驗證」即可。

使用瀏覽器的 Cookie 儲存使用者驗證資訊。
要使用Cookie驗證要小心的是資料不能太多,因為瀏覽器有4096 bytes限制。
(如果是一些舊瀏覽器就會遇到問題,所以要注意資料就是存識別資料即可)
此外,ASP.NET Core Cookie驗證,在前台會以加密方式儲存,避免使用者去修改,也有自動逾期時間,整體上來安全程度是有的。

實作

主要參考 使用 cookie 驗證但不使用 ASP.NET Core Identity[ASP.net Core] 實作 Cookie based 登入機制 這兩篇進行實作,並以7個步驟完成

先列出主要異動的檔案:
(黃色標記處為異動檔案)

1.先增加登入逾期的時間定義值

appsettings.json

1
2
3
4
5
6
7
8
9
10
11
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"LoginExpireMinute": 60, //登入逾期時間
"AllowedHosts": "*"
}
  • 第9:LoginExpireMinute表示幾分後逾期登出。

2.到Startup.cs去增加載入的授權、身份驗證服務

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace CookieAuthentication
{
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}

public IConfiguration Configuration { get; }

// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{

// 從appsettings.json讀取登入逾時設定
double LoginExpireMinute = this.Configuration.GetValue<double>("LoginExpireMinute");

// 建立驗證中介軟體服務
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(option =>
{
// 登入逾期設定,如果沒給預設14天
option.ExpireTimeSpan = TimeSpan.FromMinutes(LoginExpireMinute);
// 限制cookie不能延期
option.SlidingExpiration = false;
});

services.AddControllersWithViews(options => {
// CSRF資安有關,這裡就加入全域驗證範圍Filter的話,待會Controller就不必再加上[AutoValidateAntiforgeryToken]屬性
options.Filters.Add(new AutoValidateAntiforgeryTokenAttribute());
});
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
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.UseAuthentication(); // 啟用身份驗證
app.UseAuthorization(); // 啟用授權,指的是Controller、Action可加上驗證 [Authorize] 屬性


app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
}
}

主要改以下幾點:

  • [26~45]:ConfigureServices追加服務,先取得登入逾期的時間參數LoginExpireMinute。之後追加AddAuthentication服務,並修改AddControllersWithViews內容。
  • [65~66]:啟用身份驗證、授權

3.再來是控制器的部分,主要設定登入、登出、檢視登入後資訊的方能

HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
using CookieAuthentication.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;

namespace CookieAuthentication.Controllers
{

public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
return View();
}

[Authorize]
public IActionResult Privacy()
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("<ul>");

foreach (Claim claim in HttpContext.User.Claims)
{
sb.AppendLine($@"<li> claim.Type:{claim.Type} , claim.Value:{ claim.Value}</li>");
}
sb.AppendLine("</ul>");

ViewBag.msg = sb.ToString();
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}


[HttpPost]
public async Task<IActionResult> Login(string Account, string Password,bool IsRememberMe)
{

if ((Account == "test01" && Password == "test01pwd") == false)
{
ViewBag.errMsg = "帳號或密碼輸入錯誤";
return View("~/Views/Home/Index.cshtml"); // 登入失敗導回頁面
}


// 登入成功,建立驗證 cookie
Claim[] claims = new[] { new Claim("Account", Account) }; // Key取名"Account",方便取出後可以對資料庫做操作
ClaimsIdentity claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);

// 呼叫 SignInAsync 以登入使用者
await HttpContext.SignInAsync(claimsPrincipal,
new AuthenticationProperties()
{
//IsPersistent = false:瀏覽器關閉立馬登出;IsPersistent = true 就變成常見的Remember Me功能
IsPersistent = IsRememberMe,
});

// 導至隱私頁面
return RedirectToAction("Privacy", "Home");
}


public async Task<IActionResult> Logout()
{
await HttpContext.SignOutAsync();
return RedirectToAction("Index", "Home");// 導至登入頁
}
}
}
  • 登入:在56~82,主要是檢查帳密>建立Cookie權杖>在使用SignInAsync以Cookie權杖做登入。
  • 登出:在85~89,很簡單的一行await HttpContext.SignOutAsync();即可。
  • 檢視登入後資訊:在33~47,[Authorize]表示授權才能啟用,主要把登入後的資訊從HttpContext中取出給前端顯示。

5.之後建立登入頁,我這把登入做在Index.cshtml

Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@{
ViewData["Title"] = "Home Page";
}

<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

<!--登入區塊-->
@using (Html.BeginForm("Login", "Home", new { ReturnUrl = Context.Request.Query["ReturnUrl"] }, FormMethod.Post, true, new { name = "loginForm", autocomplete = "off" }))
{
<div>
<label>帳號:</label>@Html.TextBox("Account")
</div>
<div>
<label>密碼:</label>@Html.Password("Password")
</div>
<div>
<label>記住我:</label>@Html.CheckBox("IsRememberMe")
</div>
<div>
<button type="submit">提交</button>
</div>
<div style="color:red;">
<!--顯示登入失敗訊息 -->
@ViewBag.errMsg
</div>
}
<!--./登入區塊-->

6.登入後才可用的頁面,我這把Privacy.cshtml當成是要登入才能進的頁面

Privacy.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@{
ViewData["Title"] = "Privacy Policy";
}
<h1>@ViewData["Title"]</h1>

<p>Use this page to detail your site's privacy policy.</p>

<!--登出區塊-->
<div>
您的登入資訊↓
</div>
<div>
@Html.Raw(ViewBag.msg)
</div>
<div>
<a href="@Url.Action("Logout","Home")">登出</a>
</div>
<!--./登出區塊-->
  • 第16:實作登出,點下去回連接到HomeController.cs的Logout動作

7.完成,看看登入、登出效果。

總結

以這種Cookie-Based驗證方式不用管是DB First還是Code First都可以用,只要簡單做個設定即可!
如果是用Idenity在資料表部分會要符合特定格式,可能對某些已成形的系統不好加入。

最後,在這篇裡面實現了登入、登出,但現在的網站應該還有更細的權限,如:特定功能的新刪修查、某按鈕權限、只能看不能編輯。
因此,在下篇中將會實作更細部分的權限分則。
即以角色為基礎的存取控制(Role-based access control,RBAC)。

(( 這篇文好長

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/security/authorization/introduction?view=aspnetcore-3.1
https://ithelp.ithome.com.tw/articles/10249930
https://blogger.tigernaxo.com/post/dotnetcore31/auth/auth_guild_3/
https://ithelp.ithome.com.tw/articles/10199102
https://ithelp.ithome.com.tw/articles/10198150
https://blog.miniasp.com/post/2019/12/25/asp-net-core-3-cookie-based-authentication
https://dotblogs.com.tw/shadow/2019/01/16/105615
https://docs.microsoft.com/zh-tw/aspnet/core/security/authentication/cookie?view=aspnetcore-3.1

[Day11] C# MVC傳遞參數的方式 - C#&AspNetCore

在上回 [Day10] C# MVC 檢視元件View Compoment使用- C#&AspNetCore ,我們介紹了View Component的做法,算是把整個MVC的View摸透了。

而這回則是要探討如何在這些頁面間傳遞參數。

Model Binder

主要會透過Model Binder來完成傳遞,Model Binder主要是用於將HTTP request資料對應到controller/Action去。
參數可能是string、integer、class、list、array…等

而為什麼能接到呢?就是前面的這些設定

Startup.cs

1
2
3
4
5
6
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});

常見的傳遞參數方法

  1. 從網址傳遞

例:https://localhost:44378/Home?id=1

1
2
3
4
5
public IActionResult Index(int id)
{
Console.Write(id);
return View();
}

後面帶參數即可,以此是id,有多少個帶多少個。

  1. 從form post請求

View頁面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<form asp-action="Create">
<div>
<label asp-for="Name"></label>
<input asp-for="Name" />
<span asp-validation-for="Name"></span>
</div>
<div>
<label asp-for="Age"></label>
<input asp-for="Age" />
<span asp-validation-for="Age"></span>
</div>
<div>
<input type="submit" value="Save" />
</div>
</form>

Controller頁面:

1
2
3
4
5
6
7
8
9
10
11
12
[HttpPost]
public IActionResult Create(UsersModel usersmodel)
{
if (ModelState.IsValid)
{
//如果連結資料庫在這進行儲存

//導回首頁
return RedirectToAction(nameof(Index));
}
return View(usersmodel);
}

基本上就是有特別多資料或固定規格會使用Model去接,不然可以參考以下補充資料。

補充

通常上面的就足夠來撰寫網頁了,但偶而還是會需要取得請求的特定資料
這時還有以下方法可以用:
Header、Request、Query、Form、Route、FormBody

總結

雖然有時會因為避免用戶改資料,把參數傳遞在session storage就是了。
但缺點是他是前台Javascript,要在前台追參數。
最後,當專案大起來時勢必要注意參數的管理,不然A到B頁可能會有各種傳法,在追參數、修改上就會花比較多時間。

預計下回會稍微探討權限部分。

參考資料
https://ithelp.ithome.com.tw/articles/10240381

在網路中減少自己觸及的資訊量

2021年的今天,似乎什麼事都要用透過網路。
但每天都接觸網路上的資訊就會讓人覺得煩躁、被影響。
於是我開始想,有沒有什麼方法”減少資訊量”呢?
因為現在已不可能100%離開網路,像是在台灣工作可能還是要Line。

以下是我想到的作法:

  1. 減少用社群軟體頻率,如7天1次1小時
  2. 不看新聞、論壇
  3. 不看Youtube
  4. 手機換功能型手機
  5. 少用Gxxxxgle搜尋
  6. 瀏覽器ADBLOCK過濾廣告
  7. 轉移注意力到離線的東西

其實,第2~6點都可以靠自己戒掉
只有第1點是現代是必須的,不然就沒朋友了,就是把它當做訊息軟體用,不看動態也不發文。
第4點,則是我們要自己培養的見解能力,不要什麼都靠Gxxxxgle,手動去翻閱實體書或是自己想辦法,十年前的自己可以這樣,現在也一定可以。就是找一套自己離線方法。

然後,可能會覺得不用網路就不能學習,那我問用網路又學到什麼實際能工作的技能…,真正派得上用場的我認為都是內化在自己腦中的知識。

最後,希望可以主動抵制,不要再被演算法強迫推銷了!
一些不相干的事在我們周遭太多了。
關心自己周遭實際碰過的人事物比較重要。

[Day10] C# MVC 檢視元件View Compoment使用- C#&AspNetCore

在上回 [Day9] C# MVC View使用 - C#&AspNetCore ,我們介紹了View的操作及用法,並稍微帶了Partial View。
而這回,我們將介紹檢視元件View Compoment。

因為原本MVC5的@HTML.action在.net core MVC中已經被移除掉了,造成Partial View會無法綁後端Controller撰寫商業邏輯。
因此,要改用View Compoment的做法。

View Compoment

檢視元件與部分檢視類似,但功能更強大。 檢視元件不會使用模型繫結(看你要不要都可以),並且只取決於呼叫它時所提供的資料。

檢視元件:

  • 轉譯區塊,而不是整個回應。
  • 包含控制器與檢視之間的相同關注點分離和可測試性優點。
  • 可以有參數和商務邏輯。
  • 它通常是從配置頁面叫用。
  • 不參與控制器生命週期,無法使用Filter。
  • 非 HTTP 端點的呼叫,不處理Http Request/Response ,ViewComponent是透過程式碼叫用 。

簡言之,可以撰寫商務邏輯。
有一個專門負責這View Component商務邏輯的程式碼。而你在View中也能把參數傳進View Component裡面。

View Compoment建立與使用

照微軟官方文件,View Compoment有限定的資料夾結構,避免維護困難。
黃色標記是等等會新增的檔案。

  • ViewComponents資料夾必須在根目錄下新增這資料夾名稱才可以使用ViewComponents,否則底下的程式碼會出現錯誤。
    • ShowUserInfo.cs是以View Component為名稱命名的業務邏輯管理器。
  • Shared/Components資料夾底下都是以資料夾為名的Component名稱。
    • ShowUserInfo資料夾為View Component名稱,底下有一個Default.cshtml,為Component的視圖。

1.首先先建立業務邏輯的檔案

ShowUserInfo.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;

namespace WebApplication1.ViewComponents
{
public class ShowUserInfo : ViewComponent
{
public async Task<IViewComponentResult> InvokeAsync(string number, string name,string tel)
{
Customer customer = new Customer();
customer.Name = name;
customer.Number = number;
customer.Tel = tel;
return View(customer);
}
}
}
  • IViewComponentResult這段回應照寫就好,算是固定寫法。

在13~19行可以用Model、ViewBag、ViewData,簡單來說就是當你在寫Controller裡的action那樣。
我這邊為了求謹慎,所以回傳的是個強型別Model,因為固定規格比較好維護。

2.再來是View Component的視圖

ShowUserInfo.cshtml

1
2
3
4
5
6
7
8
@model WebApplication1.Models.Customer;

<ul>
<li>Number:@Model.Number</li>
<li>Name:@Model.Name</li>
<li>Tel:@Model.Tel</li>
</ul>

在此就把傳過來的資料顯示即可。

3.實際把View Component套用到View上。

Index.cshtml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@model WebApplication1.Models.Customer;
@using WebApplication1.ViewComponents;
@{
ViewData["Title"] = "Home Page";
}
<ul>
<li>@Model.Number</li>
<li>@Model.Name</li>
<li>@Model.Tel</li>
</ul>
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

// 呼叫View Component
@await Component.InvokeAsync(nameof(ShowUserInfo), new { number = "A007", name = "YS",tel="00-00000000" })

  • @await Component.InvokeAsync用這方法傳遞參數來呼叫View Component顯示。

如此一來就完成了,在網頁上呈現。

總結

整體流程大概是這樣:

  1. 使用者進到View頁面
  2. Controller接受並回應資料給View
  3. View上的Razor語法呼叫ViewComponent
  4. 把參數再丟給ViewComponent處理
  5. 之後結果再回傳給整個View。

感覺這種方式真是爽快,功能又強,減少了很多Controller要顧慮的生命週期。
有獨立區塊用View Component會比用Partial View輕鬆很多。(時代的眼淚 誤~*

參考資料
https://dotblogs.com.tw/shadow/2021/04/10/174425
https://docs.microsoft.com/en-us/aspnet/core/mvc/views/view-components?view=aspnetcore-5.0

[Day9] C# MVC View使用 - C#&AspNetCore

在上回 [Day8] C# MVC Controller控制器使用 - C#&AspNetCore ,我們介紹了Controller的基本用法。

而這回就是使用者畫面了,也就是MVC的View

在這篇中會介紹:

  1. View的Razor語法
  2. 可共用的View 部分檢視Partial View

View的Razor語法

通常在View中看到@出現通常就是Razor語法了。
基本上就是印出值,再來就是一些特定用法。

  • 後端註解
    1
    @*註解*@
  • 輸出@符號
    1
    @@
  • @變數名稱,取值
    1
    @DateTime.Now
  • 陳述式
    1
    @(5*3)
  • 程式碼區塊
    1
    @{ }
  • for用法
    1
    2
    3
    4
    5
    6
    @for (int i = 0; i < 10; i++)
    {
    <text>
    word
    </text>
    }
  • foreach用法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    @{
    string[] fruits = { "apple", "orange", "banana" };
    <ul>
    @foreach (var item in fruits)
    {
    <li>@item</li>
    }
    </ul>
    }
  • 變數
    1
    2
    3
    4
    @{
    var name = "mvc";
    }
    <p>@name 您好</p>

Partial View 部分檢視

在網頁時常會有需要共用的畫面,如側邊欄、頂端橫幅等。
不想要在每個頁面都寫一個,這時就會使用到Partial View把他們切出去。

Partial View前面會有個_底線
可以看到預設建立的MVC專案就有不少Partial View了。

_ViewStart.cshtml
1
2
3
@{
Layout = "_Layout";
}

Layout的進入點_ViewStart.cshtml在此可以選擇預設要套用的版,如一些網站會有前台、後台之分。
如果想把View切成前後台可以參考這篇動態切換適配的Layout:
https://dotblogs.com.tw/stanley14/2016/06/29/202231
其實就是在_ViewStart.cshtml這檔案做些適配而已

再來我們看_Layout.cs,版面前台主View

_Layout.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</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">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">WebApplication1</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-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 flex-sm-row-reverse">
<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="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">
&copy; 2021 - WebApplication1 - <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>
@RenderSection("Scripts", required: false)
</body>
</html>
  • @RenderSection("Scripts", required: false)用來輸出主板中區段的內容。required是指此Section是否一定要有資料顯示,如果設成true時,檢視中沒有定義此Section的值,那網頁就會掛掉。
    子版則可以將下列區段內容輸出到主板
    1
    2
    3
    @section Scripts {
    <script type="text/javascript" src="~/scripts/main.js"></script>
    }
  • @RenderBody()所有被導向主版的view會被載入到@RenderBody

切Partial的方法

我們可以發現_Layout.cs有段nav區塊可以共用,把他切出去吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">WebApplication1</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-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 flex-sm-row-reverse">
<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="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>

之後在_Layout將他引進

_Layout.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>@ViewData["Title"] - WebApplication1</title>
<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />
<link rel="stylesheet" href="~/css/site.css" />
</head>
<body>
<header>
@await Html.PartialAsync("_Nav")
</header>
<div class="container">
<main role="main" class="pb-3">
@RenderBody()
</main>
</div>

<footer class="border-top footer text-muted">
<div class="container">
&copy; 2021 - WebApplication1 - <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>
@RenderSection("Scripts", required: false)
</body>
</html>
  • @await Html.PartialAsync("_Nav") 表示用異步的方式把Partial區塊引進來。使用 HTML 協助程式時,最佳做法是使用 PartialAsync。 PartialAsync 會傳回包裝在 Task 的 IHtmlContent 類型。 方法的參考方式,是在等候的呼叫前面加上 @ 字元。此外,目前以不建議使用同步方式載入了,因為官方說會造成死結。

如此以來就會更專注在每個小區塊。

這時可能有人會問,Partial沒Controller怎麼載資料呢?所以要介紹另個載入的方式。

Partial傳遞資料的方式

我們查看@await Html.PartialAsync("_Nav")的函式多載,可以發現能透過Model方式將資料傳入。

我們建立一個ViewModel叫NavViewModel

1
2
3
4
5
6
7
8
9
10
11
12
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Models
{
public class NavViewModel
{
public string BrandName { get; set; }
}
}

將原本的PartialAsync傳入NavViewModel

_Layout.cshtml
1
@await Html.PartialAsync("_Nav",new NavViewModel() { BrandName = "WebApplication1" })

之後在_Nav.cshtml顯示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@model WebApplication1.Models.NavViewModel;
<nav class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3">
<div class="container">
<a class="navbar-brand" asp-area="" asp-controller="Home" asp-action="Index">
@Model.BrandName
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-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 flex-sm-row-reverse">
<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="Home" asp-action="Privacy">Privacy</a>
</li>
</ul>
</div>
</div>
</nav>
  • @Model.BrandName即可印出Model資料

因為會拉出去獨立寫的通常都是固定規格,所以建議用Model,而不是用弱型別的ViewData、ViewBag方式。

總結

MVC的View真的做到很嚴謹,讓整體資料有固定的方式作呈現,關鍵就在那一個View只能綁一個Model,讓資料減少亂竄的可能性。
而在Partial也是可以透過API方式去自行載入資料。

預計下篇可能還是View,會探討一些常見作法在拆頁面結構上。

參考資料
https://ithelp.ithome.com.tw/articles/10160185
https://sites.google.com/site/yuedumvcbiji/4-view/layout-cshtml
https://docs.microsoft.com/zh-tw/aspnet/core/mvc/views/layout?view=aspnetcore-5.0
https://docs.microsoft.com/zh-tw/aspnet/core/mvc/views/partial?view=aspnetcore-5.0
https://ithelp.ithome.com.tw/articles/10186517
https://ithelp.ithome.com.tw/articles/10205881
https://ithelp.ithome.com.tw/articles/10240368
https://dotblogs.com.tw/stanley14/2016/06/29/202231

[Day8] C# MVC Controller控制器使用 - C#&AspNetCore

在上回 [Day7] C# MVC Model模型連結資料庫 - C#&AspNetCore ,我們把Model連上了資料庫,算是把Model的基礎流程跑完了。

這回就來看Controller吧!

在這篇中主要介紹Controller把資訊傳送給View,有以下方法

  1. ViewData
  2. ViewBag
  3. 多個Model傳給View

控制器(Controller)

負責轉發請求,對請求進行處理。
當使用者進到了前台的視圖(View),對畫面操作所觸發的事件就會跑到控制器。
在控制器裡負責拿到模型(Model)的資料後加上些業務邏輯後返視圖(View)。
負責處理主要商業邏輯處理,資料格式處理,大部分介於Model 與 View 之間,處理資料流的溝通。

建立Controller

1.在Controller資料夾Add > Controller

2.選Empty

3.MVC Controller Empty

4.接下來會產生程式碼,我們增加一行ViewData的程式碼

TestController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Controllers
{
public class TestController : Controller
{
public IActionResult Index()
{
// 使用ViewData
ViewData["Message"] = "Hello World!";
return View();
}
}
}

  • IActionResult:表示動作結果,即使用Post([HttpPost])或Get( [HttpGet])時會使用。
  • ViewData:表示在該View可以使用的Data,為弱型別。通常我們會多建立一個ViewModel作為強型別用,因為可以在編譯時就知道錯誤,上線後、維護也比較穩。ViewData算是有點偷懶的傳資料方式。

接下來我們添加View畫面,Test>Index.cshtml

Index.cshtml
1
2
3
4
5
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@ViewData["Message"]

如此一來就能把資料帶進來了

補充Result種類

JsonResult:用來單純回傳JSON

1
2
3
4
public JsonResult TestJsonResult()
{
return Json(new { data = 5 });
}

結果:

FileResult:用來回傳檔案

1
2
3
4
5
public FileResult TestFileResult()
{
string excelPath = @"C:\example.xlsx";
return File(excelPath, "application/vnd.ms-excel", "example.xlsx");
}

Controller傳多個Model進View的方法

最好作法就是強型別的ViewModel了。

我們建立一個Model,裡面宣告Model類別屬性

TestViewModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Models
{
public class TestViewModel
{
Customer Customer { get; set; }
public List<Product> Products { get; set; }

}
}

TestController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
public class TestController : Controller
{
public IActionResult Index()
{
// 使用ViewData
ViewData["Message"] = "Hello World!";

TestViewModel testViewModel = new TestViewModel();
testViewModel.Customer = new Customer() ;
testViewModel.Customer.Name = "YU HSIANG";
testViewModel.Customer.Number = "A001";
testViewModel.Customer.Tel = "00-00000000";

testViewModel.Products = new List<Product>();
testViewModel.Products.Add(new Product() { Name = "PS4", Number = "P001", Price = 11000 });
testViewModel.Products.Add(new Product() { Name = "PS5", Number = "P002", Price = 15000 });

return View(testViewModel);
}
public JsonResult TestJsonResult()
{
return Json(new { data = 5 });
}


public FileResult TestFileResult()
{
string excelPath = @"C:\example.xlsx";
return File(excelPath, "application/vnd.ms-excel", "example.xlsx");
}
}
}
Index.cshtml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@model WebApplication1.Models.TestViewModel
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@ViewData["Message"]<br>
@Model.Customer.Name<br>
@Model.Customer.Number<br>
@Model.Customer.Tel<br>

<table>
@foreach (Product item in Model.Products)
{
<tr>
<td>@Html.TextBoxFor(m => item.Number)</td>
<td>@Html.TextBoxFor(m => item.Name)</td>
<td>@Html.TextBoxFor(m => item.Price)</td>
</tr>
}
</table>

成功呈現多個Model,並且又是強型別

額外的方式傳值Viewbag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public IActionResult Index()
{
// 使用ViewData
ViewData["Message"] = "Hello World!";

//使用ViewBag
ViewBag.msg = "使用ViewBag的資料";

TestViewModel testViewModel = new TestViewModel();
testViewModel.Customer = new Customer() ;
testViewModel.Customer.Name = "YU HSIANG";
testViewModel.Customer.Number = "A001";
testViewModel.Customer.Tel = "00-00000000";

testViewModel.Products = new List<Product>();
testViewModel.Products.Add(new Product() { Name = "PS4", Number = "P001", Price = 11000 });
testViewModel.Products.Add(new Product() { Name = "PS5", Number = "P002", Price = 15000 });


return View(testViewModel);
}
Index.cshtml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@model WebApplication1.Models.TestViewModel
@*
For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860
*@
@ViewData["Message"]<br>
@ViewBag.msg<br>
@Model.Customer.Name
<br>
@Model.Customer.Number
<br>
@Model.Customer.Tel
<br>

<table>
@foreach (Product item in Model.Products)
{
<tr>
<td>@Html.TextBoxFor(m => item.Number)</td>
<td>@Html.TextBoxFor(m => item.Name)</td>
<td>@Html.TextBoxFor(m => item.Price)</td>
</tr>
}
</table>

結果:

用ViewBag跟ViewData缺點都在弱型別,難以在編譯時就知道錯誤存在。

總結

從這篇中可以知道如何透過Controller傳資料給View。
在下篇預計就是介紹View了。

參考資料
https://ithelp.ithome.com.tw/articles/10240345
https://docs.microsoft.com/zh-tw/aspnet/core/web-api/action-return-types?view=aspnetcore-5.0

[Day7] C# MVC Model模型連結資料庫,使用Entity Framework - C#&AspNetCore

在上回 [Day6] C# MVC Model模型驗證 - C#&AspNetCore ,我們學會利用Model建立驗證機制。
而這回將會介紹如何把Model與真實的資料庫連結再一起。

Entity Framework

Entity Framework (又稱ADO.NET Entity Framework) 是微軟以 ADO.NET 為基礎所發展出來的物件關聯對應 (O/R Mapping) 解決方案。
Entity Framework 利用了抽象化資料結構的方式,將每個資料庫物件都轉換成應用程式物件 (entity),而資料欄位都轉換為屬性 (property),關聯則轉換為結合屬性 (association),讓資料庫的 E/R 模型完全的轉成物件模型,如此讓程式設計師能用最熟悉的程式語言來呼叫存取。

簡言之,就是一個透過物件的方式去操作資料,而不透過原生SQL的作法。
(但我還是比較熟悉原生的SQL,因為換什麼語言寫都通,但在微軟架構下還是用他提供的會比較好)

安裝Entity Framework

安裝:

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

過程:
1.Dependencies> Manage nuget packages

2.安裝Microsoft.EntityFrameworkCore

3.安裝Microsoft.EntityFrameworkCore.SqlServer

4.安裝Microsoft.EntityFrameworkCore.Design

5.安裝Microsoft.EntityFrameworkCore.Tools

使用Entity Framework

Entity Framework Core 有提供兩種開發方式

  • DB First:我已經有現有資料庫了,只是用程式去操作他。
  • Code First:我還沒有資料庫,想用程式碼去建立資料庫。
  • Model First:先定義好模型在產生資料表的方式。

以我在Laravel的架構下寫過的經驗,會覺得Code First會比較好,因為有更動歷程,也比較能知道異動。
而DB First則是把兩者分開來做,優點是很直接,缺點就是異動後很難知道改了哪。

而這回主要以DB First為主。

1.開啟package manager console

2.之後會出現命令列

3.之後把SQL Server打開,看目前資料庫,我們預計把Orders資料庫給整合進來

4.使用Scaffold-DbContext指令,來還原Orders資料庫的Models

1
Scaffold-DbContext "Server=.\SQLExpress;Database=Orders;Trusted_Connection=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models

請自行替換字串的內容:

  • Database:資料庫名稱

之後Models資料夾就會自動產生Model了

再來至Startup.cs添加資料庫連線服務

Startup.cs
1
2
3
4
5
6
7
8
public void ConfigureServices(IServiceCollection services)
{

services.AddControllersWithViews();
// 資料庫配置
var connection = @"Server=.\SQLExpress;Database=Orders;Trusted_Connection=True;ConnectRetryCount=0";
services.AddDbContext<OrdersContext>(options => options.UseSqlServer(connection));
}

再來至控制器HomeController.cs增加方法

HomeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

// 添加Context
private readonly OrdersContext _context;


public HomeController(ILogger<HomeController> logger, OrdersContext context)
{
_logger = logger;
// 初始化Context
_context = context;
}

[HttpGet]
public IActionResult Create()
{
return View();
}

[HttpPost]
public IActionResult Create(UsersModel usersmodel)
{
if (ModelState.IsValid)
{
//如果連結資料庫在這進行儲存

//導回首頁
return RedirectToAction(nameof(Index));
}
return View(usersmodel);
}


public IActionResult Index()
{
// 透過Context取得資料
var model = _context.Customers.Select(b => new Customer
{
Name = b.Name,
Number = b.Number,
Tel = b.Tel
}).ToList();

// 取第一列資料出來
Customer m = model[0];

return View(m);
}

public IActionResult Privacy()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

再來在View上修改

1
2
3
4
5
6
7
8
9
10
11
12
13
@model WebApplication1.Models.Customer;
@{
ViewData["Title"] = "Home Page";
}
<ul>
<li>@Model.Number</li>
<li>@Model.Name</li>
<li>@Model.Tel</li>
</ul>
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

接下來運行即可印出第一筆資料

總結

而現在已經能成功連結資料庫了!
微軟MVC資料庫連結方式比較麻煩,因為要自行找套件,不像Laravel已經配好,直接打帳號密碼就能用。
而現在迫切問題在Controller只能傳送一個Model,要如何讓多個Model能夠透過Controller傳送到View上。
所以,下個篇主要介紹的就是Controller。

參考資料
https://docs.microsoft.com/zh-tw/aspnet/core/data/ef-mvc/intro?view=aspnetcore-5.0
https://docs.microsoft.com/zh-tw/aspnet/core/tutorials/first-mvc-app/working-with-sql?view=aspnetcore-3.1&tabs=visual-studio
https://docs.microsoft.com/zh-tw/ef/core/cli/dotnet#dotnet-ef-dbcontext-scaffold
https://www.youtube.com/watch?v=2m_3SHGy-Rs
http://go.microsoft.com/fwlink/?LinkId=723263
https://www.uuu.com.tw/Public/content/article/20/20200413.htm
http://ikevin.tw/2019/08/04/asp-net-core-3-0-%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-database-first/

[Day6] C# MVC Model模型驗證 - C#&AspNetCore

在上回 [Day5] C# MVC Model模型使用 - C#&AspNetCore ,我們建立了基本的Model,並且示範讓資料藉由Model資料呈現在網頁上。
在這篇主要談如何Model模型上進行驗證。

Model的驗證

為了讓資料進到資料庫是符合我們要的格式,就必須在前面先擋過一層,而這層我們可以透過Model來達到。

常見的驗證屬性

  • [ValidateNever]: ValidateNeverAttribute 指出屬性或參數應該從驗證中排除。
  • [CreditCard]:驗證屬性是否具有信用卡格式。 需要 JQuery 驗證的其他方法。
  • [Compare]:驗證模型中的兩個屬性是否相符。
  • [EmailAddress]:驗證屬性具有電子郵件格式。
  • [Phone]:驗證屬性具有電話號碼格式。
  • [Range]:驗證屬性值是否落在指定的範圍內。像是最大值(Max)、最小值(min)
  • [RegularExpression]:驗證屬性值是否符合指定的正則運算式。
  • [Required]:驗證欄位不是 null。
  • [StringLength]:驗證字串屬性值不超過指定的長度限制。
  • [Url]:驗證屬性是否具有 URL 格式。
  • [Remote]:在伺服器上呼叫動作方法,以驗證用戶端上的輸入。
  • [Display]:顯示名稱為
  • [DataType]:資料型別

添加驗證屬性範例實作

我們先在UsersModel.cs增加屬性
需先添加using System.ComponentModel.DataAnnotations才可使用驗證屬性。

UsersModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;

namespace WebApplication1.Models
{
public class UsersModel
{
[Display(Name = "編號")]
public int ID { get; set; }

[Required(ErrorMessage = "姓名不可為空")]
[Display(Name = "姓名")]
public string Name { get; set; }

[Display(Name = "年齡")]
public int Age { get; set; }
}
}

接著在HomeController.cs裡面個添加一個HttpGet、HttpPost的Create方法
HttpGet是進頁面時會跑的方法
HttpPost是表單觸發發送後會跑的方法

HomeController.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[HttpGet]
public IActionResult Create()
{
return View();
}

[HttpPost]
public IActionResult Create(UsersModel usersmodel)
{
if (ModelState.IsValid)
{
//如果連結資料庫在這進行儲存

//導回首頁
return RedirectToAction(nameof(Index));
}
return View(usersmodel);
}

接著新增一個View頁面,在前台畫面上製作一個form表單。

Create.cshtml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@model WebApplication1.Models.UsersModel
@{
ViewData["Title"] = "Create Page";
}
<form asp-action="Create">
<div>
<label asp-for="Name"></label>
<input asp-for="Name" />
<span asp-validation-for="Name"></span>
</div>
<div>
<label asp-for="Age"></label>
<input asp-for="Age" />
<span asp-validation-for="Age"></span>
</div>
<div>
<input type="submit" value="Save" />
</div>
</form>

注意,asp-validation-for="Name"表示後端回傳的錯誤訊息會傳送到這裡。

然後至網址:https://localhost:44378/Home/Create 查看後
名子不填儲存就會出現錯誤了

在程式碼上會發現跑到HttpPost的Create裡面

自訂驗證

系統預設的驗證肯定不夠,所以就來製作自己的驗證。

我們在UsersModel.cs裡面增加一個CheckValidAge的class並且繼承ValidationAttribute
並覆寫裡面IsValid方法。

UsersModel.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.ComponentModel.DataAnnotations;

namespace WebApplication1.Models
{
public class UsersModel
{
[Display(Name = "編號")]
public int ID { get; set; }

[Required(ErrorMessage = "姓名不可為空")]
[Display(Name = "姓名")]
public string Name { get; set; }

[CheckValidAge]
[Display(Name = "年齡")]
public int Age { get; set; }
}

public class CheckValidAge : ValidationAttribute
{

public CheckValidAge()
{
ErrorMessage = "年齡不可超過50歲";
}

public override bool IsValid(object value)
{
if (value == null)
{
return true;
}
int age = (int)value;
if (age > 50)
{
return false;
}
else
{
return true;
}
}
}
}

如此一來,送出如果>50就會顯示出錯誤。

總結

雖然2021年的今天,大多數人都不使用這種form驗證,而是用API傳送。
因為頁面不用跳轉,用戶也能得到比較好的體驗。

然而,這種傳統的送資料方式還是有存在必要,就是系統追求安全性、穩定性,因為API方式會有比較難管理的問題。

此外,寫到這裡就開始有綁手綁腳的感覺
因為框架不那麼自由的讓你直接寫想寫的程式碼,而是照MVC架構
這就是一個好處,代表維護或後續開發的人就會照同條路走

參考資料
https://ithelp.ithome.com.tw/articles/10240340
https://docs.microsoft.com/zh-tw/aspnet/core/mvc/models/validation?view=aspnetcore-5.0

[Day5] C# MVC Model模型使用 - C#&AspNetCore

在上回 [Day4] C# MVC的程式架構淺談 - C#&AspNetCore ,我們談了整體的架構與檔案。
在這篇主要談如何在 ASP.NET Core MVC 中建立Model模型,並且使用。

Model定義

模型,是一個Class,在MVC裡是放資料的容器,通常會因為以下原因建立:

  1. 為了對應資料庫的table schema(欄位名稱)
  2. 呈現頁面資料的ViewModel

建立Model的過程

1.在Models資料夾 Add > New Item

2.選擇Class,然後自訂Model名稱

3.自己增加欄位ID、Name、Age、Address。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace WebApplication1.Models
{
public class UsersModel
{
public int ID { get; set; }
public string Name { get; set; }
public int Age { get; set; }
}
}

4.完成,建立一個Model就是這麼簡單,再來就是使用這個Model。

使用Model

再來我們透過HomeController使用他。

HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using WebApplication1.Models;

namespace WebApplication1.Controllers
{
public class HomeController : Controller
{
private readonly ILogger<HomeController> _logger;

public HomeController(ILogger<HomeController> logger)
{
_logger = logger;
}

public IActionResult Index()
{
// 建立usersModel物件
UsersModel usersModel = new UsersModel()
{
ID = 1,
Name = "YU HSIANG",
Age = 25
};
// 傳入View
return View(usersModel);
}

public IActionResult Privacy()
{
return View();
}

[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
public IActionResult Error()
{
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
}
}
}

23~31,透過模型Model建立規格,再把資料指派進去,傳進View中。

接下來要在View中使用我們傳進的Model

Index.cshtml
1
2
3
4
5
6
7
8
9
10
11
12
13
@model WebApplication1.Models.UsersModel
@{
ViewData["Title"] = "Home Page";
}
<ul>
<li>@Model.ID</li>
<li>@Model.Name</li>
<li>@Model.Age</li>
</ul>
<div class="text-center">
<h1 class="display-4">Welcome</h1>
<p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>

如此一來就能顯示我們的資料了。

此外,通常會建立ViewModel以對應特定的View畫面。

[補充]View型態主要分為兩種
Type View(強型別)-compiler時就必須過,有intellisense
Dynamic View(動態)-intellisense不會協助,在View具有不同class的model可以使用

總結

知道Model有ViewModel與table schema的用法,能夠使我們更清楚訂定需要的資料規格,讓維護或擴充時有一個良好的先天條件。@@
而在下篇中將會探討Model的資料驗證。

參考資料
https://ithelp.ithome.com.tw/articles/10240268