顯示具有 MVP2019 標籤的文章。 顯示所有文章
顯示具有 MVP2019 標籤的文章。 顯示所有文章

2020年2月26日 星期三

ASP.NET Core Blazor 使用網站跟目錄與其他目錄的靜態圖片使用練習

ASP.NET Core Blazor 使用網站跟目錄與其他目錄的靜態圖片使用練習

更多關於 Blazor 教學影片,可以參考 Blazor 教學影片播放清單 或者 Blazor 快速體驗教學影片撥放清單。也歡迎訂閱本 .NET / Blazor / Xamarin.Forms 影片頻道 。


在這篇文章中,將要來練習如何在 Blazor 專案內,顯示專案中的靜態圖片檔案;這些靜態的圖片檔案可以存在於該專案的網站根目錄下,也可以另外建立一個方案資料夾,宣告這個這料夾內可以儲存任何網站的靜態檔案,這也包括了圖片檔案。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

使用預設專案範本建立 Blazor 專案

想要進行這樣的專案開發練習,可以參考底下的操作步驟
  • 打開 Visual Studio 2019 開發工具
  • 當 [Visual Studio 2019] 對話窗出現之後,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗內,請找出 [Blazor 應用程式] 這個專案開發範本,並且點選這個專案開發範本
  • 請點選右下角 [下一步] 按鈕
  • 出現 [設定新的專案] 對話窗,輸入適當的 [專案名稱] 、 [位置] ,完成後,請點選右下角 [建立] 按鈕
    在這個範例程式碼中,將會建立一個 BlazorOutsideImage 專案名稱
  • 此時將會看到 [建立新的 Blazor 應用程式] 對話窗,這裡可以根據當時開發專案的需要,自行決定是否有調整 Blazor 專案的其他特性,若無,請點選右下角的 [建立] 按鈕
  • 此時,這個 Blazor 專案已經建立完成

建立相關方案資料夾與複製圖片檔案

現在,將會準備三個圖片檔案: blazor-webassembly.png 、 blazor-server.png 、 JavaScriptInterop.png,完成後的結果將會如同下面螢幕截圖。
  • 滑鼠右擊這個專案節點
  • 選擇 [加入] > [新增資料夾] 選項
  • 使用 StaticFilesFolder 名稱作為該方案資料夾的名稱
  • 滑鼠右擊 [wwwroot] 這個特殊資料夾
  • 選擇 [加入] > [新增資料夾] 選項
  • 使用 Images 名稱作為該方案資料夾的名稱
  • 接著,將圖片檔案 blazor-webassembly.png 複製到 [wwwroot] > [Images] 資料夾內
  • 將圖片檔案 blazor-server.png 複製到 [wwwroot] 資料夾內
  • 將圖片檔案 JavaScriptInterop.png 複製到 [StaticFilesFolder] 目錄下
  • 記得要將 JavaScriptInterop.png 這個圖片檔案,在方案總管的屬性視窗內,宣告 [建置動作] 欄位的值為 [永遠複製]
    若沒有宣告相關圖片檔案的 [建置動作] 欄位的值為 [永遠複製],則將無法在網頁上看到這些圖片

修正 Startup.cs

  • 請打開 Startup.cs 這個檔案
  • 找到 Configure
  • 在 app.UseStaticFiles(); 敘述下面,加入這段程式碼
    // 這裡加入底下 Middleware 中介軟體 宣告,在這個專案內新增一個檔案提供者指向 /StaticFiles 目錄
    app.UseStaticFiles(new StaticFileOptions()
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), "StaticFilesFolder")),
        RequestPath = "/StaticFiles"
    });
現在,這個專案可以使用這個 URL /StaticFiles 來指向任何在 [StaticFilesFolder] 方案資料夾內的靜態檔案了
底下的程式碼將會是完成的結果
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/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();
    // 這行不能刪除,因為這會指向專案內的 wwwroot 目錄下
    app.UseStaticFiles();
    // 這裡加入底下 Middleware 中介軟體 宣告,在這個專案內新增一個檔案提供者指向 /StaticFiles 目錄
    app.UseStaticFiles(new StaticFileOptions()
    {
        FileProvider = new PhysicalFileProvider(
            Path.Combine(Directory.GetCurrentDirectory(), "StaticFilesFolder")),
        RequestPath = "/StaticFiles"
    });

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}

關於 ASP.NET Core 中的檔案提供者

ASP.NET Core 透過使用檔案提供者,將檔案系統存取抽象化,可以參考 關於 ASP.NET Core 中的檔案提供者 得到更多詳細說明

修改 Index.razor Blazor 元件

現在,要在 Index.razor 元件中,來顯示這些靜態圖片
  • 在 Pages 資料夾內找到 Index.razor 檔案
  • 打開這個檔案,使用底下程式碼進行替換
@page "/"

<h1>Hello, world!</h1>

<h2>網站根目錄 /blazor-server.png</h2>
<div>
    <img src="/blazor-server.png" />
</div>

<h2>網站根目錄下的目錄 /Images/blazor-webassembly.png</h2>
<div>
    <img src="/Images/blazor-webassembly.png" />
</div>

<h2>非網站根目錄,這裡是另外宣告的 StaticFilesFolder 目錄 /StaticFiles/JavaScriptInterop.png</h2>
<div>
    <img src="/StaticFiles/JavaScriptInterop.png" />
</div>
@code
{

}

查看執行結果

好的,可以來執行這個專案
當這個 Blazor 網站跑起來之後,就會看到如上面螢幕截圖,這三個圖片都正常顯示在網頁上了。

2020年2月23日 星期日

Blazor Server 的不正確使用者登入驗證做法 - 透過 Blazor 元件

Blazor Server 的不正確使用者登入驗證做法 - 透過 Blazor 元件

更多關於 Blazor 教學影片,可以參考 Blazor 教學影片播放清單 或者 Blazor 快速體驗教學影片撥放清單。也歡迎訂閱本 .NET / Blazor / Xamarin.Forms 影片頻道 。


當要進行 Blazor Server 專案開發的時候,若想要設計具有使用者身分驗證的功能,很理所當然的會想說,在 Blazor 專案內建立一個 Blazor 元件,在該元件上加入登入用的帳號與密碼與登入按鈕,想說這樣就可以了,不過,這可能會帶來問題,在這篇文章中將會來說明所會遇到的問題。在這裡的開發練習專案中,將會試圖想要設計一個使用 Cookie 的使用者身分驗證的功能。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

使用預設專案範本建立 Blazor 專案

想要進行這樣的專案開發練習,可以參考底下的操作步驟
  • 打開 Visual Studio 2019 開發工具
  • 當 [Visual Studio 2019] 對話窗出現之後,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗內,請找出 [Blazor 應用程式] 這個專案開發範本,並且點選這個專案開發範本
  • 請點選右下角 [下一步] 按鈕
  • 出現 [設定新的專案] 對話窗,輸入適當的 [專案名稱] 、 [位置] ,完成後,請點選右下角 [建立] 按鈕
    在這個範例程式碼中,將會建立一個 BlazorSignInAsync 專案名稱
  • 此時將會看到 [建立新的 Blazor 應用程式] 對話窗,這裡可以根據當時開發專案的需要,自行決定是否有調整 Blazor 專案的其他特性,若無,請點選右下角的 [建立] 按鈕
  • 此時,這個 Blazor 專案已經建立完成

修正 Startup.cs

  • 請打開 Startup.cs 這個檔案
  • 找到 ConfigureServices 方法
  • 在該方法的最後面加入底下程式碼
#region 加入使用 Cookie 認證需要的宣告
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddAuthentication(
    CookieAuthenticationDefaults.AuthenticationScheme)
    .AddCookie();
#endregion
這裡將會註冊 IHttpContextAccessor 這個服務,因為,想要能夠在 Blazor 元件內來使用 HttpContext 這個物件
另外,也會啟用 Cookie 身分認證功能
  • 找到 Configure 方法
  • 在 app.UseStaticFiles(); 敘述下面,加入這段程式碼
app.UseAuthentication();

設計一個登入 Blazor 元件 Component

  • 滑鼠右擊這個專案節點內的 [Pages] 資料夾
  • 選擇 [加入] > [新增項目] 選項
  • 在 [新增項目] 對話窗內,選擇 [Razor 元件] 選項
  • 在下方 [名稱] 欄位內,輸入 BlazorLoginLogout.razor
  • 點選 [新增] 按鈕,完成這個登入 Razor 頁面的建立
  • 使用底下的 Razor 程式碼,替換掉產生出來的程式碼
@using Microsoft.AspNetCore.Identity;
@using Microsoft.AspNetCore.Http
@using Microsoft.AspNetCore.Authentication
@using System.Security.Claims
@using Microsoft.AspNetCore.Authentication.Cookies

@inject IHttpContextAccessor HttpContextAccessor
@inject NavigationManager NavigationManager

@page  "/BlazorAuth"

<h3>Blazor 的登入與登出身分驗證</h3>

<div class="row">
    <div class="col-md-12 bg-light">
        <EditForm Model="@userModel" OnValidSubmit="@LoginAsync">
            <div class="form-group">
                <label>帳號: </label>
                <input @bind-value="userModel.Email" class="form-control" />
            </div>
            <div class="form-group">
                <label>密碼: </label>
                <input type="password" @bind-value="userModel.Password" class="form-control" />
            </div>
            <div class="form-group">
                <button type="submit" class="btn btn-primary">登入</button>
            </div>
        </EditForm>
    </div>
</div>

@code {
    public UserModel userModel { get; set; } = new UserModel();

    public async Task LoginAsync()
    {

        var claims = new List<Claim>()
        {
            new Claim(ClaimTypes.Name, userModel.Email),
            new Claim("UserID", "123")
        };

        var claimsIdentity = new ClaimsIdentity(claims, "myTest");
        var principal = new ClaimsPrincipal(claimsIdentity);

        await HttpContextAccessor.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme,
                        principal,
                        new AuthenticationProperties
                        {
                            ExpiresUtc = DateTime.UtcNow.AddMinutes(20),
                            IsPersistent = false,
                            AllowRefresh = true
                        });
        NavigationManager.NavigateTo("/");
    }

    public class UserModel
    {
        public string Email { get; set; }
        public string Password { get; set; }
    }
}
首先,在該 Razor 元件的最前面,將會使用 @using 這樣的語法,加入這個 Blazor 元件內將會使用到各個 API 用到的命名空間,另外,也會使用 @inject 語法來使用相依注入 Dependency Injection 容器服務 Container Server (DI Container)來注入 IHttpContextAccessor & NavigationManager 這兩個服務物件到這個 Razor 元件內。
對於 HTML 標記內容而言,則是非常簡單的使用這個 Blazor 內建的 EditForm 元件,來設計出一個輸入帳號與密碼的頁面,當使用者點選了 [登入] 按鈕之後,便會觸發 EditForm 元件中的 OnValidSubmit 事件參數,也就是會執行 LoginAsync 這個方法。
在 LoginAsync 方法內,將不會檢查 帳號與密碼是否正確,因為,這裡只是要進行使用者的身分驗證執行程序,因此,將會建立起一個 ClainsPrincipal 這個 principal 物件,接者,透過相依注入的 HttpContextAccessor 物件,便可以取得 HttpContext 物件;因為已經加入了 Microsoft.AspNetCore.Authentication 命名空間參考,所以,便可以呼叫 SignInAsync 這個方法,來進行新的使用者登入作業。

修改 Index.razor Blazor 元件

現在,要在 Index.razor 元件中,來顯示這些靜態圖片
  • 在 Pages 資料夾內找到 Index.razor 檔案
  • 打開這個檔案,使用底下程式碼進行替換
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<BlazorLoginLogout/>

查看執行結果

好的,可以來執行這個專案
請在帳號與密碼欄位中隨意輸入任何文字,並且點選 登入 按鈕,將會看到如下面截圖的最下方的錯誤訊息。
想要知道發生了甚麼問題,可以先按下 [F12] ,啟動開發人員工具。
接著,按下 [F5] 按鈕,重新整理這個網頁,便可以重新輸入任意文字到帳號與密碼欄位上,而後再一次點選 登入按鈕,現在,便可以在開發人員工具視窗中,看到底下的錯誤訊息。
Error: System.InvalidOperationException: Headers are read-only, response has already started.

   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException()

   at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value)

   at Microsoft.AspNetCore.Http.ResponseCookies.Append(String key, String value, CookieOptions options)

   at Microsoft.AspNetCore.Authentication.Cookies.ChunkingCookieManager.AppendResponseCookie(HttpContext context, String key, String value, CookieOptions options)

   at Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler.HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)

   at Microsoft.AspNetCore.Authentication.AuthenticationService.SignInAsync(HttpContext context, String scheme, ClaimsPrincipal principal, AuthenticationProperties properties)

   at BlazorSignInAsync.Pages.BlazorLoginLogout.LoginAsync() in D:\Vulcan\Projects\BlazorSignInAsync\BlazorSignInAsync\Pages\BlazorLoginLogout.razor:line 47

   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)

   at Microsoft.AspNetCore.Components.Forms.EditForm.HandleSubmitAsync()

   at Microsoft.AspNetCore.Components.ComponentBase.CallStateHasChangedOnAsyncCompletion(Task task)

   at Microsoft.AspNetCore.Components.RenderTree.Renderer.GetErrorHandledTask(Task taskToHandle)
從例外異常的訊息中 Error: System.InvalidOperationException: Headers are read-only, response has already started. 可以明確的看出來,現在這個樣的 Blazor 使用者登入身分驗證的方式(使用 Blazor 元件與 SignInAsync 方法) 所設計出來的登入做法,是不成功的。

2020年1月10日 星期五

ASP.NET Core Blazor SynchronizationContext 同步內容

ASP.NET Core Blazor SynchronizationContext 同步內容

更多關於 Blazor 教學影片,可以參考 Blazor 教學影片播放清單 或者 Blazor 快速體驗教學影片撥放清單。也歡迎訂閱本 .NET / Blazor / Xamarin.Forms 影片頻道 。


在這篇文章中,將要來實際體驗一下,Blazor 專案內的 同步內容 SynchronizationContext 使用注意事項與正確使用方式。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載
現在,將在 Blazor 專案內建立一個 Razor 元件,該元件的 HTML 宣告標記與 C# 程式碼將會如下:
在這個 Razor 元件中,將會宣告三個按鈕,分別為
@using System.Threading
<h3>Blazor 有無使用同步內容之研究</h3>

<button class="btn btn-primary" @onclick="UsingBlazorEvent">使用 Blazor 內建事件機制</button>
<button class="btn btn-outline-danger" @onclick="UsingExternalThread">使用外部執行緒</button>
<button class="btn btn-success" @onclick="UsingExternalThreadBySynchronizationContext">使用外部執行緒但透過同步內容</button>
<div class="display-4 text-warning">
    @Message
</div>

@code {
    public string Message { get; set; }
    async void UsingBlazorEvent()
    {
        Message = "使用 Blazor 內建事件機制更新內容";
    }
    async void UsingExternalThread()
    {
        await Task.Run(() =>
        {
            Message = "不使用同步內容來透過外部執行緒更新內容";
            StateHasChanged();
        });
    }
    async void UsingExternalThreadBySynchronizationContext()
    {
        await Task.Run(async () =>
        {
            await InvokeAsync(() =>
            {
                Message = "使用同步內容來透過外部執行緒更新內容";
                StateHasChanged();
            });
        });
    }
}
  • 使用 Blazor 內建事件機制
    當這個按鈕被點選之後,將會立即更新 Message 這個屬性值內容,因為該屬性有綁定到 HTML 上,因此,將會透資料綁定的運作機制,重新更新轉譯樹 Render Tree 的內容,並且把有差異的地方,傳送到瀏覽器端來更新 DOM 內容,也就會造成瀏覽器的畫面有所更新。
  • 使用外部執行緒
    當點選這個按鈕之後,將會透過 Task.Run 方法,產生一個新的執行緒,使用非同步的方式來更新 Message 這個屬性的內容值,由於當更新這個屬性值的時候,是在另外一個執行緒下,而不是在 SynchronizationContext 同步內容 下來執行,因此,當執行了 StateHasChanged(); 方法要來更新轉譯樹的時候,就會產生底下的錯誤訊息。
System.InvalidOperationException: 'The current thread is not associated with the Dispatcher. Use InvokeAsync() to switch execution to the Dispatcher when triggering rendering or component state.'
  • 使用外部執行緒但透過同步內容
    正確的做法則是,當要更新資料綁定的 UI 內容的時候,記得要使用 InvokeAsync 方法,讓指定的委派方法可以在 SynchronizationContext 下來執行,這樣,就不會發生問題了。


2020年1月2日 星期四

Blazor 專案中,使用 Scaffold 身分識別修改登入頁面成為中文版

Blazor 專案中,使用 Scaffold 身分識別修改登入頁面成為中文版

更多關於 Blazor 教學影片,可以參考 Blazor 教學影片播放清單 或者 Blazor 快速體驗教學影片撥放清單。也歡迎訂閱本 .NET / Blazor / Xamarin.Forms 影片頻道 。


在這篇文章中,將會嘗試建立一個 Blazor Server side 的專案,並且該專案具有 個別使用者帳戶 身分驗證機制,接著,將會嘗試要來擴增選擇性地新增包含在身分識別 Razor 類別庫(RCL)中的原始程式碼,而在此,將會嘗試把 Login.cshtml 這個 Razor Page 頁面進行還原與客製。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

使用預設專案範本建立 Blazor 專案

想要進行這樣的專案開發練習,可以參考底下的操作步驟
  • 打開 Visual Studio 2019 開發工具
  • 當 [Visual Studio 2019] 對話窗出現之後,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗內,請找出 [Blazor 應用程式] 這個專案開發範本,並且點選這個專案開發範本
  • 請點選右下角 [下一步] 按鈕
  • 出現 [設定新的專案] 對話窗,輸入適當的 [專案名稱] 、 [位置] ,完成後,請點選右下角 [建立] 按鈕

    🕬 說明

    在這個範例程式碼中,將會建立一個 BlazorScaffoldIdentity 專案名稱
    #
  • 此時將會看到 [建立新的 Blazor 應用程式] 對話窗
  • 在該對話窗的右上方找到 [驗證] 文字,點選該文字下方的 [變更] 連結
  • 現在將會看到 [變更驗證] 這個對話窗
  • 請選擇 [個別使用者帳戶] 選項
  • 中間將會出現預設 [儲存應用程式的使用者帳戶] 下拉清單選項,使用預設值即可
  • 點選右下角的 [確定] 按鈕,關閉 [變更驗證] 這個對話窗
  • 此時回到了 [建立新的 Blazor 應用程式] 對話窗
  • 在該對話窗的右上角部分,將會出現 [驗證] 需求採用 [個別使用者帳戶]
  • 請點選右下角的 [建立] 按鈕
  • 此時,這個 Blazor 專案已經建立完成

🔑 注意事項

在建立練習專案的時候,物必須要在 [建立新的 Blazor 應用程式] 對話窗下,點選該對話窗右上方的 [驗證] 設定選項,並且要切換成為 請選擇 [個別使用者帳戶] 選項
#

執行並且查看登入畫面

  • 請開始執行這個具有身分識別的 Blazor 專案
  • 將會顯示底下螢幕截圖的網頁畫面
  • 請點選該網頁右上方的 [Log in] 連結文字
  • 此時,將會看到這個 Blazor 專案的登入頁面
    從這個登入頁面中,看到的是完全英文的畫面,而當想要修改或者擴增這個頁面的時候,將會發現在這個 Blazor 專案內並沒有相關的程式碼可以來做到這件事情
  • 請展開這個專案內的 [Areas] 資料夾內的其他資料夾,將會看到如下面螢幕截圖,確定沒有看到任何關於 Login 這個 Razor Page 頁面存在,當然,也就無法針對登入頁面做出任何修正

產生 Scaffold 身分識別 程式碼

  • 滑鼠右擊該專案節點
  • 選擇 [新增] > [新增 Scaffold 項目]
  • 現在將會看到 [新增 Scaffold 項目] 這個對話窗
  • 請點選左邊的 [識別] 標籤頁次,中間將會出現預設 [識別] 選項
  • 最後,請點選 [加入] 按鈕
  • 請等候安裝需要用到的 NuGet 套件,便會出現如下圖的 [新增 識別] 對話窗
  • 請在 [新增 識別] 對話窗 勾選 [Account\Login] 對話窗
  • 在 [資料內容類別] 下拉選單中,點選該下拉選單,選擇 ApplicationDbContext (BlazorScaffoldIdentity.Data) 這個選項
  • 完成後,請點選右下角的 [新增] 按鈕
  • 等候一段時間,相關程式碼就會產生出來
  • 下面螢幕截圖,是產生後的方案總管內容
    並且將會看到 [ScaffoldingReadMe.txt] 這個檔案產生,內容如下
Support for ASP.NET Core Identity was added to your project
- The code for adding Identity to your project was generated under Areas/Identity.

Configuration of the Identity related services can be found in the Areas/Identity/IdentityHostingStartup.cs file.


The generated UI requires support for static files. To add static files to your app:
1. Call app.UseStaticFiles() from your Configure method

To use ASP.NET Core Identity you also need to enable authentication. To authentication to your app:
1. Call app.UseAuthentication() from your Configure method (after static files)

The generated UI requires MVC. To add MVC to your app:
1. Call services.AddMvc() from your ConfigureServices method
2. Call app.UseRouting() at the top your Configure method, and UseEndpoints() after authentication:
    app.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
        endpoints.MapRazorPages();
    });

Apps that use ASP.NET Core Identity should also use HTTPS. To enable HTTPS see https://go.microsoft.com/fwlink/?linkid=848054.
與這個專案最初產生的時候相比,在 [Area] > [Identity] 目錄下,多了一個 [IdentityHostingStartup.cs] 檔案,該檔案的內容如下
using System;
using BlazorScaffoldIdentity.Data;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

[assembly: HostingStartup(typeof(BlazorScaffoldIdentity.Areas.Identity.IdentityHostingStartup))]
namespace BlazorScaffoldIdentity.Areas.Identity
{
    public class IdentityHostingStartup : IHostingStartup
    {
        public void Configure(IWebHostBuilder builder)
        {
            builder.ConfigureServices((context, services) => {
            });
        }
    }
}
而在 [Area] > [Identity] > [Pages] 目錄下,多了 [_ViewImports.cshtml] 與 [_ViewStart.cshtml] 檔案
底下將會是 [Area] > [Identity] > [Pages] > [_ViewImports.cshtml] 檔案內容
@using Microsoft.AspNetCore.Identity
@using BlazorScaffoldIdentity.Areas.Identity
@using BlazorScaffoldIdentity.Areas.Identity.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
底下將會是 [Area] > [Identity] > [Pages] > [_ViewStart.cshtml] 檔案內容
@{
    Layout = "/Pages/Shared/_Layout.cshtml";
}
在 [Area] > [Identity] > [Pages] > [Account] 目錄下,多了 [_ViewImports.cshtml] 與 [Login.cshtml] 檔案
底下將會是 [Area] > [Identity] > [Pages] > [Account] > [_ViewImports.cshtml] 檔案內容
@using BlazorScaffoldIdentity.Areas.Identity.Pages.Account
而最後這個將會是 [Area] > [Identity] > [Pages] > [Account] > [Login.cshtml] 檔案內容,這也是需要來進行客製化的 Razor Page 頁面檔案
@page
@model LoginModel

@{
    ViewData["Title"] = "Log in";
}

<h1>@ViewData["Title"]</h1>
<div class="row">
    <div class="col-md-4">
        <section>
            <form id="account" method="post">
                <h4>Use a local account to log in.</h4>
                <hr />
                <div asp-validation-summary="All" class="text-danger"></div>
                <div class="form-group">
                    <label asp-for="Input.Email"></label>
                    <input asp-for="Input.Email" class="form-control" />
                    <span asp-validation-for="Input.Email" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <label asp-for="Input.Password"></label>
                    <input asp-for="Input.Password" class="form-control" />
                    <span asp-validation-for="Input.Password" class="text-danger"></span>
                </div>
                <div class="form-group">
                    <div class="checkbox">
                        <label asp-for="Input.RememberMe">
                            <input asp-for="Input.RememberMe" />
                            @Html.DisplayNameFor(m => m.Input.RememberMe)
                        </label>
                    </div>
                </div>
                <div class="form-group">
                    <button type="submit" class="btn btn-primary">Log in</button>
                </div>
                <div class="form-group">
                    <p>
                        <a id="forgot-password" asp-page="./ForgotPassword">Forgot your password?</a>
                    </p>
                    <p>
                        <a asp-page="./Register" asp-route-returnUrl="@Model.ReturnUrl">Register as a new user</a>
                    </p>
                </div>
            </form>
        </section>
    </div>
    <div class="col-md-6 col-md-offset-2">
        <section>
            <h4>Use another service to log in.</h4>
            <hr />
            @{
                if ((Model.ExternalLogins?.Count ?? 0) == 0)
                {
                    <div>
                        <p>
                            There are no external authentication services configured. See <a href="https://go.microsoft.com/fwlink/?LinkID=532715">this article</a>
                            for details on setting up this ASP.NET application to support logging in via external services.
                        </p>
                    </div>
                }
                else
                {
                    <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal">
                        <div>
                            <p>
                                @foreach (var provider in Model.ExternalLogins)
                                {
                                    <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button>
                                }
                            </p>
                        </div>
                    </form>
                }
            }
        </section>
    </div>
</div>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}
對於 [Login.cshtml.cs] 檔案內容如下所示
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.UI.Services;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;

namespace BlazorScaffoldIdentity.Areas.Identity.Pages.Account
{
    [AllowAnonymous]
    public class LoginModel : PageModel
    {
        private readonly UserManager<IdentityUser> _userManager;
        private readonly SignInManager<IdentityUser> _signInManager;
        private readonly ILogger<LoginModel> _logger;

        public LoginModel(SignInManager<IdentityUser> signInManager, 
            ILogger<LoginModel> logger,
            UserManager<IdentityUser> userManager)
        {
            _userManager = userManager;
            _signInManager = signInManager;
            _logger = logger;
        }

        [BindProperty]
        public InputModel Input { get; set; }

        public IList<AuthenticationScheme> ExternalLogins { get; set; }

        public string ReturnUrl { get; set; }

        [TempData]
        public string ErrorMessage { get; set; }

        public class InputModel
        {
            [Required]
            [EmailAddress]
            public string Email { get; set; }

            [Required]
            [DataType(DataType.Password)]
            public string Password { get; set; }

            [Display(Name = "Remember me?")]
            public bool RememberMe { get; set; }
        }

        public async Task OnGetAsync(string returnUrl = null)
        {
            if (!string.IsNullOrEmpty(ErrorMessage))
            {
                ModelState.AddModelError(string.Empty, ErrorMessage);
            }

            returnUrl = returnUrl ?? Url.Content("~/");

            // Clear the existing external cookie to ensure a clean login process
            await HttpContext.SignOutAsync(IdentityConstants.ExternalScheme);

            ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList();

            ReturnUrl = returnUrl;
        }

        public async Task<IActionResult> OnPostAsync(string returnUrl = null)
        {
            returnUrl = returnUrl ?? Url.Content("~/");

            if (ModelState.IsValid)
            {
                // This doesn't count login failures towards account lockout
                // To enable password failures to trigger account lockout, set lockoutOnFailure: true
                var result = await _signInManager.PasswordSignInAsync(Input.Email, Input.Password, Input.RememberMe, lockoutOnFailure: false);
                if (result.Succeeded)
                {
                    _logger.LogInformation("User logged in.");
                    return LocalRedirect(returnUrl);
                }
                if (result.RequiresTwoFactor)
                {
                    return RedirectToPage("./LoginWith2fa", new { ReturnUrl = returnUrl, RememberMe = Input.RememberMe });
                }
                if (result.IsLockedOut)
                {
                    _logger.LogWarning("User account locked out.");
                    return RedirectToPage("./Lockout");
                }
                else
                {
                    ModelState.AddModelError(string.Empty, "Invalid login attempt.");
                    return Page();
                }
            }

            // If we got this far, something failed, redisplay form
            return Page();
        }
    }
}

將 Login 登入畫面,改成為中文版本

現在可以打開 [Login.cshtml] 這個 Razor 檔案,進行修正,改成中文版本
底下是修改後的執行結果