2023年2月13日 星期一

如何解決 Android 模擬器無法透過 Internet 打開任何網頁與看到網站內容

 

如何解決 Android 模擬器無法透過 Internet 打開任何網頁與看到網站內容

也許之前在使用 .NET MAUI 開發的時候,大多是用於進行各種教學課程的練習專案設計,還沒有進入到需要連線到網路上的應用範例;然而,上週需要進行使用 .NET MAUI 開發的專案,進行 OAuth2 的身分驗證開發需求,這裡首先需要用到的是要與 Azure AD 來進行身分驗證。

經過一番奮戰,首先決定先採用 Web authenticator 這個由 .NET MAUI 平台在 平臺整合 內所提供的 API 來嘗試能夠完成 OAuth2 與 Azure AD 的身分驗證。

在解決了大部分的問題之後,突然發現到,我經常使用的 Android Pixel 5 - API 33 模擬器,要使用裡面內建的 Chrome 瀏覽器來開啟自己設計的 Web API 服務的時候,卻無法開啟這個服務端點。當遇到這個問題,當然先去網路上來搜尋看看有沒有類似的問題發生,首先看到的就是,很多人都指向這樣的問題是與 DNS 設定有關,因此,根據網路上查詢到的資料,進行本機或者模擬器端的 DNS 修正,結果是沒有任何效果,之後回想,當第一次在模擬器開啟 Chrome 瀏覽器的時候,將會看到如下圖的畫面,在下方的 [Discovery] 的內容,可以看到這個瀏覽器似乎有正常運作,因為可以抓取到最新的網頁內容;另外,我也嘗試打開 [Youtube] App,發現到這個 App 可以正常運作,這表示了該模擬器的網路與 DNS 運作是沒有問題的。

其實,我是可以忽略掉這個問題,因為,在我的桌機上面,有個之前很早之前就安裝的模擬器,我用他來開啟任何網頁都可以正常運作,不過,當我沒有使用家裡的桌機時候,就必須面對到上面所提到的問題,因此,還是需要燃燒自己的生命,再次來進行探索與嘗試解決這個問題。

再次透過網路來搜尋,無意見看到一個這段文字

It's caused by vulkan. To fix it, you must turn vulkan off on emulator or chrome.

其中 Vulkan 代表甚麼與為什麼會造成這樣的問題,其實我並不關心,所以,我根據相關網頁提到的線索,進行底下的操作

  • 找到這個目錄 [C:\Users%USER%.android] 下的 [advancedFeatures.ini]
  • 使用任何文字編輯器工具來開啟這個檔案
  • 底下將會是我這台電腦上所看到的預設內容
WindowsHypervisorPlatform=on
  • 在這個檔案內,加入底下兩行敘述
WindowsHypervisorPlatform=on
Vulkan = off
GLDirectMem = on
  • 儲存並且關閉這個檔案
  • 重新開啟 Android 模擬器
  • 打開模擬器上的 Chrome 瀏覽器
  • 打開任何網頁 ,這裡打開聯合報的網站
  • 此時便可以正常運作了







2023年2月8日 星期三

動手練習 ASP.NET Core7 用強型別來讀取設定練習

動手練習 ASP.NET Core7 用強行別來讀取設定練習

最近在進行 ASP.NET Core 7 專案開發的時候,突然遇到一個需要,那就是在 [Program.cs] 這個檔案內,進行使用 [build.Services] 這個屬性要進行 DI / IoC 容器注入的時候,卻想要讀取 ASP.NET Core 的設定 Configuration 內提供的相關設定值的時候,卻又想要使用強型別的方式,將一個區段內的設定值,直接全部讀取出來,並且儲存到一個物件內,這樣就會免除了需要逐一透過 [Configuration] 物件來取得這些物件值的麻煩程式碼,也會造成神奇字串輸入錯誤所造成的額外副作用影響問題。

會有這樣的需求,是因為要進行 JWT 的需求設計,而在 [Program.cs] 內,需要使用 [builder.Services.AddAuthentication().AddJwtBearer] 來進行 JWT 服務的宣告,然而,在這個 [AddJwtBearer] 方法內,將會需要建立一個 [TokenValidationParameters] 物件,宣告這個 JWT 要進行那些驗證行為與這個 JWT 的設定值。

由於在進行設計使用者登入身分驗證的時候,若通過身分驗證後,需要產生一個適合的 JWT 物件出來,此時,也需要用剛剛提到的 JWT 相關設定值。因此,就會想要把這些設定值設定到 [appsettings.json] 檔案內,方便可以集中管理與變更,也不用把這些設定值使用 Hard Code 的方式,寫死在程式碼內,造成使用 Ctrl + C / Ctrl + V 操作模式下所造成的副作用影想。

在這裡將會要使用 ASP.NET Core 中的選項模式 所提供的能力來做到可以使用強型別的方式,來讀取 Configuration 設定的內容值,另外,在這裡也會使用較不方便的 Configuration 物件,說明如何使用弱型別的方式來讀取設定值的做法。

建立 ASP.NET Core 7 專案

首先先來建立一個 ASP.NET Core 空白專案,請依照底下說明來建立這個專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [Web]
  • 在中間的專案範本清單中,找到並且點選 [空的 ASP.NET Core] 專案範本選項
  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 AN020 作為專案名稱
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 請點選右下角的 [建立] 按鈕

修正設定檔案的來源內容

  • 在專案根節點內,找到並且打開 [appsettings.json] 檔案
  • 找到 "AllowedHosts": "*" 屬性宣告項目
  • 在該節點之後,加入底下宣告內容
"JWT": {
"ValidIssuer": "Backend",
"ValidAudience": "Backend",
"ExpireMinutes": 20,
"RefreshExpireDays": 7,
"IssuerSigningKey": "https://randomkeygen.com/"
}
  • 在這裡建立一個設定設定節點,該設定節點物件內將會包含五個屬性值,分別是 ValidIssuer 、 ValidAudience 、 ExpireMinutes 、 RefreshExpireDays 、 IssuerSigningKey
  • 這五個屬性值將會用於設定這個專案內的 JWT 元件運作行為與表現
  • 底下將會是完成後的 [appsettings.json] 檔案內所有內容
{
  "Logging": {
    "LogLevel": {
      "Default": "Information",
      "Microsoft.AspNetCore": "Warning"
    }
  },
  "AllowedHosts": "*",
  "JWT": {
    "ValidIssuer": "Backend",
    "ValidAudience": "Backend",
    "ExpireMinutes": 20,
    "RefreshExpireDays": 7,
    "IssuerSigningKey": "https://randomkeygen.com/"
  }
}

使用弱型別方式,讀取 設定 中的 JWT 區段內容

  • 在專案根目錄內,找到並且打開 [Program.cs] 檔案
  • 找到 var builder = WebApplication.CreateBuilder(args); 敘述
  • 在此敘述下方加入底下程式碼
#region 使用 Configuration 物件來讀取設定內容
string ValidIssuer = builder.Configuration["JWT:ValidIssuer"];
IConfigurationSection configurationSection = builder
    .Configuration.GetSection("JWT");
string ValidAudience = configurationSection["ValidAudience"];
string IssuerSigningKey = builder.Configuration["JWT:IssuerSigningKey"];
string ExpireMinutesContent = builder.Configuration["JWT:ExpireMinutes"];
int ExpireMinutes = int.Parse(ExpireMinutesContent);
string RefreshExpireDaysContent = configurationSection["RefreshExpireDays"];
int RefreshExpireDays = int.Parse(RefreshExpireDaysContent);

Console.WriteLine($"Configuration ValidIssuer : {ValidIssuer}");
Console.WriteLine($"Configuration ValidAudience : {ValidAudience}");
Console.WriteLine($"Configuration ExpireMinutes : {ExpireMinutes}");
Console.WriteLine($"Configuration RefreshExpireDays : {RefreshExpireDays}");
Console.WriteLine($"Configuration IssuerSigningKey : {IssuerSigningKey}");
  • 首先,想要取得型別為 [ConfigurationManager] 的物件,可以透過 [builder.Configuration] 物件來得到
  • ConfigurationManager 提供 key 為字串,而 value 為字串的資料字典運算子,因此,可以使用設定路徑字串作為鍵值,取得當時設定屬性值是甚麼。
  • 不同屬性間,可以使用 冒號 : 來分隔不同屬性階層關係
  • 例如,這裡使用 string ValidIssuer = builder.Configuration["JWT:ValidIssuer"]; 敘述,宣告鍵值 Key 為 JWT:ValidIssuer 這個絕對路徑,取得 [ConfigurationManager] 物件內的相對應 Value 值到 [ValidIssuer] 區域變數內
  • 另外,可以透過 [Configuration.GetSection] 方法,設定內容內的某個子階層設定內容,得到的物件型別將會是 ConfigurationSection
  • [ConfigurationSection] 型別的物件,提供 key 為字串,而 value 為字串的資料字典運算子
  • 透過 [ConfigurationSection] 物件,可以使用相對路徑的描述方式,取得某個子階層內的設定屬性值是甚麼。
  • 例如,這個敘述 string ValidAudience = configurationSection["ValidAudience"]; ,將會取得指定子階層內的 [ValidAudience] 屬性值到 ValidAudience 區域變數內
  • 在這個 JWT 設定區段內,有兩個屬性的屬性值將會是整數,其他的都是字串
  • 因此,需要使用上述的方式,取得相關屬性在設定環境內的實際文字內容,接著使用 [int.Parse] 方法,強制轉型成為整數數值。
  • 最後,就可以將這五個 .NET 物件,顯示在 Console 螢幕上
  • 從這裡程式碼與使用方式可以觀察到,由於 Configuration 屬性本身是透過 資料字典 資料結構來取的指定鍵值所對應的數值,不過,所取得的 Value 都是一樣的型別,都是字串;因此,若在設定環境中,指定某個屬性的型別要為 bool, 整數, 浮點數, 列舉,此時,程式設計師需要自己進行這些型別的轉換。
  • 另外一個看到的問題,那就是這裡要取得五個設定屬性內容,就需要在 .NET 系統內宣告五個區域物件來分別儲存這些設定數值,這顯得相當的不方便與不好維護。

建立要讀取設定內容的類別

  • 使用滑鼠右擊專案根節點
  • 從彈出功能表中,點選 [加入] > [類別] 功能項目
  • 此時出現 [新增項目] 對話窗視窗
  • 在 [新增項目] 對話窗下方的名稱欄位內,輸入 JwtConfiguration
  • 點選右下方 [新增] 按鈕,完成新增這個類別檔案
  • 使用底下程式碼,替換掉剛剛產生出來的檔案內容
namespace AN020
{
    public class JwtConfiguration
    {
        public string ValidIssuer { get; set; }
        public string ValidAudience { get; set; }
        public int ExpireMinutes { get; set; }
        public int RefreshExpireDays { get; set; }
        public string IssuerSigningKey { get; set; }
    }
}
  • 在這裡建立一個新的類別,其名稱為 [JwtConfiguration]
  • 在這個類別內,建立了五個屬性,其屬性名稱將會與剛剛在 [appsettings.json] 檔案內新增內容的屬性名稱需要相同。

使用強型別方式來取得設定環境內的數值

  • 有了 [JwtConfiguration] 類別,就可以開始進行用強型別方式來取得設定內的綁定數值
  • 在專案根目錄內,找到並且打開 [Program.cs] 檔案
  • 找到 var app = builder.Build(); 敘述
  • 在此敘述上方加入底下程式碼
#region 加入 設定 強型別 注入宣告
builder.Services.Configure<JwtConfiguration>(builder.Configuration
    .GetSection("JWT"));
#endregion

#region 使用 ServiceProvider 進行強型別讀取 JWT 設定值
using ServiceProvider serviceProvider = builder.Services.BuildServiceProvider();
JwtConfiguration jwtConfiguration = serviceProvider
    .GetRequiredService<IOptions<JwtConfiguration>>().Value;
#endregion

#region 顯示 JWT 設定值
Console.WriteLine($"ValidIssuer: {jwtConfiguration.ValidIssuer}");
Console.WriteLine($"ValidAudience: {jwtConfiguration.ValidAudience}");
Console.WriteLine($"ExpireMinutes: {jwtConfiguration.ExpireMinutes}");
Console.WriteLine($"RefreshExpireDays: {jwtConfiguration.RefreshExpireDays}");
Console.WriteLine($"IssuerSigningKey: {jwtConfiguration.IssuerSigningKey}");
#endregion
  • 首先,需要宣告準備要使用強型別的方式來取得設定環境內的數值
  • 透過 [builer.Services] 來對 DI / IoC 容器進行需要用到的型別註冊
  • 在此將會透過 builder.Services.Configure<JwtConfiguration>(builder.Configuration.GetSection("JWT")); 敘述來做到,在這裡將會透過 builder.Configuration.GetSection("JWT") 方法取得子階層節點的設定內容,接著,註冊這個設定子節點內容將會對應到 [JwtConfiguration] 類別
  • 想要取得這個設定內容子階層的所有數值到 [JwtConfiguration] 的物件內,這個時候就需要透過 DI 相依性注入機制,使用 [IOptions] 這樣的型別來取得一個物件,接著使用這個物件的 Value 屬性,便可以取得一個型別為 [JwtConfiguration] 物件
  • 不過,因為在這裡的程式碼,還在進行 DI 容器的註冊與設定工作,所以,幾乎沒有辦法透過建構式等方式來注入相依物件
  • 因此,在此將會使用 服務定位器 Service Locator 機制來取的一個 DI 容器物件,使用該 DI Container 物件提供的方法,取得指定型別的相依型別物件
  • 這可以從 ServiceProvider serviceProvider = builder.Services.BuildServiceProvider() 敘述可以看出,透過 Services (型別為 [IServiceCollection] 的物件)提供的 [BuildServiceProvider] 方法,便可以取得這裡個 IoC 容器物件
  • 使用 JwtConfiguration jwtConfiguration = serviceProvider.GetRequiredService<IOptions<JwtConfiguration>>().Value 敘述,便可以取得位於設定環境中的 [JWT] 子階層的所有設定內容到 JwtConfiguration 物件內
  • 最後,將 [JwtConfiguration] 物件內的屬性值輸出到螢幕上

執行專案與查看輸出結果

  • 請執行此專案,並且查看 Console 輸出結果是否如下所示
Configuration ValidIssuer : Backend
Configuration ValidAudience : Backend
Configuration ExpireMinutes : 20
Configuration RefreshExpireDays : 7
Configuration IssuerSigningKey : https://randomkeygen.com/
ValidIssuer: Backend
ValidAudience: Backend
ExpireMinutes: 20
RefreshExpireDays: 7
IssuerSigningKey: https://randomkeygen.com/
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: https://localhost:7033
info: Microsoft.Hosting.Lifetime[14]
      Now listening on: http://localhost:5063
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: C:\Vulcan\Projects\AN020\AN020 

 








2023年2月3日 星期五

解析 .NET MAUI 中 Microsoft.Toolkit.Mvvm 的運作方式

解析 .NET MAUI 中 Microsoft.Toolkit.Mvvm 的運作方式

在以往進行 Xamarin.Forms 專案開發時期,通常會使用 Prism 開發框架來進行整體專案開發,這是因為 Prism 提供了相當豐富的功能來方便與簡化行動裝置應用程式的開發,然而對於 MVVM 的開發上,進行 ViewModel 類別設計過程中,並沒有使用到繼承 [BindableBase] 這個類別來施做,而是使用了 [PropertyChanged.Fody] 這個套件來進行設計。

底下會是 [BindableBase] 類別的程式碼

public abstract class BindableBase : INotifyPropertyChanged
{
    public event PropertyChangedEventHandler PropertyChanged;

    protected virtual bool SetProperty<T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(storage, value)) return false;

        storage = value;
        RaisePropertyChanged(propertyName);

        return true;
    }

    protected virtual bool SetProperty<T>(ref T storage, T value, Action onChanged, [CallerMemberName] string propertyName = null)
    {
        if (EqualityComparer<T>.Default.Equals(storage, value)) return false;

        storage = value;
        onChanged?.Invoke();
        RaisePropertyChanged(propertyName);

        return true;
    }

    protected void RaisePropertyChanged([CallerMemberName] string propertyName = null)
    {
        OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }

    protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
    {
        PropertyChanged?.Invoke(this, args);
    }
}

從這個類別中可以看出,其實將會實作 [INotifyPropertyChanged] 介面,也就是在這個類別內需要有 public event PropertyChangedEventHandler PropertyChanged; 這個成員宣告存在。

對於想要能夠讓 資料綁定 Data Binding 機制可以正常運作,需要透過呼叫 [SetProperty] 這個泛型方法。

底下將會是使用繼承一個 [BindableBase] 類別,並且在 ViewModel 內宣告 [Text] 屬性,使得這個屬性可以用於 XAML 中的 {Binding Text} 延伸標記用法,這樣就完成了一個 資料綁定 的設計。

private string _text = "Click me";
public string Text
{
    get => _text;
    set => SetProperty(ref _text, value);
}

對於 [Text] 屬性的設計,將會採用 C# 自動實作的屬性 來設計;一旦當 [Text] 屬性有變更的時候,將會呼叫 [SetProperty] 這個方法,以便可以觸發 [PropertyChanged] 這個事件,如此,有關注或綁定這個事件的物件,將會收到通知,以便進行相對應的處理工作。

以上是對於一般資料型態的資料綁定的設計方式,對於需要綁定到 XAML 內的 [Command] 屬性上的命令綁定,則是透過在 ViewModel 內宣告 public DelegateCommand CountCommand { get; } 這個屬性,並且在建構式內,使用 CountCommand = new DelegateCommand(OnCountCommandExecuted); 敘述,產生一個 [DelegateCommand] 型別的物件,而在建立此物件的時候,至少需要傳入一個委派方法,而當這個命令被觸發的時候,將會來執行這裏所指定的委派方法。

在 PrismLibrary 內,對於 [DelegateCommand] 這個型別,將會繼承 [DelegateCommandBase] 類別,最終需要實作出 [ICommand] 這個介面,如此,才能夠使用這樣的物件於 XAML 內的 Command 來進行命令綁定之用

從這裡可看出,想要讓 MVVM 設計模式正常運作,達到關注點分離與鬆散耦合設計效果,程式設計師需要寫出相當多的程式碼,當然也就造成寫出許多原始碼內容,當然也會造成許多不良的副作用影響。

所以, [MVVM 工具] ,也就是 CommunityToolkit.Mvvm 套件,(也稱為 MVVM Toolkit,先前稱為 Microsoft.Toolkit.Mvvm) 是模組化的 MVVM 程式庫,使用了 [Roslyn] SDK 內提供的 來源產生器 Source Generators,透過這個機制, 來源產生器 ,可讓 C# 開發人員檢查正在編譯的使用者程式碼,來源產生器可以在即時新增至使用者的編譯時建立新的 C# 來源檔案。所得到的效果將會是可以讓整個專案原始碼變得更加簡潔與清爽,因為,Roslyn 編譯器已經把許多繁雜、瑣碎的工作與程式碼,都自動產生出來了。

現在,就來了解看看, [CommunityToolkit.Mvvm] 這個套件,在 .NET MAUI 專案內是如何運行的

建立 .NET MAUI 應用程式 專案

  • 開啟 Visual Studio 2022
  • 點選螢幕右下角的 [建立新的專案] 按鈕
  • 切換右上角的 [所有專案類型] 下拉選單控制項
  • 找到並且點選 [MAUI] 這個選項
  • 從清單中找到並選擇 [.NET MAUI 應用程式] 這個專案範本
  • 點選右下角的 [下一步] 按鈕
  • 當出現了 [設定新的專案] 對話窗
  • 在 [專案名稱] 欄位內,輸入 MA52
  • 點選右下角的 [下一步] 按鈕
  • 當出現了 [其他資訊] 對話窗
  • 對於 [架構] 的下拉選單控制項,使用預設值
  • 點選右下角的 [建立] 按鈕

加入 CommunityToolkit.Mvvm 的 NuGet 套件

  • 滑鼠右擊該專案的 [相依性] 節點
  • 從彈出功能表中選擇 [管理 NuGet 套件] 功能選項
  • 此時,[NuGet: csCommunityToolkitMVVM] 視窗將會出現
  • 點選 [瀏覽] 標籤頁次
  • 在左上方的搜尋文字輸入盒內輸入 CommunityToolkit.Mvvm 關鍵字
  • 現在,將會看到 CommunityToolkit.Mvvm 套件出現在清單內
  • 點選這個 CommunityToolkit.Mvvm 套件,並且點選右上方的 [安裝] 按鈕,安裝這個套件到這個專案內。

建立 MainPageViewModel 類別

在這個建立好的專案,採用的是 .NET MAUI 預設的專案模板,因此,並沒有 ViewModel 預設建立在這個專案內,所以,在這裡先來建立一個 MainPage 這個 View 要使用的 ViewModel 類別

  • 滑鼠右擊該專案節點
  • 從彈出功能表中選擇 [加入] > [類別] 功能選項
  • 此時,[新增項目] 對話窗將會出現
  • 在對話窗下方的 [名稱] 欄位內,輸入 MainPageViewModel 作為這個類別的名稱
  • 點選右下方 [新增] 按鈕
  • 底下將會這次產生出來的類別檔案內容
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MA52
{
    internal class MainPageViewModel
    {
    }
}

檢查 Rolysn 來源產生器有沒有甚麼程式碼自動產生

  • 因為 .NET MAUI 採用單一專案架構,但是可以在不同平來下來執行,因此,請先確認現在的執行平台是哪個

  • 預設來說,將會是 [Windows Machine] ,可以在 Visual Studio 2022 最上方找到 綠色 三角形的工具列按鈕,確認是否如下圖樣貌

  • 滑鼠右擊該專案節點

  • 從彈出功能表中選擇 [重建] 功能選項

  • 現在這個專案將會重新編譯

  • 一旦建置完成後

  • 找到專案節點,參考下圖,依序展開這些節點 [MA52] > [相依性] > [net7.0-windows10.0.19041.0] > [分析器]

  • 可以看到 [分析器] 將會看到 [CommunityToolkit.Mvvm.SourceGenerators]節點存在

  • 展開這個點之後,將會看到更多節點項目,請找到 [CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator] 這個節點項目

  • 請展開這個節點,將會出現 [此產生氣未產生檔案] 訊息,表示這裡尚未產生任何內容,不過,後面的內容,將會可以看到這裡產生出新項目。

  • 不過,另外可以看到有個 [Microsoft.Maui.Controls.SourceGen] 節點

  • 展開此節點將會看到有個 [Microsoft.Maui.Controls.SourceGen.CodeBehindGenerator] 節點存在

  • 請繼續展開此節點,將會如下面節圖

  • 從展開內容名稱可以猜測出來,這些都是 [Rolysn] 來源產生器產生出來的程式碼,而且都是在此專案內找到所有 .xaml 檔案,產生出相對應的 Code Behind 程式碼

  • 有興趣的人,可以打開這些產生檔案名稱,就會看到產生出來的程式碼

  • 底下將會是 [App.xaml.sg.cs] 節點內容

//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a .NET MAUI source generator.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

[assembly: global::Microsoft.Maui.Controls.Xaml.XamlResourceId("MA52.App.xaml", "App.xaml", typeof(global::MA52.App))]
namespace MA52
{
	[global::Microsoft.Maui.Controls.Xaml.XamlFilePath("App.xaml")]
	public partial class App : global::Microsoft.Maui.Controls.Application
	{
		[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Maui.Controls.SourceGen", "1.0.0.0")]
#if NET5_0_OR_GREATER
#endif
		private void InitializeComponent()
		{
			global::Microsoft.Maui.Controls.Xaml.Extensions.LoadFromXaml(this, typeof(App));
		}
	}
}

簡化預設產生的 MainPage 內容

  • 因為預設產生的頁面檔案,有使用到 Code Behind 內容,為了接下來的深入理解內容,故在此先將這個頁面內容簡化
  • 在專案根目錄下,找到並且打開 [MainPage.xaml] 檔案
  • 使用底下 XAML 標記內容,替換掉剛剛打開的檔案內容
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MA52.MainPage">

    <ScrollView>
        <VerticalStackLayout
            Spacing="25" Padding="30,0"
            VerticalOptions="Center">

            <Image
                Source="dotnet_bot.png"
                HeightRequest="200" HorizontalOptions="Center" />

            <Label
                Text="Hello, World!"
                FontSize="32" HorizontalOptions="Center" />

            <Label
                Text="Welcome to .NET Multi-platform App UI"
                FontSize="18" HorizontalOptions="Center" />

            <Button
                Text="Click me"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>
  • 在專案根目錄下,找到並且打開 [MainPage.xaml.cs] 檔案
  • 使用底下 C# 標記內容,替換掉剛剛打開的檔案程式碼
namespace MA52;

public partial class MainPage : ContentPage
{
	public MainPage()
	{
		InitializeComponent();
	}
}

修正 MainPageViewModel 可以使用 CommunityToolkit.MVVM 功能

  • 在專案根目錄下,找到並且打開 [MainPageViewModel.cs] 檔案
  • 使用底下 C# 程式碼替換掉這個檔案內容
using CommunityToolkit.Mvvm.ComponentModel;

namespace MA52;

public partial class MainPageViewModel : ObservableObject
{
}
  • 這裡展示了一個採用 [CommunityToolkit.MVVM] 套件的 ViewModel 標準類別設計形式

  • 首先, ViewModel 類別需要繼承 [ObservableObject] 這個類別,因為該類別內有實作 [INotifyPropertyChanged] 與 [INotifyPropertyChanging] 這兩個介面,有了實作介面的相關程式碼,便可以實踐出 MVVM 內的 資料綁定 Data Binding 機制了

  • 接下來,還要做個修正,那就是這個類別必須修改使用 [partial] 這個修飾詞,也就是要使用 部分類別 來進行設計

    若在此沒有加入 [partial] 這個修飾詞,將會導致等下要加入的程式碼,產生類似這樣的錯誤訊息

    錯誤 CS0260 類型 'MainPageViewModel' 的宣告中遺漏 partial 修飾元; 還存在此類型的其他部分宣告 MA52 (net7.0-android), MA52 (net7.0-ios), MA52 (net7.0-maccatalyst), MA52 (net7.0-windows10.0.19041.0) C:\Vulcan\Projects\MA52\MA52\MainPageViewModel.cs 5 作用中

    若忘記加入,也沒有關係,編譯器到時候會提醒你要加入回去

使用 CommunityToolkit.MVVM 提供的資料綁定功能

  • 假設這裡需要在 ViewModel 內,設計一個 Text 屬性,可以用於 XAML 中來進行資料綁定之用
  • 當使用 [PrismLibrary] 提供的 [BindableBase] 類別,需要使用底下六行 C# 程式碼來進行設計
  • 要宣告一個 Public 的 屬性 Property
private string _text = "Click me";
public string Text
{
    get => _text;
    set => SetProperty(ref _text, value);
}
  • 同樣的需求,對於使用 [CommunityToolkit.MVVM] 方法來設計,就僅需要使用底下的兩行 C# 程式碼就可以完成了
[ObservableProperty]
string text = "Click me";
  • 這裡需要宣告一個類別的 欄位 Field ,而不是 屬性 Property,當然,既然是 欄位 成員,就不需要是 public,這裡將會使用預設 private 存取權限

  • 還有一個特別要注意的事情,那就是這個 欄位 成員的名稱,必須採用 Camel Case (駝峰式) 命名規範,也就是第一個英文字母必須為小寫

  • 若採用 Pascal Case (Pascal式) 命名規範,也就是第一個英文字母必須為大寫,將會造成編譯器發出錯誤通知,背後的理由很單純,因為,編譯器會產生一個使用 Pascal Case 命名方式的 屬性 Property 成員原始碼,並且在這裡會加入更多的程式碼

  • 現在,可以從方案總管視窗內找看到 [分析器] 節點內的 [CommunityToolkit.Mvvm.SourceGenerators],在這個節點內展開 [CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator] 這個節點項目,將會看到如下面畫面截圖

  • 打開 [MA52.MainPageViewModel.g.cs] 這個由編譯器產生出來的原始碼,將會看到底下的內容

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MA52
{
    partial class MainPageViewModel
    {
        /// <inheritdoc cref="text"/>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        public string Text
        {
            get => text;
            set
            {
                if (!global::System.Collections.Generic.EqualityComparer<string>.Default.Equals(text, value))
                {
                    OnTextChanging(value);
                    OnPropertyChanging(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangingArgs.Text);
                    text = value;
                    OnTextChanged(value);
                    OnPropertyChanged(global::CommunityToolkit.Mvvm.ComponentModel.__Internals.__KnownINotifyPropertyChangedArgs.Text);
                }
            }
        }

        /// <summary>Executes the logic for when <see cref="Text"/> is changing.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
        partial void OnTextChanging(string value);
        /// <summary>Executes the logic for when <see cref="Text"/> just changed.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.ObservablePropertyGenerator", "8.1.0.0")]
        partial void OnTextChanged(string value);
    }
}
  • [Rosyln] 編譯器產生了一個 [MainPageViewModel] 類別,由於這裡也使用到了 [partial] 修飾詞,因此,這兩個 [MainPageViewModel] 類別將會由編譯器編譯到同一個類別內。
  • 如同剛剛說明到的,來源產生器產生了一個 [Text] 屬性,並且在 [set] 屬性存取子內,也產生出許多程式碼,用於完成資料綁定所需要的工作
  • 試想看看,若開發人員在進行 MVVM 專案開發的過程,沒有 [CommunityToolkit.MVVM] 套件的幫助,將會需要自己來寫出這些程式碼,並且也要確保這些自己寫出的程式碼正確性,對於日後要進行維護專案程式碼的時候,將會面臨到自己寫出繁多程式碼,也會造成維護上的負擔。

使用 CommunityToolkit.MVVM 提供的命令綁定功能

  • 假設這裡需要在 ViewModel 內,設計一個型別為 [RelayCommand] 的 [CountCommand] 屬性,可以用於 XAML 中來進行命令綁定之用;例如,可以綁定到按鈕的 [Command] 屬性上,使用 <Button Command{Binding CountCommand}>
[RelayCommand]
void Count()
{ }
  • 在這裡請先設計一個這個 ViewModel 類別內的 [Count] 方法成員,該方法名稱命名方式將會依照 .NET C# 內建議的 Pascal Case 命名式 (每個英文字的第一個字母要大小)

  • 最後,僅需要在這個方法的上方,使用 [RelayCommand] 這個屬性宣告即可

  • 現在,可以從方案總管視窗內找看到 [分析器] 節點內的 [CommunityToolkit.Mvvm.SourceGenerators],在這個節點內展開 [CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator] 這個節點項目,將會看到如下面畫面截圖

  • 打開 [MA52.MainPageViewModel.Count.g.cs] 這個由編譯器產生出來的原始碼,將會看到底下的內容

// <auto-generated/>
#pragma warning disable
#nullable enable
namespace MA52
{
    partial class MainPageViewModel
    {
        /// <summary>The backing field for <see cref="CountCommand"/>.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
        private global::CommunityToolkit.Mvvm.Input.RelayCommand? countCommand;
        /// <summary>Gets an <see cref="global::CommunityToolkit.Mvvm.Input.IRelayCommand"/> instance wrapping <see cref="Count"/>.</summary>
        [global::System.CodeDom.Compiler.GeneratedCode("CommunityToolkit.Mvvm.SourceGenerators.RelayCommandGenerator", "8.1.0.0")]
        [global::System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
        public global::CommunityToolkit.Mvvm.Input.IRelayCommand CountCommand => countCommand ??= new global::CommunityToolkit.Mvvm.Input.RelayCommand(new global::System.Action(Count));
    }
}
  • 從這裡產生的程式碼可以看到,這裡又是產生一個 [MainPageViewModel] 的 [partial] 類別
  • 這裡宣告一個型別為 [RelayCommand] 的 [countCommand] 欄位成員
  • 接著建立一個 屬性之運算式主體成員 的 CountCommand 屬性
  • 有了這個公開的 CountCommand 屬性,便可以在 XAML 頁面內,宣告與使用命令綁定功能