2021年5月22日 星期六

快取 Web API 內容 1 : 將 .NET 物件永久保存儲存,並可以讀取回應用程式內

 

快取 Web API 內容 1 : 將 .NET 物件永久保存儲存,並可以讀取回應用程式內

當在進行 Xamarin.Forms 手機 App 開發的時候,會需要呼叫 Web API 來存取後端伺服器上的資料,不過,對於手機 App 的開發,有許多時候因為網路品質問題,需要能夠進行 off-line 的操作;面對這樣的需求,很多人會使用資料庫架構來進行與遠端資料庫的相關紀錄進行同步更新的設計方式,面對這樣的設計,當然會遇到許多設計上的問題,也就是說,整體技術門檻與設計能力需求較高。

我則是採用另外一種設計方式,那就是採用 Cache 快取方式,設計想是,每次進行 Web API 呼叫的時候,會把取得的集合紀錄快取到手機檔案系統內,採用的設計方式為把希望快取的 .NET 物件,經過 JSON 序列化處理後,將會得到 JSON 物件的文字內容,接著再把這些 JSON 文字內容寫入到檔案內;當重新啟動 App 的時候,會先將之前快取起來的相關 JSON 物件文字檔案內容讀取出來,接著,使用 JSON 反序列化成為 .NET 物件,這樣重新啟動的 App,便會擁有最後一次呼叫 Web API 取得的相關物件值。

採用這樣的設計方式的好處是十分輕巧,因為,只需要 JSON 文字內容寫入到檔案內,或者從檔案中讀取出來。另外,可以免除面對資料庫設計上的相關知識與經驗。

根據這樣的需要,規劃出需要具備底下的相關開發技能:

  • 可以將 .NET 物件寫入到檔案的支援方法
  • 可以將 檔案文字內容讀取出來,轉換成為 .NET 物件的支援方法
  • 一個基底類別,可以呼叫 CRUD 的 Web API
  • 該基底 Web API 類別,可以提供寫入或者讀取出需要永久保存的物件內容

在這篇文章中,將會先來設計出如何將 .NET 物件轉成 檔案的 JSON 文字檔案功能(當然,反向操作也要能夠運作)

這篇文章的原始碼位於 csObjectToFile

建立測試用主控台應用程式專案

  • 開啟 Visual Studio 2019
  • 選擇右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗中
  • 從右上方的專案類型下拉按鈕中,找到並選擇 [主控台]
  • 從可用專案範本清單內,找到並選擇 [主控台應用程式]
  • 點選左下方 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗中
  • 在 [專案名稱] 欄位中輸入 csObjectToFile
  • 點選左下方 [下一步] 按鈕
  • 在 [其他資訊] 對話窗中
  • 在 [目標 Framework] 下拉選單中,選擇 [.NET 5.0 (目前)]
  • 點選左下方 [建立] 按鈕

加入所需要使用到的 NuGet 套件

  • 滑鼠右擊 [csObjectToFile] 專案內的 [相依性] 節點
  • 從彈出功能表中,選擇 [管理 NuGet 套件]
  • 當 [NuGet: csObjectToFile] 視窗出現後,切換到 [瀏覽] 標籤頁次
  • 搜尋 [Newtonsoft.Json] 並且安裝並且套件

建立 Helper 類別使用到的資料夾

  • 滑鼠右擊 [csObjectToFile] 專案
  • 點選 [加入] > [新增資料夾]
  • 輸入 Storages 這個文字成為此資料夾的名稱

建立 把文字內容寫入到檔案或者從檔案讀取出來的支援類別

  • 滑鼠右擊 [Storages] 資料夾
  • 點選 [加入] > [類別]
  • 輸入 StorageUtility 這個類別的名稱
  • 點選 [新增] 按鈕,完成建立這個類別檔案
  • 使用底下程式碼,替換這個檔案內的內容
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading.Tasks;

namespace csObjectToFile.Storages
{
    /// <summary>
    /// Storage 相關的 API
    /// </summary>
    public class StorageUtility
    {
        /// <summary>
        /// 將所指定的字串寫入到指定目錄的檔案內
        /// </summary>
        /// <param name="folderName">目錄名稱</param>
        /// <param name="filename">檔案名稱</param>
        /// <param name="content">所要寫入的文字內容</param> 
        /// <returns></returns>
        public static async Task WriteToDataFileAsync
            (string folderName, string filename, string content)
        {
            //string rootPath = Environment
            //    .GetFolderPath(Environment.SpecialFolder.ApplicationData);
            string rootPath = Environment.CurrentDirectory;

            if (string.IsNullOrEmpty(folderName))
            {
                throw new ArgumentNullException(nameof(folderName));
            }

            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentNullException(nameof(filename));
            }

            if (string.IsNullOrEmpty(content))
            {
                throw new ArgumentNullException(nameof(content));
            }

            try
            {
                #region 建立與取得指定路徑內的資料夾
                string fooPath = Path.Combine(rootPath, folderName);
                if (Directory.Exists(fooPath) == false)
                {
                    Directory.CreateDirectory(fooPath);
                }
                fooPath = Path.Combine(fooPath, filename);
                Console.WriteLine($"寫入檔案的路徑 {fooPath}");
                #endregion

                byte[] encodedText = Encoding.UTF8.GetBytes(content);

                using (FileStream sourceStream = new FileStream(fooPath,
                    FileMode.Create, FileAccess.Write, FileShare.None,
                    bufferSize: 4096, useAsync: true))
                {
                    await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
            finally
            {
            }
        }

        /// <summary>
        /// 從指定目錄的檔案內將文字內容讀出
        /// </summary>
        /// <param name="folderName">目錄名稱</param>
        /// <param name="filename">檔案名稱</param>
        /// <returns>文字內容</returns>
        public static async Task<string> ReadFromDataFileAsync
            (string folderName, string filename)
        {
            string content = "";
            //string rootPath = Environment
            //    .GetFolderPath(Environment.SpecialFolder.ApplicationData);
            string rootPath = Environment.CurrentDirectory;

            if (string.IsNullOrEmpty(folderName))
            {
                throw new ArgumentNullException(nameof(folderName));
            }

            if (string.IsNullOrEmpty(filename))
            {
                throw new ArgumentNullException(nameof(filename));
            }

            try
            {
                #region 建立與取得指定路徑內的資料夾
                string fooPath = Path.Combine(rootPath, folderName);
                if (Directory.Exists(fooPath) == false)
                {
                    Directory.CreateDirectory(fooPath);
                }
                fooPath = Path.Combine(fooPath, filename);
                Console.WriteLine($"讀取檔案的路徑 {fooPath}");
                #endregion

                if (File.Exists(fooPath) == false)
                {
                    return content;
                }

                using (FileStream sourceStream = new FileStream(fooPath,
                    FileMode.Open, FileAccess.Read, FileShare.Read,
                    bufferSize: 4096, useAsync: true))
                {
                    StringBuilder sb = new StringBuilder();

                    byte[] buffer = new byte[0x1000];
                    int numRead;
                    while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
                    {
                        string text = Encoding.UTF8.GetString(buffer, 0, numRead);
                        sb.Append(text);
                    }

                    content = sb.ToString();
                }
            }
            catch (Exception ex)
            {
                Debug.WriteLine(ex.ToString());
            }
            finally
            {
            }

            return content.Trim();
        }
    }
}

這個 [StorageUtility] 類別能夠將指定的文字內容寫入到檔案內或者從指定的目錄與檔案名稱,讀取出來該檔案內的文字內容;該類別主要是提供兩個方法

  • WriteToDataFileAsync

    將所指定的字串寫入到指定目錄的檔案內

    在這個方法內將會接收 folderName 參數,要寫入資料夾的名稱,這裡可以是一個不同路徑組合,不過,這個檔案要寫入的最上層主要目錄,將會透過 Environment.CurrentDirectory 來取得。另外兩個則是這個檔案 filename 的名稱與要寫入的檔案文字內容 content。

    若指定的目錄不存在,則在寫入檔案之前,將會強制建立起這些目錄,以免寫入檔案的時候發生例外異常。

    當在寫入檔案的時候,會使用到 FileStream 這個類別,接著需要把寫入的文字內容使用 UTF8 進行編碼成為位元陣列,然後就可以寫入到指定的檔案內。

  • ReadFromDataFileAsync

    從指定目錄的檔案內將文字內容讀出

    在這個方法內將會接收 folderName 參數,要讀出資料夾的名稱。另外則是這個檔案 filename 的名稱,而該方法將會回傳讀取到的文字內容

    同樣的,若指定的目錄不存在,則在寫入檔案之前,將會強制建立起這些目錄,以免寫入檔案的時候發生例外異常。

    在讀取檔案內的文字內容時候,將會透過 FileStream 這個類別,使用 FileAccess.Read & FileShare.Read 屬性來讀取內容,另外,也會宣告 useAsync: true 表示要使用非同步的方式來讀取檔案內容。為了降地記憶體回收 GC Garbage Collection 的處理工作量,這裡會使用 StringBuilder 來組合從檔案內讀取到的許多小區塊文字內容,從程式碼可以看的出來,每次將會從檔案中讀取至多 0x1000 大小的資料。

建立 .NET 物件 <-> JSON 永久儲存的支援類別

  • 滑鼠右擊 [Storages] 資料夾
  • 點選 [加入] > [類別]
  • 輸入 StorageJSONService 這個類別的名稱
  • 點選 [新增] 按鈕,完成建立這個類別檔案
  • 使用底下程式碼,替換這個檔案內的內容

這個 [StorageJSONService] 類別設計的目的在於提供可以將任何 .NET 型別的物件,寫入到檔案內,或者可以從檔案中讀取該 .NET 物件狀態值,而建立好的 .NET 物件就像當初寫入到檔案內的物件值相同;該類別主要是提供兩個方法

  • WriteToDataFileAsync

    這裡將會傳入要寫入的資料夾名稱與檔案名稱(通常,我會將這個檔案名稱使用該物件的類別名稱,作為該檔案的名稱,這裡可以使用 nameof 這個方法來讀取到該物件的類別名稱),最後是要傳入所要寫入的 .NET 物件,由於這個類別是個泛型類別 ,因此,StorageJSONService<T> ,在使用該類別的時候,需要傳入這個寫入 .NET 物件的型別。

    在這個方法內,會使用 JsonConvert.SerializeObject(data) 將這個 .NET 物件轉換成為 JSON 物件,也就是會將 .NET 物件轉換成為文字內容,接著呼叫上面設計的 StorageUtility 類別,使用 await StorageUtility.WriteToDataFileAsync(directoryName, fileName, output); 敘述,接這個 JSON 文字內容寫入到檔案內。

    因此,有了這個方法,便可以將任何的 .NET 物件寫入到檔案內來保存當時的狀態值。

  • ReadFromFileAsync

    這裡將會傳入要讀取的資料夾名稱與檔案名稱。

    在這個方法內,呼叫上面設計的 StorageUtility 類別,使用 await StorageUtility.ReadFromDataFileAsync(directoryName, fileName); 敘述,將儲存在檔案內的 JSON 文字內容讀取出來;接著會使用 JsonConvert.DeserializeObject<T>(tempStr) 將這個 JSON 文字內容轉換成為 .NET 物件

    因此,有了這個方法,便可以將任何的 JSON 文字內容 從檔案內讀取出來,變成一個新的 .NET 物件。

    在這裡有使用到 T loadedFile = (T)Activator.CreateInstance(typeof(T)); 敘述,會建立一個指定型別的物件,這裡將會使用 無參數建構函式,所謂 「無參數建構函式」 就是不接受任何參數的建構函式

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Text;
using System.Threading.Tasks;

namespace csObjectToFile.Storages
{
    public class StorageJSONService<T>
    {

        /// <summary>
        /// Loads data from a file
        /// </summary>
        /// <param name="fileName">Name of the file to read.</param>
        /// <returns>Data object</returns>
        public static async Task<T> ReadFromFileAsync
            (string directoryName, string fileName)
        {
            //T loadedFile = default(T);
            T loadedFile = (T)Activator.CreateInstance(typeof(T));
            string tempStr = "";
            try
            {
                tempStr = await StorageUtility.ReadFromDataFileAsync(directoryName, fileName);
                loadedFile = JsonConvert.DeserializeObject<T>(tempStr);
            }
            catch
            {
                //ApplicationState.ErrorLog.Add(new ErrorLog("LoadFromFile", e.Message));
            }

            return loadedFile;
        }

        public static T LoadFromString(string SourceString)
        {
            T loadedFile = (T)Activator.CreateInstance(typeof(T));
            try
            {
                loadedFile = JsonConvert.DeserializeObject<T>(SourceString);
            }
            catch
            {
                //ApplicationState.ErrorLog.Add(new ErrorLog("LoadFromFile", e.Message));
            }

            return loadedFile;
        }

        /// <summary>
        /// Saves data to a file.
        /// </summary>
        /// <param name="fileName">Name of the file to write to</param>
        /// <param name="data">The data to save</param>
        public static async Task WriteToDataFileAsync
            (string directoryName, string fileName, T data)
        {
            try
            {
                string output = JsonConvert.SerializeObject(data);
                await StorageUtility.WriteToDataFileAsync(directoryName, fileName, output);
            }
            catch
            {
                // Add desired error handling for your application
                // ApplicationState.ErrorLog.Add(new ErrorLog("SaveToFile", e.Message));
            }
        }

    }
}

設計測試程式碼

  • 修正 [Program.cs] 檔案內容,使用底下程式碼來替換
using csObjectToFile.Storages;
using System;
using System.Threading.Tasks;

namespace csObjectToFile
{
    class Program
    {
        static async Task Main(string[] args)
        {
            var myClass = new MyClass()
            {
                MyPropertyInt = 20,
                MyPropertyString = "Vulcan Lee",
                MyPropertyDateTime = DateTime.Now.AddDays(-3),
                MyPropertyDouble = 99.82,
                MyPropertyTimeSpan = new TimeSpan(15, 23, 58),
            };

            await StorageJSONService<MyClass>.WriteToDataFileAsync
                ("Data", nameof(myClass), myClass);
        }
    }
}

執行結果

請按下 F5 開始執行這個專案,將會看到底下的執行結果

寫入檔案的路徑 D:\Vulcan\GitHub\CSharp2021\csObjectToFile\csObjectToFile\bin\Debug\net5.0\Data\myClass





2021年5月8日 星期六

Blazor 使用 Parameter 資料綁定方式的觸發時機

Blazor 使用 Parameter 資料綁定方式的觸發時機

當在進行 Blazor 專案程式設計的時候,會將一個頁面切割成為不同的 Blazor 元件,接著便可以透過 Blazor 的 使用元件參數進行系結 功能,將 A 元件內的物件值,透過安樹綁定的方式傳遞到元件 B 內。

不過,最近許多夥伴和我反映,他們設計了一個這樣的情境,在一個頁面元件上,使用者可以輸入相關的資料,一旦輸入完成之後,會按下送出按鈕,此時會呼叫 Syncfusion 的 DataGrid 這個元件的 Refresh 方法,請求 DataGrid 這個元件,依據剛剛輸入的內容,重新透過 Adapter 這個元件,從資料庫內抓取符合設定條件的紀錄出來,並且顯示在螢幕上。

這樣看是十分簡單設計需求,卻面臨到了一個十分詭異的現象,那就是當第一次點選送出按鈕之後,無法使用剛剛設定的內容來進行資料庫的查詢,進而取得符合的紀錄,而是需要按次點選一次該送出按鈕,此時才會取得所設定內容的相關紀錄。

通常,我都是鼓勵大家,若遇到這樣詭異的問題,並且需要請求他人的協助的時候,要能夠使用一個簡單的專案、描述如何做出這樣的專案,並且可以在這個執行中的專案上,進行甚麼樣的操作,以便可以重現每次都會遇到的問題。

若沒有辦法做到這樣的自我技能能力提升的情況,將會造成要幫你查看問題的人,很難在第一時間內找出問題,因為,他需要使用你開發的專案,在自己的電腦上跑起來這個專案(若沒有相關指引或者方法來說明如何建立起資料庫或者相關設定步驟,想要幫你解決問題的人,在第一時間內,是無法幫你解決問題的),了解你專案上的商業邏輯,看懂你寫的程式碼(當然,若你的程式碼寫得十分混亂,或者使用很多非常規的寫法,就需要花費其他時間再來解決這些額外問題),才有可能開始幫你看看到底是哪裡出了問題。

面對這個問題,我設計了一個簡單的專案,設計一個新的元件,該元件會接收一個可以綁定的參數與一個方法。當透過參數綁定成功之後,會將當時使用的執行緒ID 與訊息顯示在螢幕上,而當在其他元件要呼叫該元件的方法時候,也會顯示當時使用的執行緒ID 與訊息顯示在螢幕上

現在來看看如何做出這樣的範例成程式碼。

這篇文章的原始碼位於 bzParameterBinding

建立Blazor Server 應用程式專案

  • 開啟 Visual Studio 2019
  • 選擇右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗中
  • 從右上方的專案類型下拉按鈕中,找到並選擇 [Web]
  • 從可用專案範本清單內,找到並選擇 [空的 ASP.NET Core]
  • 點選左下方 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗中
  • 在 [專案名稱] 欄位中輸入 bzParameterBinding
  • 點選左下方 [下一步] 按鈕
  • 在 [其他資訊] 對話窗中
  • 在 [目標 Framework] 下拉選單中,選擇 [.NET 5.0 (目前)]
  • 點選左下方 [建立] 按鈕

建立一個新的元件

  • 滑鼠右擊 [Pages] 資料夾
  • 選擇 [加入] > [Razor 元件]
  • 在 [新增項目 - bzParameterBinding] 對話窗內的 [名稱] 欄位,輸入 Component.razor
  • 點選該對話窗右下方的 [新增] 按鈕
  • 使用底下的內容替換原先檔案內容
<h3>Component</h3>
<h4>@MyProperty</h4>

@code {
    private int myVar;
    [Parameter]
    public int MyProperty
    {
        get { return myVar; }
        set
        {
            myVar = value;
            Console.WriteLine($"設定參數綁定 ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
        }
    }
    protected override void OnParametersSet()
    {
        Console.WriteLine($"OnParametersSet ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
    }
    public void ShowSomething()
    {
        Console.WriteLine($"呼叫方法 [{MyProperty}] ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
    }
}

修正 Index.razor 元件

  • 打開 [Pages] 資料夾內的 [Index.razor] 檔案
  • 將該類別設計為底下內容
  • 使用底下的內容替換原先檔案內容
@page "/"

<h1>Hello, Parameter Binding</h1>

<br />
<button class="btn btn-primary"
        @onclick="btnClick">
    加一
</button>
<br />
<Component @ref="component" MyProperty="counter" />

<SurveyPrompt Title="How is Blazor working for you?" />

@code{
    int counter = 0;
    Component component;
    async Task btnClick()
    {
        #region 使用同步方式來執行
        //Console.WriteLine();
        //Console.WriteLine($"觸發按鈕事件 ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
        //counter++;
        //Console.WriteLine($"計數器 + 1 ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
        //component.ShowSomething();
        #endregion

        #region 呼叫元件方法前,使用 await 
        //Console.WriteLine();
        //Console.WriteLine($"觸發按鈕事件 ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
        //counter++;
        //Console.WriteLine($"計數器 + 1 ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
        //await Task.Yield();
        //await Task.Delay(100);
        //component.ShowSomething();
        #endregion

        #region 將呼叫元件方法改成非同步方式呼叫
        //Console.WriteLine();
        //Console.WriteLine($"觸發按鈕事件 ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
        //counter++;
        //Console.WriteLine($"計數器 + 1 ({System.Threading.Thread.CurrentThread.ManagedThreadId})");
        //var myTask = System.Threading.Tasks.Task.Run(() =>
        //{
        //    System.Threading.Thread.Sleep(100);
        //    component.ShowSomething();
        //});
        #endregion
    }

}

執行並且測試

  • 首先,在 [Index.razor] 檔案中,找到 #region 使用同步方式來執行 文字
  • 將 #region 使用同步方式來執行 與 #endregion 區段內的程式碼,解除其註解
  • 按下 F5 開始執行這個專案
  • 當專案一啟動執行,必且顯示 Index.razor 頁面後
  • 可以從 Console 視窗中看到底下的輸出內容
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Vulcan\Projects\bzParameterBinding\bzParameterBinding
設定參數綁定 (4)
OnParametersSet (4)
設定參數綁定 (14)
OnParametersSet (14)

從 Console 視窗中看到的輸出結果,顯示了兩次 [設定參數綁定] 的文字,這是因為第一次的文字輸出是因為 Blazor 內部的 Pre-render 機制所造成的,第二次的輸出內容,則是顯示該頁面到網頁上的時候,所造成的。

另外,這裡也看到了,當透過參數綁定把物件傳遞到其他元件內,會先把物件值更新到有標示 [Parameter] 的屬性上,接著便會觸發 Blazor 內建生命週期的 [OnParameterSet] 事件方法。

在瀏覽器上可以看底下的畫面

請點選 [加一] 這個按鈕,此時 Console 螢幕會出現底下內容

觸發按鈕事件 (4)
計數器 + 1 (4)
呼叫方法 [0] (4)
設定參數綁定 (4)
OnParametersSet (4)

觸發按鈕事件 (13)
計數器 + 1 (13)
呼叫方法 [1] (13)
設定參數綁定 (13)
OnParametersSet (13)

不論你點選幾次這個 [加一] 按鈕,你會發現到,當在這個按鈕事件程式碼執行完成之後,會把計數器 conuter 加一,接著執行 component.razor 這個元件的 ShowSomething 方法,也就是呼叫 component.ShowSomething();

最後從 Console 輸出內容看到,當 ShowSomething 方法執行完成之後,參數綁定的動作才會完成,然而,此時對於正在執行 ShowSomething() 方法的時候,所看到的參數綁定物件內容,卻不是父元件所傳送過來的物件值,因此,這樣的執行結果不是當初所要設計出來的結果,也是我們小夥伴所遇到的問題已經透過這樣的範例程式碼重現出來了。

使用暫停一段時間的非同步設計,執行並且測試

  • 停止這個專案執行
  • 首先將剛剛解除註解的程式碼重新註解起來
  • 在 [Index.razor] 檔案中,找到 #region 呼叫元件方法前,使用 await 文字
  • 將 #region 呼叫元件方法前,使用 await 與 #endregion 區段內的程式碼,解除其註解
  • 按下 F5 開始執行這個專案
  • 當專案一啟動執行,必且顯示 Index.razor 頁面後
  • 可以從 Console 視窗中看到底下的輸出內容
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Vulcan\Projects\bzParameterBinding\bzParameterBinding
設定參數綁定 (9)
OnParametersSet (9)
設定參數綁定 (4)
OnParametersSet (4)

從 Console 視窗中看到的輸出結果,顯示了兩次 [設定參數綁定] 的文字,這是因為第一次的文字輸出是因為 Blazor 內部的 Pre-render 機制所造成的,第二次的輸出內容,則是顯示該頁面到網頁上的時候,所造成的。

另外,這裡也看到了,當透過參數綁定把物件傳遞到其他元件內,會先把物件值更新到有標示 [Parameter] 的屬性上,接著便會觸發 Blazor 內建生命週期的 [OnParameterSet] 事件方法。

在瀏覽器上可以看底下的畫面

請點選 [加一] 這個按鈕,此時 Console 螢幕會出現底下內容

觸發按鈕事件 (9)
計數器 + 1 (9)
設定參數綁定 (9)
OnParametersSet (9)
呼叫方法 [1] (16)

觸發按鈕事件 (9)
計數器 + 1 (9)
設定參數綁定 (9)
OnParametersSet (9)
呼叫方法 [2] (4)

觸發按鈕事件 (6)
計數器 + 1 (6)
設定參數綁定 (6)
OnParametersSet (6)
呼叫方法 [3] (11)

現在,你將會發現到,當按下 [加一] 按鈕之後,計數器會加一,之後會執行 [await Task.Yield()] ,當然,也可以執行 [await Task.Delay(100)] 休息 0.1 秒,不論是哪種方式,當程式碼遇到 await 之後,將會立即 return 回去,而此時,將會發現到參數綁定的工作將會正確無誤的執行,一旦作業系統配置原先等待的程式碼可以繼續使用 CPU 來執行的時候,將會正確顯示出剛剛父元件所傳遞過去的物件值。

從輸出內容可以看到,當呼叫 [ShowSomething()] 方法的時候,使用的是另外一個執行緒,而不是原先的執行緒,若你對於 await 這個運算子有充分的了解,就會知道為什麼會是這樣的結果。

將呼叫元件方法改成非同步方式呼叫,執行並且測試

  • 停止這個專案執行
  • 首先將剛剛解除註解的程式碼重新註解起來
  • 在 [Index.razor] 檔案中,找到 #region 將呼叫元件方法改成非同步方式呼叫 文字
  • 將 #region 將呼叫元件方法改成非同步方式呼叫 與 #endregion 區段內的程式碼,解除其註解
  • 按下 F5 開始執行這個專案
  • 當專案一啟動執行,必且顯示 Index.razor 頁面後
  • 可以從 Console 視窗中看到底下的輸出內容
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Vulcan\Projects\bzParameterBinding\bzParameterBinding
設定參數綁定 (9)
OnParametersSet (9)
設定參數綁定 (6)
OnParametersSet (6)

請點選 [加一] 這個按鈕,此時 Console 螢幕會出現底下內容

觸發按鈕事件 (6)
計數器 + 1 (6)
設定參數綁定 (6)
OnParametersSet (6)
呼叫方法 [1] (10)

觸發按鈕事件 (10)
計數器 + 1 (10)
設定參數綁定 (10)
OnParametersSet (10)
呼叫方法 [2] (12)

觸發按鈕事件 (6)
計數器 + 1 (6)
設定參數綁定 (6)
OnParametersSet (6)
呼叫方法 [3] (4)

現在,你將會發現到,當按下 [加一] 按鈕之後,計數器會加一,接著使用一個非同步工作來執行 [component.ShowSomething();] 這個方法,將會得到你想要的執行結果;然而,你可能發現到了,在這個 非同步 工作內,有執行 [System.Threading.Thread.Sleep(100);] 這個敘述,在呼叫 [component.ShowSomething();] 這個方法前,休息了 0.1 秒的時間。

現在,請將 [System.Threading.Thread.Sleep(100);] 這個敘述註解起來,重新執行一次,接著點選三次 [加一] 按鈕,你將會看到決然不同的執行結果,你知道發生了甚麼問題嗎?

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: D:\Vulcan\Projects\bzParameterBinding\bzParameterBinding
設定參數綁定 (5)
OnParametersSet (5)
設定參數綁定 (5)
OnParametersSet (5)

觸發按鈕事件 (5)
計數器 + 1 (5)
設定參數綁定 (5)
OnParametersSet (5)
呼叫方法 [1] (10)

觸發按鈕事件 (11)
計數器 + 1 (11)
呼叫方法 [1] (15)
設定參數綁定 (11)
OnParametersSet (11)

觸發按鈕事件 (9)
計數器 + 1 (9)
呼叫方法 [2] (4)
設定參數綁定 (9) 

OnParametersSet (9)