2019年12月29日 星期日

ASP.NET Core Blazor元件中的參數傳遞與值變化

ASP.NET Core Blazor元件中的參數傳遞與值變化

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


在這篇關於 Blazor 的參數傳遞與使用變化上的文章,將會進行自我解惑相關問題,所以,將會需要建立一個測試專案,將相關問題在這個專案上實際跑看看,以便知道實際執行結果。
也就是說,若一個參數傳遞某個元件A,且該元件A內部也使用了元件B,而元件B也會接收到來自元件A相同的參數,現在的問題是,要顯示元件A的 Blazor 元件,變更了傳遞參數物件的值,對於元件A與元件B內的參數物件,會有甚麼變化呢?另外,若在元件A中變更了參數物件值,對於元件B與要顯示元件A的那個元件,其參數物件又會有甚麼樣子的變化呢?
首先,建立 Blazr 伺服器端的專案範本來建立一個專案,接著,建立一個方案資料夾,並且在這個資料夾內,建立起四個元件,如下圖
對於 HighLevelComponent [高階元件] 這個元件,將會接收一個參數,並且將這個參數顯示在螢幕上
而對於 HighLevelComponentWithInput [高階元件有 input index] 這個元件,將會接收一個參數,並且將這個參數顯示在螢幕上,而且,還會有一個 input 標籤,透過雙向綁定將這個傳遞進來的參數綁定再一起。
對於 LowLevelComponent [低階元件] 這個元件,將會接收一個參數,並且將這個參數顯示在螢幕上,這個元件除了可以在 index.razor 首頁頁面顯示之外,也會用於底下的 [高階元件有 input index] 這個元件內,以便觀察當在傳遞進來的 Index 有變化的時候,會有甚麼影響。
而對於 HighLevelComponentWithLow [高階元件自我包含低階元件] 這個元件,將會接收一個參數,並且將這個參數顯示在螢幕上,而且,還會顯示 LowLevelComponent [低階元件] 這個元件,當然,也會將同樣的參數傳遞到 [低階元件] 這個元件。
並且將這四個元件,實際用於這個專案的 index.razor 元件中,底下是執行效果
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

HighLevelComponent [高階元件] 這個元件

底下是這個元件的 Blazor 程式碼,可以看到這裡使用了 [Parameter] 這個 C# Attribute 屬性,宣告 HighLevelMessage 這個字串變數為一個參數變數,也就是,這個物件值,可以透過 Blazor 參數傳遞的方式進行變更。
另外,這個 HighLevelMessage 參數也會透過單向綁定方式,顯示在 HTML 頁面上
<div class="card">
    <div class="card-body">
        <h5 class="card-title">高階元件</h5>
        <p class="card-text">@HighLevelMessage</p>
    </div>
</div>

@code {
    [Parameter]
    public string HighLevelMessage { get; set; } = "Higher";
}

HighLevelComponentWithInput [高階元件有 input index] 這個元件

底下是這個元件的 Blazor 程式碼,可以看到這裡使用了 [Parameter] 這個 C# Attribute 屬性,宣告 HighLevelComponentWithInputMessage 這個字串變數為一個參數變數,也就是,這個物件值,可以透過 Blazor 參數傳遞的方式進行變更。
另外,這個 HighLevelComponentWithInputMessage 參數也會透過單向綁定方式,顯示在 HTML 頁面上,而且,這個 HighLevelComponentWithInputMessage 參數,也會透過雙向資料綁定的方式,綁定到 input 這個標記上(使用 @bind-value="HighLevelComponentWithInputMessage")
<div class="card">
    <div class="card-body">
        <h5 class="card-title">高階元件有 input</h5>
        <p class="card-text">@HighLevelComponentWithInputMessage</p>
        <div class="card-text m-3">
            <input @bind-value="HighLevelComponentWithInputMessage" @bind-value:event="oninput" />
        </div>
    </div>
</div>

@code {
    [Parameter]
    public string HighLevelComponentWithInputMessage { get; set; } = "Higher";
}

LowLevelComponent [低階元件] 這個元件

底下是這個元件的 Blazor 程式碼,可以看到這裡使用了兩個 [Parameter] 這個 C# Attribute 屬性,宣告 LowLevelMessage 這個字串變數為一個參數變數與 BackgroundColor 這個字串變數,作為要設定用於 css 背景顏色的支用,也就是,這個物件值,可以透過 Blazor 參數傳遞的方式進行變更。
另外,這個 LowLevelMessage 參數也會透過單向綁定方式,顯示在 HTML 頁面上,這個 BackgroundColor 參數也會透過單向綁定方式,套用到 class 屬性的值上
<div class="card @BackgroundColor">
    <div class="card-body">
        <h5 class="card-title">低階元件</h5>
        <p class="card-text">@LowLevelMessage</p>
    </div>
</div>

@code {
    [Parameter]
    public string LowLevelMessage { get; set; } = "Lower";
    [Parameter]
    public string BackgroundColor { get; set; } = "bg-info";
}

HighLevelComponentWithLow [高階元件自我包含低階元件] 這個元件

底下是這個元件的 Blazor 程式碼,可以看到這裡使用了 [Parameter] 這個 C# Attribute 屬性,宣告 HighLevelWithLowMessage 這個字串變數為一個參數變數,也就是,這個物件值,可以透過 Blazor 參數傳遞的方式進行變更。
另外,這個 HighLevelMessage 參數也會透過單向綁定方式,顯示在 HTML 頁面上
不過,在這個元件內,也同時使用了 LowLevelComponent 這個元件,並且使用了 LowLevelMessage="@HighLevelWithLowMessage" BackgroundColor="bg-success" 語法,將引數傳遞到 LowLevelComponent 這個元件內。
<div class="bg-warning p-4">

    <div class="card">
        <div class="card-body">
            <h5 class="card-title">高階元件自我包含低階元件</h5>
            <p class="card-text">@HighLevelWithLowMessage</p>
        </div>
    </div>
    <LowLevelComponent LowLevelMessage="@HighLevelWithLowMessage"
                       BackgroundColor="bg-success" />
</div>

@code {
    [Parameter]
    public string HighLevelWithLowMessage { get; set; } = "Higher";
}

關於 index.razor 這個元件

現在要把剛剛設計的元件,全部整合在一起,在這裡,將會把這個專案範本內的 index.razor 元件內的宣告與程式碼,修改成為如下面列表
@page "/"

<h1 class="display-5 text-danger">Blazor元件中的參數傳遞與值變化</h1>

<div class="my-3">
    <input @bind-value="UserInput" @bind-value:event="oninput" />
</div>

<div class="card-columns">
    <HighLevelComponent HighLevelMessage="@UserInput" />
    <HighLevelComponentWithInput HighLevelComponentWithInputMessage="@UserInput"/>
    <LowLevelComponent LowLevelMessage="@UserInput" />
    <HighLevelComponentWithLow HighLevelWithLowMessage="@UserInput" />
</div>

@code{
    public string UserInput { get; set; } = "Index";
}

進行測試

現在,來看看所關心的問題之執行結果,首先,會看到執行結果的畫面如下圖
首先,將螢幕左上角的 Input 文字輸入盒內的 Index 文字,修改為 [第一次變更],此時,將會看到底下的效果
在這裡,所有接收到來自於 index.razor 內傳送過去的引數,都會完全變更了
現在,請修改左下角的 input 文字輸入盒,將文字輸入盒的內容修改成為 [第2次變更],此時,執行結果如下圖。
在這裡,將會看到,這樣的操作動作,並不會影響到其他的參數物件值,僅有自己會影響到。
最後,請修改左上角的 input 文字輸入盒,將內容修改為 [第三次變更],網頁內容將會變成底下結果
此時,所有的參數都受到了影響,全部變更成為這次修正的內容




2019年12月28日 星期六

Blazor 使用者身分驗證 之 登入與登出

ASP.NET Core Blazor 使用者身分驗證 之 登入與登出

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


在這篇文章中,將會說明如何不使用 ASP.NET Core 上的身分識別 功能 (ASP.NET Core 身分識別是支援使用者介面(UI)登入功能的 API。管理使用者、密碼、設定檔資料、角色、宣告、權杖、電子郵件確認等等。),來實作出登入/登出的效果,並且,也將會說明如何設計使用者有登入與沒有登入的時候,將會看到不同的網頁內容。
在這篇文章所提到的專案原始碼,可以從 GitHub 下載

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

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

    🕬 說明

    在這個範例程式碼中,將會建立一個 BlazorAuthentication 專案名稱
    #
  • 此時將會看到 [建立新的 Blazor 應用程式] 對話窗,這裡可以根據當時開發專案的需要,自行決定是否有調整 Blazor 專案的其他特性,若無,請點選右下角的 [建立] 按鈕
  • 此時,這個 Blazor 專案已經建立完成

🔑 注意事項

在建立練習專案的時候,無須在 [建立新的 Blazor 應用程式] 對話窗下,點選該對話窗右上方的 [驗證] 設定選項,也就是,維持 [驗證] 選項為 [無驗證]
#

加入本專案會用到的 NuGet 套件

  • 滑鼠右擊這個專案節點
  • 選擇 [管理 NuGet 套件] 選項
  • 點選 [瀏覽] 標籤頁次
  • 請記得在這裡一定要勾選 [包括搶鮮版] 選項
  • 從清單中找到 [Microsoft.AspNetCore.Blazor.HttpClient] 項目,請點選這個項目
  • 點選右上方的 [安裝] 按鈕
  • 完成這個 [Microsoft.AspNetCore.Blazor.HttpClient] NuGet 套件的安裝

建立相關方案資料夾

  • 滑鼠右擊這個專案節點
  • 選擇 [加入] > [新增資料夾] 選項
  • 使用 Controllers 名稱作為該方案資料夾的名稱

建立登入與登出的 Razor Page 頁面

建立登入 Razor Page 頁面

  • 滑鼠右擊這個專案節點內的 [Pages] 資料夾
  • 選擇 [加入] > [新增項目] 選項
  • 在 [新增項目] 對話窗內,選擇 [Razor 頁面] 選項
  • 在下方 [名稱] 欄位內,輸入 Login.cshtml
  • 點選 [新增] 按鈕,完成這個登入 Razor 頁面的建立
  • 打開 [Pages] 資料夾內的 [Login.cshtml] 檔案
  • 使用底下 Razor 標記語法替換掉這個檔案內的內容
@page
@model BlazorAuthentication.Pages.LoginModel
@{
    ViewData["Title"] = "登入";
}
<h2>登入</h2>
  • 在 [Pages] 資料夾內,找到 [Login.cshtml] 節點內的 [Login.cshtml.cs]
  • 使用滑鼠雙擊這個 [Login.cshtml.cs] 節點,打開這個檔案內容
  • 使用底下 C# 程式碼替換掉原先這個檔案內的內容
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

#region 這裡將會是新加入的命名空間宣告
using System.Security.Claims;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
#endregion

namespace BlazorAuthentication.Pages
{
    [AllowAnonymous]
    public class LoginModel : PageModel
    {
        public string ReturnUrl { get; set; }
        public async Task<IActionResult> OnGetAsync(string paramUsername, string paramPassword)
        {
            string returnUrl = Url.Content("~/");
            try
            {
                // 清除已經存在的登入 Cookie 內容
                await HttpContext
                    .SignOutAsync(
                    CookieAuthenticationDefaults.AuthenticationScheme);
            }
            catch { }

            #region 這裡將會要針對傳入的使用者帳號與密碼進行驗證

            #region 本練習簡化將不做任何驗證,不過,本練習將簡化不做任何設計

            #endregion

            #region 加入這個使用者需要用到的 宣告類型 Claim Type
            var claims = new List<Claim>
            {
                new Claim(ClaimTypes.Name, paramUsername),
                new Claim(ClaimTypes.Role, "Administrator"),
            };
            #endregion

            #region 建立 宣告式身分識別
            // ClaimsIdentity類別是宣告式身分識別的具體執行, 也就是宣告集合所描述的身分識別
            var claimsIdentity = new ClaimsIdentity(
                claims, CookieAuthenticationDefaults.AuthenticationScheme);
            #endregion

            #region 建立關於認證階段需要儲存的狀態
            var authProperties = new AuthenticationProperties
            {
                IsPersistent = true,
                RedirectUri = this.Request.Host.Value
            };
            #endregion

            #region 進行使用登入
            try
            {
                await HttpContext.SignInAsync(
                CookieAuthenticationDefaults.AuthenticationScheme,
                new ClaimsPrincipal(claimsIdentity),
                authProperties);
            }
            catch (Exception ex)
            {
                string error = ex.Message;
            }
            #endregion

            #endregion

            return LocalRedirect(returnUrl);
        }
    }
}

建立登出 Razor Page 頁面

  • 滑鼠右擊這個專案節點內的 [Pages] 資料夾
  • 選擇 [加入] > [新增項目] 選項
  • 在 [新增項目] 對話窗內,選擇 [Razor 頁面] 選項
  • 在下方 [名稱] 欄位內,輸入 Logout.cshtml
  • 點選 [新增] 按鈕,完成這個登入 Razor 頁面的建立
  • 打開 [Pages] 資料夾內的 [Logout.cshtml] 檔案
  • 使用底下 Razor 標記語法替換掉這個檔案內的內容
@page
@model BlazorAuthentication.Pages.LogoutModel
@{
    ViewData["Title"] = "登出";
}
<h2>登出</h2>
  • 在 [Pages] 資料夾內,找到 [Logout.cshtml] 節點內的 [Logout.cshtml.cs]
  • 使用滑鼠雙擊這個 [Logout.cshtml.cs] 節點,打開這個檔案內容
  • 使用底下 C# 程式碼替換掉原先這個檔案內的內容
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

#region 這裡將會是新加入的命名空間宣告
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
#endregion

namespace BlazorAuthentication.Pages
{
    public class LogoutModel : PageModel
    {
        public async Task<IActionResult> OnGetAsync()
        {
            // 清除已經存在的登入 Cookie 內容
            await HttpContext
                .SignOutAsync(
                CookieAuthenticationDefaults.AuthenticationScheme);
            return LocalRedirect(Url.Content("~/"));
        }
    }
}

建立登出入使用的 Blazor Razor Component 元件

  • 滑鼠右擊這個專案節點內的 [Shared] 資料夾
  • 選擇 [加入] > [新增項目] 選項
  • 在 [新增項目] 對話窗內,選擇 [Razor 頁面] 選項
  • 在下方 [名稱] 欄位內,輸入 LoginControl.razor
  • 點選 [新增] 按鈕,完成這個登入 Razor 頁面的建立
  • 打開 [Shared] 資料夾內的 [LoginControl.razor] 檔案
  • 使用底下 Razor 標記語法替換掉這個檔案內的內容
@page "/LoginControl"
<AuthorizeView>
    <Authorized>
        <b>Hello, @context.User.Identity.Name!</b>
        <a class="ml-md-auto btn btn-primary"
           href="/logout?returnUrl=/"
           target="_top"> 登出 </a>
    </Authorized>
    <NotAuthorized>
        <input type="text"
               placeholder="User Name"
               @bind="@Username" />
        &nbsp;&nbsp;
        <input type="password"
               placeholder="Password"
               @bind="@Password" />
        <a class="ml-md-auto btn btn-primary"
           href="/login?paramUsername=@Username&paramPassword=@Password"
           target="_top"> 登入 </a>
    </NotAuthorized>
</AuthorizeView>
@code {
    string Username = "";
    string Password = "";
}

進行 ASP.NET Core 專案的啟動設定修正

  • 在這個專案內找到 [Startup.cs] 檔案
  • 使用滑鼠雙擊這個檔案,打開這個檔案
  • 使用底下 C# 程式碼替換掉原有的檔案內容
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using BlazorAuthentication.Data;

#region 這裡將會是新加入的命名空間宣告
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Http;
using System.Net.Http;
#endregion

namespace BlazorAuthentication
{
    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.
        // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
        public void ConfigureServices(IServiceCollection services)
        {
            #region 加入使用 Cookie 認證需要的宣告
            services.Configure<CookiePolicyOptions>(options =>
            {
                options.CheckConsentNeeded = context => true;
                options.MinimumSameSitePolicy = Microsoft.AspNetCore.Http.SameSiteMode.None;
            });
            services.AddAuthentication(
                CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie();
            #endregion

            services.AddRazorPages();
            services.AddServerSideBlazor();
            services.AddSingleton<WeatherForecastService>();

            #region 加入會用到的服務宣告
            services.AddHttpContextAccessor();
            services.AddScoped<HttpContextAccessor>();
            services.AddHttpClient();
            services.AddScoped<HttpClient>();
            #endregion
        }

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

            #region 指定要使用 Cookie & 使用者認證的中介軟體
            app.UseCookiePolicy();
            app.UseAuthentication();
            #endregion

            app.UseRouting();

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

                #region 這裡要加入 Web API 控制器會用到的路由
                endpoints.MapControllerRoute("default", "{controller=Home}/action=Index/{id}");
                #endregion
            });
        }
    }
}

修正這個 Blazor 專案的整體路由設定

  • 在該專案根目錄下,找到並且打開 [App.razor] 檔案
  • 使用底下 Razor 元件 標記語法替換掉這個檔案內的內容
<CascadingAuthenticationState>
    <Router AppAssembly="@typeof(Program).Assembly">
        <Found Context="routeData">
            <RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
        </Found>
        <NotFound>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </NotFound>
    </Router>
</CascadingAuthenticationState>

修正主要版片配置的內容

  • 在該專案的 [Shared] 目錄下,找到並且打開 [MainLayout.razor] 檔案
  • 使用底下 Razor 元件 標記語法替換掉這個檔案內的內容
@inherits LayoutComponentBase

<div class="sidebar">
    <NavMenu />
</div>

<div class="main">
    <div class="top-row px-4">
        @*在這裡加入剛剛建立的登出入 Blazor 元件*@
        <LoginControl />
        <a href="https://docs.microsoft.com/aspnet/" target="_blank">About</a>
    </div>

    <div class="content px-4">
        @Body
    </div>
</div>

修正該專案的首頁 Blazor 元件

  • 在該專案的 [Pages] 目錄下,找到並且打開 [Index.razor] 檔案
  • 使用底下 Razor 元件 標記語法替換掉這個檔案內的內容
@page "/"

<h1>Hello, world!</h1>

Welcome to your new app.

<AuthorizeView>
    <Authorized>
        <h1>你好, @context.User.Identity.Name!</h1>
        <p>因為已經通過身分驗證成功,所以,會看到這個訊息</p>
    </Authorized>
    <NotAuthorized>
        <p>你尚未通過身分驗證,所以,會看到這個訊息</p>
    </NotAuthorized>
</AuthorizeView>

執行專案

  • 請執行這個 Blazor 專案
  • 將會看到如下圖的網頁顯示出來
  • 在網頁的最上方,將會看到剛剛設計的 [LoginControl.razor] 元件
  • 因為現在尚未通過身分驗證,所以,在 [LoginControl.razor] 元件上將會看到登入按鈕
  • 而且,在首頁上因為使用了 AuthorizeView 這個 Blazor 內建的元件,因此,將會根據使用者是否有成功通過驗證,而顯示出不同的內容
  • 由於現在尚未通過驗證,因此,在首頁上將會顯示尚未通過驗證的訊息
  • 請在 [User Name] 與 [Password] 欄位,輸入使用的帳號與密碼 (因為在這個練習專案中,並未實際做使用帳號與密碼的檢查,因此,可以輸入任何內容即可)
  • 請點選右上方的 [登入] 按鈕,如下面螢幕截圖
  • 在成功通過身分驗證過程,也就是登入成功了,在網頁最上方的 [LoginControl.razor] 元件內容也自動切換成已經登入的狀態
  • 並且,在首頁頁面上,也因為成功登入了,顯示出已經登入訊息,這裡的內容將會動態的切換,與尚未成功登入前有所不同
  • 因為採用 Cookie 的方式,所以,不論關閉這個網頁,或者開啟一個新的瀏覽器標籤頁次,都會看到如上面螢幕截圖的畫面