2021年7月30日 星期五

Blazor Server 必會開發技能 - C# 與 JavaScript 互相呼叫

Blazor Server 必會開發技能 - C# 與 JavaScript 互相呼叫


Blazor Server 必會開發技能

Blazor Server 建立一個新的頁面

Blazor Server 元件生命週期事件 Component Life Cycle

Blazor Server C# 程式碼設計方法

Blazor Server 單向資料綁定 One Way Data Binding 與 重新轉譯 Binding

Blazor Server Hello 互動頁面與事件設計 Two Way Data Binding

Blazor Server 元件間的參數傳遞與回應事件 Component Parameter EventCallback

Blazor Server C# 與 JavaScript 互相呼叫 IJSRuntime

Blazor Server 表單和驗證 Form Validation 


當在使用 Blazor 進行網站應用系統開發的時候,原則上,不再需要使用到 JavaScript 程式語言,不過,有些時候還是要需要能夠讓 C# 來呼叫 JavaScript 程式碼或者在 JavaScript 程式語言中來呼叫 .NET C# 的靜態或者執行個體公開方法,例如,想要把 Google Map 這樣的應用需求加入到 Blazor 專案內,就有可能需要進行這樣的設計,當然,也可以使用已經把這些需求打包成為 Blazor 套件來使用,在這篇文章將會來練習如何設計這樣的程式碼。

這裡說明的範例專案原始碼位於 BS07

建立 Blazor Server-Side 的專案

  • 打開 Visual Studio 2019

  • 點選右下方的 [建立新的專案] 按鈕

  • [建立新專案] 對話窗將會顯示在螢幕上

  • 從[建立新專案] 對話窗的中間區域,找到 [Blazor 應用程式] 這個專案樣板選項,並且選擇這個項目

  • 點選右下角的 [下一步] 按鈕

  • 現在 [設定新的專案] 對話窗將會出現

  • 請在這個對話窗內,輸入適當的 [專案名稱] 、 [位置] 、 [解決方案名稱]

    在這裡請輸入 [專案名稱] 為 BS07

  • 完成後,請點選 [建立] 按鈕

  • 當出現 [建立新的 Blazor 應用程式] 對話窗的時候

  • 請選擇最新版本的 .NET Core 與 [Blazor 伺服器應用程式]

  • 完成後,請點選 [建立] 按鈕

    稍微等會一段時間,Blazor 專案將會建立起來

加入 JavaScript 程式碼

  • 打開 [Pages] 資料夾內的 [_Host.cshtml] 檔案
  • 在 <head> 區段加入底下的 JavaScript 程式碼
@*客製化的 JavaScript*@
<script>
    window.helloWorld = {
        askYourName: function (title, hint) {
            return prompt(title, hint);
        },
        sayYourAgeToCsharp: function (dotNetHelper) {
            age = prompt('請輸入你的年紀', '所輸入數值將會傳遞給 C# 方法')
            dotNetHelper.invokeMethodAsync('GetStringFromJavaScript', age);
        },
    };
</script>

修改 Index.razor 元件

  • 打開 [Pages] 資料夾內的 [Index.razor] 檔案
  • 請使用底下程式碼替換到這個檔案內容
@page "/"
@inject IJSRuntime JSRuntime
<h1>Hello, C# 與 JavaScript 互相呼叫!</h1>

<button class="btn btn-primary" @onclick="AskYourName">你是誰? (C# -> JS)</button>
<div class="display-4 text-success">@Name</div>
<button class="btn btn-primary my-4" @onclick="AskYourAge">你幾歲? (C# -> JS -> C#)</button>
<div class="display-4 text-danger">@Message</div>

@code {
    public string Name { get; set; }
    public string Message { get; set; }
    private DotNetObjectReference<Index> objRef;

    async Task AskYourName()
    {
        Name = await JSRuntime.InvokeAsync<string>(
            "helloWorld.askYourName", "你是誰", "請輸入你的姓名");
    }

    async Task AskYourAge()
    {
        DotNetObjectReference<Index> objRef = DotNetObjectReference.Create(this);
        await JSRuntime.InvokeVoidAsync("helloWorld.sayYourAgeToCsharp", objRef);
    }

    [JSInvokable]
    public void GetStringFromJavaScript(string message)
    {
        Message = message;
    }
}

首先來看看如何在 C# 程式碼內來呼叫 JavaScript 內的方法,要能夠做到這樣的程式設計,首先要先使用 @inject 指示詞 Directive 來注入 [IJSRuntime] 這個物件。

在 [你是誰? (C# -> JS)] 按鈕所綁定的委派事件中,使用了這樣的敘述 Name = await JSRuntime.InvokeAsync<string>("helloWorld.askYourName", "你是誰", "請輸入你的姓名"); 來呼叫 JavaScript 方法,在 [JSRuntime.InvokeAsync] 方法的第一個參數將會是要呼叫的 [JavaScript] 方法名稱,而後的引數將會是要傳遞到 JavaScript 方法參數。

從這個 [window.helloWorld] 方法可以看出,該 [JavaScript] 方法使用了 [Prompt] 方法,要求使用者輸入一個文字字串,一旦使用者輸入完成後,就會回傳到 .NET C# 方法內了,因此,透過 [Name] 這個變數,就可以取得使用者輸入的文字內容

在這個 [你幾歲? (C# -> JS -> C#)] 按鈕中,將會呈現先使用 C# 呼叫一個 [JavaScript] 方法,接著,在 [JavaScript] 程式碼內,將會透過了 dotNetHelper.invokeMethodAsync('GetStringFromJavaScript', age); 方法來呼叫 .NET C# 內的 [GetStringFromJavaScript]。

首先,在 C# 按鈕方法內將會使用 DotNetObjectReference<Index> objRef = DotNetObjectReference.Create(this); 敘述來建立一個 [DotNetObjectReference] 物件,這裡的泛型型別參數將會表示將要採用執行個體的方法來呼叫,而不是使用靜態的方法 (要在 JavaScript 內呼叫 .NET 的靜態方法,需要使用 [DotNet.invokeMethodAsync] 方式);這裡產生的物件將會傳入到 JavaScrip 內,接著,在 [JavaScript] 內就可以使用這個物件來呼叫 .NET C# 物件的方法了。

最後,為了要能夠讓 [JavaScript] 呼叫 .NET C# 的方法,該方法需要使用 [JSInvokable] 屬性標住在方法前面,代表這個方法可以被 .NET C# 呼叫之用。

執行這個專案

  • 按下 [F5] 按鍵,開始執行這個 Blazor 專案

  • 一旦啟動完成,就會自動開以瀏覽器

  • 請點選 [你是誰? (C# -> JS)] 按鈕

  • 現在在網頁上將會出現一個對話窗,請輸入任何文字並且點選 [確定] 按鈕

  • 請點選 [你幾歲? (C# -> JS -> C#)] 按鈕

  • 現在在網頁上將會出現一個對話窗,請輸入年紀並且點選 [確定] 按鈕

  • 此時,可以從網頁上看到剛剛在使用 [JavaScript] 程式碼所取得的文字

 










2021年7月26日 星期一

ASP.NET Core 應該具備知識 - 日誌記錄

ASP.NET Core 應該具備知識 - 日誌記錄


ASP.NET Core 應該具備知識

ASP.NET Core 應用程式啟動與 Startup 類別

ASP.NET Core 靜態檔案 UseStaticFiles

ASP.NET Core 相依性注入設計模式 Dependency Injection

ASP.NET Core 中介軟體 Middleware

ASP.NET Core 設定 Configuration

ASP.NET Core 選項模式 IOptions

ASP.NET Core 日誌記錄 Logger

ASP.NET Core 從空白專案建立 Blazor 應用


建立一個 ASP.NET Core MVC 專案

  • 開啟 Visual Studio 2019

  • 在 [Visual Studio 2019] 對話窗中,點選右下方的 [建立新的專案] 選項

  • 在 [建立新專案] 對話窗中,在中間上方的專案範本過濾條件中

    1. 設定程式語言為 [C#]

    2. 設定專案範本為 [Web]

    3. 選擇專案範本項目清單,點選 [ASP.NET Core Web API] 這個專案範本項目

      用於建立 ASP.NET Core 應用程式的專案範本,附有 RESTful HTTP 服務的控制器範例。此範本也可用於 ASP.NET Core MVC 的檢視及控制器。

    4. 點選右下方的 [下一步] 按鈕

  • 在 [設定新的專案] 對話窗出現後

    在 [專案名稱] 內,輸入 AC07

    點選右下角的 [下一步] 按鈕

  • 在 [其他資訊] 對話窗出現後,確認 [目標 Framework] 的下拉選單要選擇 [.NET 5.0 (目前)]

  • 點選右下角的 [建立] 按鈕

建立服務類別與介面

  • 滑鼠右擊專案節點
  • 從彈出功能表來選取 [加入] > [類別]
  • 當出現 [新增項目 - AC07] 對話窗
  • 在下方 [名稱] 欄位輸入 [UnhandledExceptionMiddleware.cs]
  • 在右下方點選 [新增] 按鈕
  • 使用底下的程式碼替換掉這個檔案原先內容
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Threading.Tasks;

namespace AC07
{
    public class UnhandledExceptionMiddleware
    {
        private readonly ILogger logger;
        private readonly RequestDelegate next;

        public UnhandledExceptionMiddleware(ILogger<UnhandledExceptionMiddleware> logger, RequestDelegate next)
        {
            this.logger = logger;
            this.next = next;
        }

        public async Task Invoke(HttpContext context)
        {
            try
            {
                await next(context);
                if(IsSuccessStatusCode(context.Response))
                {
                    logger.LogDebug($"HTTP 回應成功 {context.Response.StatusCode}");
                }
                else
                {
                    logger.LogWarning($"HTTP 回應不是成功代碼 {context.Response.StatusCode}");
                }
            }
            catch (Exception exception)
            {
                logger.LogError(exception,
                    $"Request {context.Request?.Method}: {context.Request?.Path.Value} failed");
            }
        }
        public bool IsSuccessStatusCode(HttpResponse response)
        {
            return response.StatusCode >= 200 && response.StatusCode <= 299;
        }
    }
}

設計新的 API 控制器

  • 滑鼠右擊 [Controllers] 資料夾
  • 從彈出功能表清單中點選 [加入] > [控制器]
  • 當 [新增 Scaffold 項目] 對話窗出現之後
  • 依序點選 [已安裝] > [通用] > [API] > [API 控制器 - 空白]
  • 點選右下角的 [加入] 按鈕
  • 現在將會看到 [新增項目 - AC07] 對話窗
  • 在下方的 [名稱] 欄位中輸入 WebLogController
  • 點選右下方 [新增] 按鈕
  • 使用底下的程式碼替換這個檔案內容
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using System;
using System.Diagnostics;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

namespace AC07.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class WebLogController : ControllerBase
    {
        public WebLogController(ILogger<WebLogController> logger)
        {
            Logger = logger;
        }

        public ILogger<WebLogController> Logger { get; }

        [HttpGet]
        public async Task<string> Get()
        {
            StringBuilder result = new StringBuilder();

            var client = new HttpClient();

            try
            {
                Stopwatch stopwatch = new Stopwatch();
                stopwatch.Start();
                
                Logger.LogDebug($"開始讀取 Google 網頁資料");
                string message = await client.GetStringAsync("http://www.google.com");
                Logger.LogInformation($"完成讀取 Google 網頁資料");
                stopwatch.Stop();
                result.Append($"讀取 google 資料共花費 {stopwatch.ElapsedMilliseconds} ms" + Environment.NewLine);
                
                Logger.LogInformation($"開始讀取 m1cr050ft 網頁資料");
                message = await client.GetStringAsync("http://www.m1cr050ft.com");
                Logger.LogInformation($"完成讀取 m1cr050ft 網頁資料");
                result.Append($"讀取 micr0s0ft 資料共花費 {stopwatch.ElapsedMilliseconds} ms" + Environment.NewLine);
            }
            catch (Exception ex)
            {
                Logger.LogError(ex, $"使用 HttpClient 發生了問題");
            }

            return result.ToString();
        }
    }
}

執行這個專案

  • 請按下 [F5] 按鍵,開始執行這個專案

  • 瀏覽器的畫面如下

  • 請在瀏覽器上輸入 https://localhost:5001/api/WebLog 網址

  • 將會看到底下的畫面

  • 切換到執行這個專案的 [命令提示字元視窗] 下

  • 將會看到底下的內容

info: AC07.Controllers.WebLogController[0]
      完成讀取 Google 網頁資料
info: AC07.Controllers.WebLogController[0]
      開始讀取 m1cr050ft 網頁資料
fail: AC07.Controllers.WebLogController[0]
      使用 HttpClient 發生了問題
      System.Net.Http.HttpRequestException: Response status code does not indicate success: 403 (Forbidden).
         at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
         at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
         at AC07.Controllers.WebLogController.Get() in D:\Vulcan\GitHub\Blazor-Xamarin-Full-Stack-HOL\Examples\AC07\AC07\Controllers\WebLogController.cs:line 41
warn: AC07.UnhandledExceptionMiddleware[0]
      HTTP 回應不是成功代碼 404
  • 在這個 public async Task<string> Get() 方法內,看到原本對於有呼叫 Logger.LogDebug($"開始讀取 Google 網頁資料"); 方法,要把這段文字內容寫入到日誌提供者內,可是卻沒有看到;而其他有使用到 [Logger] 物件所產出的日誌輸出,都可以在 [命令提示字元視窗] 下看到。

對於呼叫 await client.GetStringAsync("http://www.m1cr050ft.com"); 方法之後,因為該網址不存在,所以,會拋出例外異常,這裡也會被 try {...} catch{...} 敘述捕捉起來,並且透過 Logger.LogError(ex, $"使用 HttpClient 發生了問題"); 敘述將這個例外異常訊息寫入到日誌提供者內。

修正那些日誌資訊可以看得到

現在要來解決為什麼執行 Logger.LogDebug($"開始讀取 Google 網頁資料"); 敘述,但是在 [命令提示字元視窗] 內沒有出現;要理解這個問題,要先來看看 [appsettings.json]這個檔案的內。

  • 底下為 [appsettings.json] 檔案內的內容
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "AllowedHosts": "*"
}
  • 在第一個 [Logging] 屬性是用來設定日誌輸出設定
  • 從 [LogLevel] > [Default] 的屬性值看到,這個日誌提供者僅會針對 [Information] 以上等級的日誌才會寫入輸出
  • 若此時變更 [LogLevel] > [Default] 屬性值為 [Trace]
  • 按下 [Ctrl] + [F5] 按鈕,停止這個專案執行
  • 請按下 [F5] 按鍵,開始執行這個專案
  • 請在瀏覽器上輸入 https://localhost:5001/api/WebLog 網址
  • 切換到執行這個專案的 [命令提示字元視窗] 下
  • 將會看到底下的內容
info: AC07.Controllers.WebLogController[0]
      完成讀取 Google 網頁資料
info: AC07.Controllers.WebLogController[0]
      開始讀取 m1cr050ft 網頁資料
fail: AC07.Controllers.WebLogController[0]
      使用 HttpClient 發生了問題
      System.Net.Http.HttpRequestException: Response status code does not indicate success: 403 (Forbidden).
         at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
         at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
         at AC07.Controllers.WebLogController.Get() in D:\Vulcan\GitHub\Blazor-Xamarin-Full-Stack-HOL\Examples\AC07\AC07\Controllers\WebLogController.cs:line 41
warn: AC07.UnhandledExceptionMiddleware[0]
      HTTP 回應不是成功代碼 404
  • 不過,還是沒有看到 Logger.LogDebug($"開始讀取 Google 網頁資料"); 這個日誌的輸出
  • 這是因為對於 [ASP.NET Core 設定] 功能
  • 對於在開發除錯模式下
  • 當要讀取 設定 屬性值,會先讀取 [appsettings.json] 內的設定屬性值
  • 底下為 [appsettings.Development.json] 檔案內的內容
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  }
}
  • 接著,會來讀取 [appsettings.Development.json] 內的設定屬性值,並且會優先使用這個檔案內的屬性值

因此,雖然在 [appsettings.json] 內變更 [LogLevel] > [Default] 屬性值為 [Trace],不過在 [appsettings.Development.json] 內的 [LogLevel] > [Default] 屬性值為 [Information],所以,還是排除掉了 [Debug] 這個等級的日誌訊息。

  • 現在修正 [appsettings.Development.json] 內的 [LogLevel] > [Default] 屬性值為 [Trace]
  • 按下 [Ctrl] + [F5] 按鈕,停止這個專案執行
  • 請按下 [F5] 按鍵,開始執行這個專案
  • 請在瀏覽器上輸入 https://localhost:5001/api/WebLog 網址
  • 切換到執行這個專案的 [命令提示字元視窗] 下
  • 將會看到底下的內容
dbug: AC07.UnhandledExceptionMiddleware[0]
      HTTP 回應成功 200
dbug: AC07.UnhandledExceptionMiddleware[0]
      HTTP 回應成功 200
dbug: AC07.Controllers.WebLogController[0]
      開始讀取 Google 網頁資料
info: AC07.Controllers.WebLogController[0]
      完成讀取 Google 網頁資料
info: AC07.Controllers.WebLogController[0]
      開始讀取 m1cr050ft 網頁資料
fail: AC07.Controllers.WebLogController[0]
      使用 HttpClient 發生了問題
      System.Net.Http.HttpRequestException: Response status code does not indicate success: 403 (Forbidden).
         at System.Net.Http.HttpResponseMessage.EnsureSuccessStatusCode()
         at System.Net.Http.HttpClient.GetStringAsyncCore(HttpRequestMessage request, CancellationToken cancellationToken)
         at AC07.Controllers.WebLogController.Get() in D:\Vulcan\GitHub\Blazor-Xamarin-Full-Stack-HOL\Examples\AC07\AC07\Controllers\WebLogController.cs:line 41
dbug: AC07.UnhandledExceptionMiddleware[0]
      HTTP 回應成功 200
warn: AC07.UnhandledExceptionMiddleware[0] 

      HTTP 回應不是成功代碼 404