2020年4月8日 星期三

在 Visual Studio 內,指定特定的 NuGet 套件安裝來源作法

在 Visual Studio 內,指定特定的 NuGet 套件安裝來源作法

當在進行專案開發的時候,可以將團隊需要用到的各種常用 API,設計到類別庫內,接著,可以將該類別庫打包成為一個 NuGet 套件,而該 NuGet 套件便可以散發到網路上,提供其他開發團隊的成員,在 Visual Studio 內來設定該 NuGet 套件來源,取得所需要的套件。通常,開發者會打開他們的 Visual Studio,點選功能表 [工具] > [選項] > [NuGet 套件管理員] > [套件來源]
NuGet 可用套件來源
當看到 [可用套件來源] 對話窗出現之後,便可以在這個對話窗中,輸入想要新增的額外 NuGet 來源與名稱,這些 NuGet 的來源可能是公司內部的某個檔案伺服器的路徑、公司內部的私有 NuGet 伺服器、外部其他廠商提供的私有 NuGet 伺服器。
在這篇文章中,我已經將一些授課或者寫書會用到的常用 API,設計成為一個 類別庫,並且打包成為 NuGet 檔案,最後,上傳到 https://www.myget.org/ 網站上;當對於這些 API 有興趣者,只要輸入這個 URL https://www.myget.org/F/course-lab/api/v3/index.json 到 NuGet 來源設定中,便可以取得這些套件。
不過,問題來了,若在某台電腦上的 Visual Studio 已經設定了這個 NuGet 來源,對於使用其他電腦的開發者,一旦打開了這個正在開發的專案,則會因為那台電腦上沒有設定相關 NuGet 來源,而造成無法順利建置該專案;又或者是你自己的電腦重新安裝作業系統或者換了一台新電腦,也會遇到需要重新進行設定的困境。
現在可以使用 [nuget.config] 設個檔案,定義該專案要使用的其他 NuGet 來源,一旦設定好了之後,這些 NuGet 來源,便會跟著該專案跑,也就是說,當這個專案在別台電腦上打開之後,在這台電腦上,就已經會看到這些額外設定的 NuGet 來源。
這個說明專案的原始碼為 CustomNuGetSource

建立練習專案

  • 在這裡將會建立一個 Console 類型的專案來做為說明
  • 當該專案建立之後,請在該方案總管的節點上,使用滑鼠右擊 [加入] > [新增資料夾]
  • 對於該新的方案資料夾,輸入 nuget
  • 完成之後,滑鼠右擊 [nuget] 這個資料夾,選擇 [加入] > [新增項目]
  • 當新增項目對話窗顯示之後,在 [一般] 標籤頁次內的中間區域,會看到 [XML檔] 這個選項
  • 請點選 [XML 檔]這個選項,在下方的名稱欄位中,輸入 : nuget.config
  • 最後,請將底下內容,填入到這個檔案內
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <add key="course-lab" value="https://www.myget.org/F/course-lab/api/v3/index.json" />
  </packageSources>
</configuration>

檢查 NuGet 來源是否已經綁定專案了

  • 請先關閉 Visual Studio ,並且重新開啟 Visual Studio
  • 滑鼠右擊該專案的 [參考] 節點,選擇 [管理 NuGet 套件]
  • 當管理NuGet套件視窗出現之後,可以點選該視窗右上方的 [套件來源] 下拉選單
  • 此時,將會看到一個新的 NuGet 套件來源選項,這就是剛剛設定的 NuGet 套件來源,而該套件來源,將會跟著該專案跑。
    NuGet 套件
  • 另外,點選 Visual Studio 功能表 [工具] > [選項]
  • 當選項對話窗出現之後,展開 [NuGet 套件管理員] > [套件來源],也會看到這個新增的套件來源項目
    Visual Studio 可用套件來源

設計測試專案原始碼

在 [Program.cs] 檔案中,使用底下程式碼替代
    class Program
    {
        static void Main(string[] args)
        {
            var foo = Vulcan.ServiceSample.MonkeyData.Monkeys;
            foreach (var item in foo)
            {
                Console.WriteLine($"{item.Name}");
            }


            Console.WriteLine("Press any key for continuing...");
            Console.ReadKey();
        }
    }



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 下來執行,這樣,就不會發生問題了。