2025年2月21日 星期五

Azure OpenAI AOAI 2.0 : 14 Prompt 的各種參數使用與變化差異

Azure OpenAI AOAI 2.0 : 14 Prompt 的各種參數使用與變化差異

當在進行呼叫 Azure OpenAI API,用來進行聊天操作時候,可以透過 ChatCompletionOptions 物件設定不同的參數,來達到不同的效果。這些參數將會包含了 MaxOutputTokenCount:控制回應的最大長度、Temperature:控制回應的隨機性,值越高回應越有創意但可能不夠精確,值越低回應越保守但更可預測、TopP:另一種控制輸出多樣性的方式,與 Temperature 擇一使用即可、FrequencyPenalty:減少模型重複使用相同詞彙的傾向、PresencePenalty:增加模型談論新主題的傾向。

透過不同的參數設定,可以讓回應的內容有不同的風格,這篇文章將會示範如何透過 Temperature 與 TopP 這兩個參數,來控制回應的內容風格。

建立測試專案

請依照底下的操作,建立起這篇文章需要用到的練習專案

  • 打開 Visual Studio 2022 IDE 應用程式
  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕
  • 在 [建立新專案] 對話窗右半部
    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [主控台]
  • 在中間的專案範本清單中,找到並且點選 [主控台應用程式] 專案範本選項

    專案,用於建立可在 Windows、Linux 及 macOS 於 .NET 執行的命令列應用程式

  • 點選右下角的 [下一步] 按鈕
  • 在 [設定新的專案] 對話窗
  • 找到 [專案名稱] 欄位,輸入 csPromptOption 作為專案名稱
  • 在剛剛輸入的 [專案名稱] 欄位下方,確認沒有勾選 [將解決方案與專案至於相同目錄中] 這個檢查盒控制項
  • 點選右下角的 [下一步] 按鈕
  • 現在將會看到 [其他資訊] 對話窗
  • 在 [架構] 欄位中,請選擇最新的開發框架,這裡選擇的 [架構] 是 : .NET 8.0 (長期支援)
  • 在這個練習中,需要去勾選 [不要使用最上層陳述式(T)] 這個檢查盒控制項

    這裡的這個操作,可以由讀者自行決定是否要勾選這個檢查盒控制項

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

稍微等候一下,這個 背景工作服務 專案將會建立完成

安裝要用到的 NuGet 開發套件

因為開發此專案時會用到這些 NuGet 套件,請依照底下說明,將需要用到的 NuGet 套件安裝起來。

安裝 Azure.AI.OpenAI 套件

請依照底下說明操作步驟,將這個套件安裝到專案內

  • 滑鼠右擊 [方案總管] 視窗內的 [專案節點] 下方的 [相依性] 節點
  • 從彈出功能表清單中,點選 [管理 NuGet 套件] 這個功能選項清單
  • 此時,將會看到 [NuGet: csPromptOption] 視窗
  • 切換此視窗的標籤頁次到名稱為 [瀏覽] 這個標籤頁次
  • 在左上方找到一個搜尋文字輸入盒,在此輸入 Azure.AI.OpenAI
  • 在視窗右方,將會看到該套件詳細說明的內容,其中,右上方有的 [安裝] 按鈕

    請確認有取消 Pre-release 這個選項,與選擇 2.0 正式版

  • 點選這個 [安裝] 按鈕,將這個套件安裝到專案內

修改 Program.cs 類別內容

在這篇文章中,將會把會用到的新類別與程式碼,都寫入到 [Program.cs] 這個檔案中,請依照底下的操作,修改 [Program.cs] 這個檔案的內容

  • 在專案中找到並且打開 [Program.cs] 檔案
  • 將底下的程式碼取代掉 Program.cs 檔案中內容
using Azure.AI.OpenAI;
using OpenAI.Chat;

namespace csPromptOption;

internal class Program
{
    static void Main(string[] args)
    {
        // 讀取環境變數 AOAILabKey 的 API Key
        string apiKey = System.Environment.GetEnvironmentVariable("AOAILabKey");
        AzureOpenAIClient azureClient = new(
            new Uri("https://gpt4tw.openai.azure.com/"),
            new System.ClientModel.ApiKeyCredential(apiKey));
        ChatClient chatClient = azureClient.GetChatClient("gpt-4");

        string userPrompt = "你如何形容海灘?";
        ChatCompletionOptions options = new();
        options.Temperature = 0.8f;

        Chart(chatClient, userPrompt, options);
        NewLine();

        userPrompt = "你如何形容海灘?";
        options = new();
        options.Temperature = 0.3f;

        Chart(chatClient, userPrompt, options);
        NewLine();

        userPrompt = "形容一棵樹";
        options = new();
        options.TopP = 1f;

        Chart(chatClient, userPrompt, options);
        NewLine();

        userPrompt = "形容一棵樹";
        options = new();
        options.TopP = 0.1f;

        Chart(chatClient, userPrompt, options);
        NewLine();
    }

    private static void NewLine()
    {
        Console.WriteLine(new string('-', 40));
        Console.WriteLine(new string('=', 40));
    }

    private static void Chart(ChatClient chatClient, string userPrompt, ChatCompletionOptions options)
    {
        List<ChatMessage> prompts = new()
        {
            UserChatMessage.CreateUserMessage(userPrompt)
        };

        foreach (var message in prompts)
        {
            string roleName = message is SystemChatMessage ? "System" :
                message is UserChatMessage ? "User" :
                "Assistant";
            Console.WriteLine($"{DateTime.Now}  [{roleName}]: {message.Content[0].Text}");
        }

        ChatCompletion completion = chatClient.CompleteChat(prompts, options);
        Console.WriteLine($"Role : {completion.Role}");

        foreach (var message in completion.Content)
        {
            Console.WriteLine($"{DateTime.Now} {message.Text}");
        }
        Console.WriteLine($"InputTokenCount : {completion.Usage.InputTokenCount}");
        Console.WriteLine($"OutputTokenCount : {completion.Usage.OutputTokenCount}");
        Console.WriteLine($"ReasoningTokenCount : {completion.Usage.OutputTokenDetails?.ReasoningTokenCount}");
        Console.WriteLine($"TotalTokenCount : {completion.Usage.TotalTokenCount}");
    }
}

第一個提示詞將會是 [你如何形容海灘?] ,在這裡將會建立一個 [ChatCompletionOptions] 物件,使用 options.Temperature = 0.8f; 敘述,將溫度值設定為 0.8,其中,溫度值越高回應越有創意但可能不夠精確,值越低回應越保守但更可預測,看看呈現結果為何?

第二個提示詞將會是 [你如何形容海灘?] ,在這裡將會建立一個 [ChatCompletionOptions] 物件,使用 options.Temperature = 0.3f; 敘述,將溫度值設定為 0.3,看看呈現結果為何?

第三個提示詞將會是 [形容一棵樹] ,在這裡將會建立一個 [ChatCompletionOptions] 物件,使用 options.TopP = 1f; 敘述,將 TopP 值設定為 1,這個值是另一種控制輸出多樣性的方式,與 Temperature 擇一使用即可,看看呈現結果為何?

第四個提示詞將會是 [形容一棵樹] ,在這裡將會建立一個 [ChatCompletionOptions] 物件,使用 options.TopP = 0.1f; 敘述,將 TopP 值設定為 0.1,看看呈現結果為何?

執行測試專案

  • 按下 F5 開始執行專案
  • 將會看到輸出結果
2025/2/21 上午 09:09:19  [User]: 你如何形容海灘?
Role : Assistant
2025/2/21 上午 09:09:37 海灘是自然界中一種迷人且多樣化的景象,通常由以下幾個特徵來描述:

1. **地理位置**:海灘是海洋與陸地相遇的地方,通常位於海岸線沿岸。

2. **沙質**:許多海灘擁有細軟的沙粒,這些沙子可能是由於岩石經歷風化和侵蝕作用而形成的,顏色可以從白色、金色、粉紅色到黑 色不等。

3. **海浪**:海浪從溫和的拂浪到強烈的激浪不等,提供了不同的水上活動場景,如衝浪或是悠閒的浮潛。

4. **生物多樣性**:海灘上經常有各式各樣的生物,從海鳥、螃蟹到各種貝殼和海洋植物,是生物多樣性的熱點。

5. **聲音**:海浪拍打沙灘的聲音帶來了一種舒緩的節奏,而海鳥的叫聲則增添了一種自然的樂章。

6. **氣味**:海灘特有的鹹鹹的海水氣息,以及海藻和海洋生物的香味,構成了一個獨特的嗅覺體驗。

7. **景觀**:從壯麗的日出到黃昏時分的日落,海灘的美景不斷變化,提供了絕佳的拍照機會。

8. **氣氛**:海灘通常與休閒和放鬆聯繫在一起,是人們逃離日常生活壓力、尋求寧靜的好去處。

海灘可以是活潑的,充滿人們嬉戲、運動和社交;也可以是偏遠和寧靜的,讓人有機會獨自沉思。??如何,海??是能?以其自然之美和宁?之感吸引?我?。
InputTokenCount : 16
OutputTokenCount : 677
ReasoningTokenCount :
TotalTokenCount : 693
----------------------------------------
========================================
Role : Assistant9:09:37  [User]: 你如何形容海灘?
2025/2/21 上午 09:09:54 海灘是一個自然景觀,通常由細膩的沙子、礫石或小石頭構成,它位於陸地與海洋、湖泊或河流的交界處。以下是對 海灘的一些形容詞和描述:

1. 寧靜的:一片平靜的海灘,海浪輕輕拍打著岸邊,營造出一種放鬆和安詳的氛圍。
2. 熱鬧的:在熱門的旅遊地點,海灘可能會擠滿了遊客,充滿了活力和喧囂,有人在玩沙、曬太陽、游泳或參與水上運動。
3. 原始的:一些較少被開發的海灘保持著它們的自然美,周圍可能有茂密的植被、岩石或懸崖。
4. 金色的:許多海灘以其閃亮的金色沙粒而聞名,這些沙粒在陽光下閃閃發光,給人一種溫暖的感覺。
5. 白色的:有些海灘擁有純白色的細沙,這些沙灘在陽光下顯得特別耀眼和引人注目。
6. 岩石的:不是所有的海灘都有細軟的沙子,有些海灘可能是由鋒利的岩石或圓滑的卵石構成,給人一種粗獷和原始的感覺。
7. 寬闊的:某些海灘擁有非常寬廣的沙岸,讓人有大片空間可以遊玩和放鬆。
8. 窄長的:有些海灘可能只有一條窄窄的沙帶,夾在海水和高地之間。
9. 清澈的:海灘旁的水域可能非常清澈,可以看到水下的沙子、岩石甚至海洋生物。
10. 翡翠綠的:有時候,海灘附近的海水會呈現出翡翠綠色,這通常是由於水質極佳和海底的特定礦物質。

海灘不僅是一個美麗的自然場所,也是許多人休閒和娛樂的目的地。它們可以是浪漫的、冒險的、放鬆的或者是運動的場所,取決於海灘的特點 和訪客的興趣。
InputTokenCount : 16
OutputTokenCount : 806
ReasoningTokenCount :
TotalTokenCount : 822
----------------------------------------
========================================
2025/2/21 上午 09:09:54  [User]: 形容一棵樹
Role : Assistant
2025/2/21 上午 09:10:07 形容一棵?的方式有很多,取?于你想表?的具体特征和情感。以下是一些形容?和句子作?例子:

1. 壯觀的(Majestic)- ?棵?以其宏?的姿?傲立在山?,仿若一位年?的守?者,俯瞰?周?的一切。

2. 繁茂的(Lush)- ?上??繁茂,?炎?的夏日提供了一??爽的避?所。

3. 古老的(Ancient)- ?干扭曲粗?,??了??年的??雨雨,它的存在是??的??者。

4. 粗糙的(Rough)- ?干上皮糙肉厚,布?了?月的?路,摸起?如同?史的碑文。

5. 高?的(Towering)- ?冠高?入云,仿佛要触及天空的??。

6. 枯萎的(Withered)- ?枝枯萎?力,?子?了,生命力似乎正在慢慢流逝。

7. 芬芳的(Fragrant)- ?花季??,?上的花朵散?出??芬芳,吸引?了蜂蝶的舞?。

8. 落幕的(Deciduous)- ??秋天的到?,??如同金?的地毯一般落?了地面。

9. 持久的(Sturdy)- ?管???吹雨打,?棵?依然屹立不倒,根深蒂固,在大自然中?得十分??。

10. 生机勃勃的(Vibrant)- 春天???,?上的新芽嫩?欲滴,生机勃勃,充?了希望和活力。

形容一棵?,你可以?它的外?(如高矮、粗?、?色)、年?(如古老或年?)、?境(如孤立或森林中)、季??化(落?或常青)、生命力(如?健或 瑟?)等多??度去描述。
InputTokenCount : 15
OutputTokenCount : 701
ReasoningTokenCount :
TotalTokenCount : 716
----------------------------------------
========================================
2025/2/21 上午 09:10:07  [User]: 形容一棵樹
Role : Assistant
2025/2/21 上午 09:10:21 形容一棵樹可以從多個角度進行,包括它的外觀、生長環境、生態作用以及與人類的關聯等。以下是一個例子:

這棵樹挺拔而雄偉,它的樹幹粗壯而堅固,像是一位經歷風霜的老士兵,屹立不搖。樹皮粗糙,刻滿了時間的痕跡,透露出一種古老而神秘的氣 息。它的枝椏繁茂,葉片綠油油,如同無數綠色的小手在空中輕輕搖曳,與陽光嬉戲。在春天,樹上可能開滿了花朵,散發著淡淡的香氣,吸引 著蜜蜂和蝴蝶前來採蜜。夏天時,它提供了一片涼爽的陰涼,成為行人休憩的好地方。秋天,樹葉漸漸變成金黃色或火紅色,像是為大地披上了 一層絢爛的地毯。冬天,樹枝裸露,顯得格外堅韌,即使覆蓋著白雪,依然挺立,等待春天的到來。

這棵樹不僅是自然界的一部分,也是許多生物的家園,鳥兒在它的枝頭築巢,昆蟲在它的樹皮下尋找食物。對於人類而言,它可能是一個歷史的 見證者,見證了周圍環境的變遷和人們生活的點滴。它是大自然的一份子,也是我們共同的寶貴財富。
InputTokenCount : 15
OutputTokenCount : 604
ReasoningTokenCount :
TotalTokenCount : 619
---------------------------------------- 

======================================== 




2025年2月4日 星期二

.NET MAUI MAUI 捕捉到無法預期例外異常,並將其寫入剪貼簿

.NET MAUI MAUI 捕捉到無法預期例外異常,並將其寫入剪貼簿

對於開發出來的軟體程式,在執行時期會有可能遭遇到不同的例外異常,這些例外異常可能是由於程式碼的錯誼、資料的錯誤、或是外部環境的問題所導致。而在傳統的桌機應用程式或者Web應用程式,面對到這樣的問題,可以透過 C# 內的 try-catch-finally 來進行例外異常的處理。

當捕說到例外異常物件後,便可以將該例外異常物件的資訊內容,寫入到當時執行主機上的檔案、資料庫、或是透過網路傳送到遠端的伺服器上。這樣的做法,可以讓開發者在程式發生問題時,可以透過這些例外異常的資訊內容,來進行問題的追蹤與解決。

面對這樣技術可能會在行動裝置類型的應用程式開發上遇到困境,這是因為若行動裝置遇到閃退的問題,雖然應用程式可以將例外異常資訊寫入到檔案或者透過網路傳送到遠端主機上,但是,若該應用程式一啟動就產生閃退問題,此時,根本沒有機會透過應用程式顯示出例外異常的 Log 資訊,更不用說透過其他工具將 Log 檔案將讀取出來;透過網路將 Log 寫到遠端主機也會面臨到因為行動網路的問題,導致無法將當時例外異常資訊寫入到遠端主機上。

若開發者無法透過有效的方式取得當時發生例外異常的資訊,便會很難判斷出真正發生原因的問題在哪裡,進行能夠在短時間內快速解決問題,這樣的問題將會對應用程式的品質與使用者的體驗造成很大的影響。

在 .NET MAUI 應用程式開發上,若要解決這樣的問題,可以透過將例外異常的資訊內容寫入到剪貼簿上,這樣的做法可以讓使用者在應用程式閃退時,可以透過手機的剪貼簿功能,將例外異常的資訊內容複製到其他地方,進行問題的追蹤與解決。例如,將這些例外異常詳細記錄,貼到電子郵件內,寄送給開發人員或者可以透過聊天軟體傳送給開發人員。

在這篇文章中,將會介紹如何在 .NET MAUI 應用程式開發上,透過 Prism 開發框架,來捕捉到無法預期的例外異常,並將其寫入到剪貼簿上,讓使用者可以透過手機的剪貼簿功能,將例外異常的資訊內容複製到其他地方,進行問題的追蹤與解決。

建立採用 Prism 開發框架的 MAUI 專案

  • 打開 Visual Studio 2022 IDE 應用程式

  • 從 [Visual Studio 2022] 對話窗中,點選右下方的 [建立新的專案] 按鈕

  • 在 [建立新專案] 對話窗右半部

    • 切換 [所有語言 (L)] 下拉選單控制項為 [C#]
    • 切換 [所有專案類型 (T)] 下拉選單控制項為 [MAUI]
  • 在中間的專案範本清單中,找到並且點選 [Vulcan Custom Prism .NET MAUI App] 專案範本選項

    若沒有看到這個專案範本,請參考 使用 Vulcan.Maui.Template 專案範本來進行 MAUI for Prism 專案開發 文章,進行安裝這個專案範本到 Visual Studio 2022 內

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

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

  • 在 [專案名稱] 欄位內輸入 mauiExceptionToclipboard 做為這個專案名稱

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

  • 此時,將會建立一個可以用於 MAUI 開發的專案

修正 MainPage 之 View & ViewModel

  • 打開 [Views] 資料夾下的 [MainPage.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"
             Title="{Binding Title}"
             x:Class="mauiExceptionToclipboard.Views.MainPage"
             xmlns:viewModel="clr-namespace:mauiExceptionToclipboard.ViewModels"
             x:DataType="viewModel:MainPageViewModel">

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

            <Editor HeightRequest="300"/>
            
            <Label Text="Hello, World!"
             SemanticProperties.HeadingLevel="Level1"
             FontSize="32"
             HorizontalOptions="Center" />

            <Label Text="Welcome to Prism for .NET MAUI"
             SemanticProperties.HeadingLevel="Level2"
             SemanticProperties.Description="Welcome to Prism for dot net Multi platform App U I"
             FontSize="18"
             HorizontalOptions="Center" />

            <Button Text="{Binding Text}"
              SemanticProperties.Hint="Counts the number of times you click"
              Command="{Binding CountCommand}"
              HorizontalOptions="Center" />

            <HorizontalStackLayout Spacing="20" HorizontalOptions="Center">
                <Button Text="例外異常" Command="{Binding ThrowUnhandleExceptionCommand}"/>
                <Button Text="內部例外異常" Command="{Binding ThrowUnhandleInnerExceptionCommand}"/>
                <Button Text="聚合例外異常" Command="{Binding ThrowUnhandleAggregateExceptionCommand}"/>
            </HorizontalStackLayout>

            <HorizontalStackLayout Spacing="20" HorizontalOptions="Center">
                <Button Text="清空剪貼簿" Command="{Binding CleanClipboardCommand}"/>
            </HorizontalStackLayout>

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

在這個頁面的標記宣告中,設計了四個按鈕,分別是用來觸發不同類型例外異常的按鈕,以及用來清空剪貼簿的按鈕。

另外,這裡也使用了 Editor 控制項,這個控制項是用來讓使用者可以將剪貼簿內的例外異常文字內容,貼到這個 Editor 控制項內,這樣可以讓使用者在發生例外異常時,可以將這些文字內容複製到剪貼簿上,進行問題的追蹤與解決。

  • 打開 [ViewModels] 資料夾下的 MainPageViewModel.cs 檔案
  • 使用底下程式碼替換掉這個原有檔案內容
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;

namespace mauiExceptionToclipboard.ViewModels;

public partial class MainPageViewModel : ObservableObject, INavigatedAware
{
    #region Field Member
    private int _count;
    private readonly INavigationService navigationService;

    #endregion

    #region Property Member
    [ObservableProperty]
    string title = "Main Page";

    [ObservableProperty]
    string text = "Click me";
    #endregion

    #region Constructor
    public MainPageViewModel(INavigationService navigationService)
    {
        this.navigationService = navigationService;
    }
    #endregion

    #region Method Member
    #region Command Method
    [RelayCommand]
    async Task CleanClipboard()
    {
        await Clipboard.Default.SetTextAsync(null);
    }
    [RelayCommand]
    void ThrowUnhandleException()
    {
        throw new Exception("喔喔,這裡發生例外異常");
    }

    [RelayCommand]
    void ThrowUnhandleAggregateException()
    {
        var exceptions = new List<Exception>();

        exceptions.Add(new ArgumentException("Argument Exception Message"));
        exceptions.Add(new NullReferenceException("Null Reference Exception Message"));

        throw new AggregateException("Aggregate Exception Message", exceptions);
    }

    [RelayCommand]
    void ThrowUnhandleInnerException()
    {
        try
        {
            throw new Exception("喔喔,這裡發生例外異常");
        }

        catch (ArgumentException e)
        {
            //make sure this path does not exist
            if (File.Exists("file://Bigsky//log.txt%22)%20==%20false") == false)
            {
                throw new FileNotFoundException("File Not found when trying to write argument exception to the file", e);
            }
        }
    }

    [RelayCommand]
    private void Count()
    {
        _count++;
        if (_count == 1)
            Text = "Clicked 1 time";
        else if (_count > 1)
            Text = $"Clicked {_count} times";
    }
    #endregion

    #region Navigation Event
    public void OnNavigatedFrom(INavigationParameters parameters)
    {
    }

    public void OnNavigatedTo(INavigationParameters parameters)
    {
    }
    #endregion

    #region Other Method
    #endregion
    #endregion
}

在這個 ViewModel 類別中,設計了四個按鈕的 Command 方法,分別是用來觸發不同類型例外異常的按鈕,以及用來清空剪貼簿的按鈕。

對於 [CleanClipboard()] 這個命令方法,將會使用 .NET MAUI 提供的 平台功能 的 Clipboard 類別,來清空剪貼簿內的文字內容。這裡將會執行 await Clipboard.Default.SetTextAsync(null); 這行程式碼,來清空剪貼簿內的文字內容。

對於 [ThrowUnhandleException()] 這個命令方法,將會使用 throw new Exception("喔喔,這裡發生例外異常"); 這行程式碼,來觸發一個例外異常。

對於 [ThrowUnhandleAggregateException()] 這個命令方法,將會使用 throw new AggregateException("Aggregate Exception Message", exceptions); 這行程式碼,來觸發一個聚合例外異常。

對於 [ThrowUnhandleInnerException()] 這個命令方法,將會使用 throw new Exception("喔喔,這裡發生例外異常"); 這行程式碼,來觸發一個例外異常,並且在 catch 區塊內,再次觸發一個內部例外異常。

執行與確認結果

  • 底下是在 Android 模擬器內執行的結果

現在可以點選 [例外異常] 按鈕,來觸發一個例外異常,這個例外異常將會被 Prism 捕捉到,並且將例外異常的資訊內容寫入到剪貼簿上。

不過此時,該 App 也因為這個例外異常產生了閃退問題,所以無法透過 App 來查看這個例外異常的資訊內容。

再次重新啟動該 App,並且將剪貼簿內的文字內容,貼到 Editor 控制項內,來查看這個例外異常的資訊內容。

底下將會是這個例外異常的資訊內容

System.Exception: 喔喔,這裡發生例外異常
   at mauiExceptionToclipboard.ViewModels.MainPageViewModel.ThrowUnhandleException() in C:\Vulcan\Github\CSharp2024\mauiExceptionToclipboard\mauiExceptionToclipboard\ViewModels\MainPageViewModel.cs:line 39
   at CommunityToolkit.Mvvm.Input.RelayCommand.Execute(Object parameter) in /_/src/CommunityToolkit.Mvvm/Input/RelayCommand.cs:line 77
   at Microsoft.Maui.Controls.ButtonElement.ElementClicked(VisualElement visualElement, IButtonElement ButtonElementManager) in /_/src/Controls/src/Core/Button/ButtonElement.cs:line 39
   at Microsoft.Maui.Controls.Button.SendClicked() in /_/src/Controls/src/Core/Button/Button.cs:line 250
   at Microsoft.Maui.Controls.Button.Microsoft.Maui.IButton.Clicked() in /_/src/Controls/src/Core/Button/Button.cs:line 484
   at Microsoft.Maui.Handlers.ButtonHandler.OnClick(IButton button, View v) in /_/src/Core/src/Handlers/Button/ButtonHandler.Android.cs:line 151
   at Microsoft.Maui.Handlers.ButtonHandler.ButtonClickListener.OnClick(View v) in /_/src/Core/src/Handlers/Button/ButtonHandler.Android.cs:line 172
   at Android.Views.View.IOnClickListenerInvoker.n_OnClick_Landroid_view_View_(IntPtr jnienv, IntPtr native__this, IntPtr native_v) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/obj/Release/net8.0/android-34/mcw/Android.Views.View.cs:line 2374 

   at Android.Runtime.JNINativeWrapper.Wrap_JniMarshal_PPL_V(_JniMarshal_PPL_V callback, IntPtr jnienv, IntPtr klazz, IntPtr p0) in /Users/runner/work/1/s/xamarin-android/src/Mono.Android/Android.Runtime/JNINativeWrapper.g.cs:line 121