顯示具有 Xamarin.Forms 標籤的文章。 顯示所有文章
顯示具有 Xamarin.Forms 標籤的文章。 顯示所有文章

2022年8月12日 星期五

使用 Code Behind 取得裝置畫素與設計尺寸

使用 Code Behind 取得裝置畫素與設計尺寸

對於想要使用 .NET MAUI 來進行跨平台開發的程式設計師,大部分的人最初心選擇將可能是採用 Code Behind 模式,而不是採用 MVVM 的方式,我猜想可能的原因是,絕大部分第一次採用 MAUI 開發的人,對於 MVVM 不太孰悉,有或者是有些懼怕這個新的設計模式的使用,也對於資料綁定 Data Binding 的用法有所懼怕吧。

在這裡將會使用 Code Behind 的方式,透過 .NET MAUI 內建的 平臺整合 內的 裝置顯示資訊 API ,來讀取這些數據。

在這個練習中,其實最重要的是要表達設計尺寸這樣的觀念

建立 .NET MAUI 應用程式 專案

  • 開啟 Visual Studio 2022 Preview 版本

  • 點選螢幕右下角的 [建立新的專案] 按鈕

  • 切換右上角的 [所有專案類型] 下拉選單控制項

  • 找到並且點選 [MAUI] 這個選項

  • 從清單中找到並選擇 [.NET MAUI 應用程式] 這個專案範本

    此專案可用於建立適用於 iOS、Android、Mac Catalyst、Tizen和WinUI 的 .NET MAUI 應用程式

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

  • 當出現了 [設定新的專案] 對話窗

  • 在 [專案名稱] 欄位內,輸入 MA02

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

  • 當出現了 [其他資訊] 對話窗

  • 對於 [架構] 的下拉選單控制項,使用預設值

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

修正 View 檢視頁面

  • 在專案根目錄找到並且打開 [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="MA02.MainPage"
             Title="裝置螢幕資訊與Code Behind">

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

            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />

            <Label
                x:Name="labelDisplay"/>

            <Button
                x:Name="btnGetDisplayInformation"
                Clicked="btnGetDisplayInformation_Clicked"
                Text="取得螢幕畫素資訊"/>

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

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

            <Button
                x:Name="CounterBtn"
                Text="Click me"
                SemanticProperties.Hint="Counts the number of times you click"
                Clicked="OnCounterClicked"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

在這裡,首先在這個根項目 Root Element ,也就是 ContentPage 內容頁面內,加入一個 Title 屬性,在這裡宣告了這個頁面的主題名稱,而這個文字將會顯示在這個頁面最上方。

在這個 版面配置 Layout 的 VerticalStackLayout 裡面,加入兩個檢視,第一個為文字標籤 <Label x:Name="labelDisplay"/>,在這個文字標籤內,使用標記延伸 Markup Extension 語法,也就是 x:Name 宣告這個控制項將會產生一個欄位名稱,也就是 labelDisplay,這個欄位將可以用於這個頁面類別內,讓各種方法或者事件來存取,如此,便可以透過這個物件來變更該文字標籤控制項的各個屬性或者樣貌。

底下將會是 .NET MAUI 根據 MainPage.xaml 這個 XAML 文件檔案內容,採用 原始碼產生器 技術,所產生出來的一個 MainPage 類別。

從這裡首先得知,每個頁面 .xaml 檔案,最終都會形成 .NET 中的一個類別,而在此 XAML 文件中宣告的各個 版面配置、檢視,都會透過這裡的敘述 global::Microsoft.Maui.Controls.Xaml.Extensions.LoadFromXaml(this, typeof(MainPage)); 產生出相對應的 .NET 執行個體,總而言之,這些 xaml 內容,都會有相對應的 .NET 物件存在,每個 XAML 物件,都有相對應的 .NET 類別。

最後,因為在 XAML 文件中有使用到 x:Name="..." 這樣的 XAML 標記延伸 敘述,因此,將會在這個頁面類別內,宣告一個私有欄位 private global::Microsoft.Maui.Controls.Label labelDisplay; ,並且在建構式內,取得這個私有欄位對應的實際 XAML 中宣告的檢視物件 labelDisplay = global::Microsoft.Maui.Controls.NameScopeExtensions.FindByName<global::Microsoft.Maui.Controls.Label>(this, "labelDisplay");

為了要能夠做到與使用者互動機制,因此,加入一個按鈕 Button 按鈕,並且在這個按鈕的 Clicked 屬性中,宣告綁定一個事件 btnGetDisplayInformation_Clicked ,這個事件將會於 Code Behind 區段內進行設計相關商業邏輯程式碼。

[assembly: global::Microsoft.Maui.Controls.Xaml.XamlResourceId("MA02.MainPage.xaml", "MainPage.xaml", typeof(global::MA02.MainPage))]
namespace MA02
{
	[global::Microsoft.Maui.Controls.Xaml.XamlFilePath("MainPage.xaml")]
	public partial class MainPage : global::Microsoft.Maui.Controls.ContentPage
	{
		[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Maui.Controls.SourceGen", "1.0.0.0")]
		private global::Microsoft.Maui.Controls.Label labelDisplay;

		[global::System.CodeDom.Compiler.GeneratedCode("Microsoft.Maui.Controls.SourceGen", "1.0.0.0")]
		private global::Microsoft.Maui.Controls.Button btnGetDisplayInformation;

        ...

		private void InitializeComponent()
		{
			global::Microsoft.Maui.Controls.Xaml.Extensions.LoadFromXaml(this, typeof(MainPage));
			labelDisplay = global::Microsoft.Maui.Controls.NameScopeExtensions.FindByName<global::Microsoft.Maui.Controls.Label>(this, "labelDisplay");
			btnGetDisplayInformation = global::Microsoft.Maui.Controls.NameScopeExtensions.FindByName<global::Microsoft.Maui.Controls.Button>(this, "btnGetDisplayInformation");
			
            ...
		}
	}
}

修正 View 檢視頁面 Code Behind 程式碼

  • 在專案根目錄找到並且打開 [MainPage.xaml.cs] 檔案
  • 使用底下 C# 程式碼替換這個檔案內容
namespace MA02;

public partial class MainPage : ContentPage
{
    int count = 0;

    public MainPage()
    {
        InitializeComponent();
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);
    }

    private void btnGetDisplayInformation_Clicked(object sender, EventArgs e)
    {
        double designWidth =this.Width;
        double designHeight = this.Height;
        IDeviceDisplay display = DeviceDisplay.Current;
        labelDisplay.Text = $"螢幕轉向:{display.MainDisplayInfo.Orientation}{Environment.NewLine}" +
            $"螢幕寬畫素:{display.MainDisplayInfo.Width}{Environment.NewLine}" +
            $"螢幕高畫素:{display.MainDisplayInfo.Height}{Environment.NewLine}" +
            $"設計寬畫素:{designWidth}{Environment.NewLine}" +
            $"設計高畫素:{designHeight}{Environment.NewLine}" +
            $"螢幕密度:{display.MainDisplayInfo.Density}{Environment.NewLine}"+
            $"螢幕更新頻率:{display.MainDisplayInfo.RefreshRate}{Environment.NewLine}";
    }
}

這個類別 MainPage ,設計為一個 partial class 部分類別,那表示了在另外一個地方,也就是原始碼產生器產生的地方,將會產生一個同樣名稱的部分類別,在整個專案編譯過程中,將會把這兩個部分類別程式碼組合成為同一個類別。

在最後面,加入一個 private void btnGetDisplayInformation_Clicked(object sender, EventArgs e) 事件委派方法,在該方法內首先取得這個內容頁面的設計畫素寬度與高度值,接著透過 平台整合 內的 DeviceDisplay.Current 屬性,取得當前這個裝置的螢幕相關資訊,在此將會取得與螢幕有關的相關資訊,例如,這個裝置的真正可用寬度與高度的畫素,現在的密度,也稱之為縮放比率。一旦取得這些資訊之後,便可以將這些字串設定到螢幕上的 文字標籤 檢視的 Text 屬性內,這樣,當使用者點選這個按鈕之後,就可以看到這些相關資訊在畫面中了。

在 Android 平台執行專案

  • 點選中間上方工具列的 [Windows Machine] 這個工具列按鈕旁的下拉選單三角形

  • 從彈出功能表中,找到 [Android Emulators] 內的任何一個模擬器

  • 接者,開始執行這個專案,讓他可以在 Android 模擬器出現

  • 此時,終於可以看到 [裝置螢幕資訊與Code Behind] 這個頁面了

  • 請點選 [取得螢幕畫素資訊] 按鈕

  • 將會看到底下資訊

  • 將這個螢幕旋轉 90 度

  • 請點選 [取得螢幕畫素資訊] 按鈕

  • 將會看到底下資訊

 






2022年8月2日 星期二

MAUI : 資料綁定 Data Binding - 5 在 Maui 專案內,如何得知 ViewModel 內的屬性產生異動,而 View 可以收到通知呢?

資料綁定 Data Binding - 5 在 Maui 專案內,如何得知 ViewModel 內的屬性產生異動,而 View 可以收到通知呢?

在最後對於資料綁定 Data Binding 的 INotifyPropertyChanged 的用法,將會回到 MAUI 專案內來實際觀察,在這篇文章中,將會使用 CommunityToolkit.Mvvm 這個套件來進行檢測

系列文章清單

1 自行建置 INotifyPropertyChanged 介面

2 設計基底類別,透過繼承可以使用屬性變更通知

3 PropertyChanged.Fody 套件,大幅簡化屬性變更通知程式設計碼

4 CommunityToolkit.Mvvm 套件,透過原始碼產生來簡化屬性變更通知程式設計碼

5 在 Maui 專案內,如何得知 ViewModel 內的屬性產生異動,而 View 可以收到通知呢?

建立新專案

  • 開啟 Visual Studio 2022 Preview 開發工具

    在這個時間點,若想要使用 MAUI 專案來進行開發,需要安裝 Visual Studio 2022 17.3 Preview 版本,才能夠順利建立 MAUI 專案

  • 當 [Visual Studio 2022 Preview] 對話窗出現的時候

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

  • 現在將看到 [建立新專案] 對話窗

  • 切換右上角的 [所有專案類型] 下拉選單控制項

  • 從清單中找到並選擇 [.NET MAUI 應用程式] 這個專案範本

    此專案可用於建立適用於 iOS、Android、Mac Catalyst、Tizen 和 WinUI 的 NET MAUI 應用程式

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

  • 此時將會看到 [設定新的專案] 對話窗

  • 在 [專案名稱] 欄位,輸入 mauiMonitorPropertyChanged

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

  • 最後會看到 [其他資訊] 對話窗

  • 使用預設設定值,也就是 [架構] 為 [.NET 6.0 (長期支援)]

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

加入 CommunityToolkit.Mvvm 的 NuGet 套件

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

進行 ViewModel 的設計

  • 滑鼠右擊專案節點
  • 從彈出功能表中,點選 [加入] > [類別]
  • 當出現 [新增項目 - mauiMonitorPropertyChanged] 對話窗後
  • 在下方的 [名稱] 欄位內,輸入 MainPageViewModel
  • 點選對話窗右下角的 [新增] 按鈕
  • 使用底下 C# 程式碼將這個檔案內容替換掉
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace mauiMonitorPropertyChanged
{
    using CommunityToolkit.Mvvm.ComponentModel;
    using CommunityToolkit.Mvvm.Input;

    public partial class MainPageViewModel : ObservableObject
    {
        public MainPageViewModel()
        {
            person = new Person();
        }
        [ObservableProperty]
        private Person person;

        [RelayCommand]
        void ChangeData()
        {
            Person.Name = "Vulcan Lee";
            Person.Age = 25;
        }
    }
    public partial class Person : ObservableObject
    {
        [ObservableProperty]
        private string name;

        [ObservableProperty]
        private int age;

        private string customDesign;
        public string CustomDesign
        {
            get => name;
            set => SetProperty(ref customDesign, value);
        }
    }
}

在這裡設計一個 Person 類別,並且透過 CommunityToolkit.Mvvm 套件的原始碼自動產生機制,幫助產生出相關的 PropertyChanged 屬性變更事件需要用到的相關程式碼。

在最上面則是建立一個新的類別,將會是 MainPage 這個 XAML 頁面需要用到的 ViewModel,因此,在這裡將會把這個 ViewModel 類別命名為 MainPageViewModel。

在 MainPageViewModel 建構式內,首先將 Person 這個屬性建立初始物件值,避免之後產生 null reference 空值存取的問題。

這裡設計一個 void ChangeData() 方法,一旦執行這個方法之後,將會變更 Person 物件內的 Name 與 Age 屬性值,其實,這是一個用於命令要綁定的委派方法,在這裡,透過在此方法上方加入了 [RelayCommand] 屬性宣告,讓這個方法可以用於 XAML 頁面上來進行命令的綁定,因此,在 MainPage.xaml 檔案中,將會看到有個 ChangeDataCommand 這樣的物件可以選擇。

進行 View 與 ViewModel 的服務註冊

接下來要來進行使用 DI Container 相依性服務容器的註冊工作,要使用這樣的功能,將會在取得頁面物件的時候,可以自動注入相對應的 ViewModel 物件來使用。

  • 在專案根目錄找到並且打開 [MauiProgram.cs] 檔案
  • 在最後面找到 return builder.Build(); 敘述
  • 在此敘述前,加入底下程式碼
#region 在這裡進行 ViewModel 的註冊
builder.Services.AddSingleton<MainPage>();
builder.Services.AddSingleton<MainPageViewModel>();
#endregion

修正 View XAML 內容,加入 Entry 控制項與宣告與 ViewModel 的資料綁定設定

  • 在專案根目錄下,找到並且打開 [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"
             xmlns:local="clr-namespace:mauiMonitorPropertyChanged"
             x:DataType="local:MainPageViewModel"
             x:Class="mauiMonitorPropertyChanged.MainPage">

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

            <Entry Placeholder="請輸入姓名"
                   x:Name="entryName"
                   Text="{Binding Person.Name}"/>
            <Entry Placeholder="請輸入年紀"
                   x:Name="entryAge"
                   Text="{Binding Person.Age}"/>
            <Button Text="變更屬性"
                    Command="{Binding ChangeDataCommand}"/>
            
            <Image
                Source="dotnet_bot.png"
                SemanticProperties.Description="Cute dot net bot waving hi to you!"
                HeightRequest="200"
                HorizontalOptions="Center" />

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

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

            <Button
                x:Name="CounterBtn"
                Text="Click me"
                SemanticProperties.Hint="Counts the number of times you click"
                Clicked="OnCounterClicked"
                HorizontalOptions="Center" />

        </VerticalStackLayout>
    </ScrollView>

</ContentPage>

在根目錄節點, ContentPage 內,宣告一個 local 命名空間,這次要用來指定該頁面 ViewModel 所在的命名空間位置,接下來的 x:DataType , 這是用於 編譯的系結 ,在微軟官方文件上,是這樣描述的 : 編譯的系結藉由在編譯時期解析系結運算式,而不是執行時間,以改善 .NET MAUI 應用程式中的資料系結效能。 此外,在編譯時間驗證繫結運算式可改善開發人員的疑難排解體驗,因為無效的繫結會回報為建置錯誤。

我非常熱愛這項功能,因為,可以讓我在設計 XAML 的時候,即時得知綁定的物件名稱是甚麼與是否正確,避免要等到執行時期才知道發生了問題。

接著,宣告了兩個 Entry 控制項與一個按鈕,前面兩個控制項用於顯示與讓使用者輸入 姓名 與 年紀 的內容,而後者的按鈕控制項,透過 Command 這個屬性值,來綁定到 ViewModel 內的這個具有實作 ICommand 的物件 (ChangeDataCommand 這個物件,是由 CommunityToolkit.Mvvm 透過原始碼產生器自動產生的,在 ViewModel 內的綁定方法為 ChangeData)

修正 Code Behind 程式碼,注入 ViewModel 與 綁定屬性變更通知

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

namespace mauiMonitorPropertyChanged;

public partial class MainPage : ContentPage
{
    public MainPageViewModel MainPageViewModel { get; }
    int count = 0;

    public MainPage(MainPageViewModel mainPageViewModel)
    {
        InitializeComponent();
        MainPageViewModel = mainPageViewModel;
        this.BindingContext = MainPageViewModel;

        MainPageViewModel.Person.PropertyChanging += (s, e) =>
        {
            Debug.WriteLine($"   ViewModel.Person : {e.PropertyName} 正在進行變更完成");
        };
        MainPageViewModel.Person.PropertyChanged += (s, e) =>
        {
            Debug.WriteLine($"   ViewModel.Person :  {e.PropertyName} 已經變更完成");
        };
        entryName.PropertyChanged += (s, e) =>
        {
            Debug.WriteLine($"   姓名控制項 {e.PropertyName} 已經變更完成");
        };
        entryName.PropertyChanging += (s, e) =>
        {
            Debug.WriteLine($"   姓名控制項 {e.PropertyName} 正在進行變更完成");
        };
    }

    private void OnCounterClicked(object sender, EventArgs e)
    {
        count++;

        if (count == 1)
            CounterBtn.Text = $"Clicked {count} time";
        else
            CounterBtn.Text = $"Clicked {count} times";

        SemanticScreenReader.Announce(CounterBtn.Text);
    }
}

在這裡的 Code Behind 程式碼中,透過了建構式注入方式,取得該頁面要使用的 ViewModel 物件,也就是 MainPageViewModel。接著,將這個 ViewModel 物件設定到 XAML ContentPage 內的 BindingContext 屬性內。

在建構式內,還針對的姓名文字輸入盒 Entry 這個控制項,訂閱綁定了該控制項的 PropertyChanging 與 PropertyChanged 這兩個事件,所以,當在使用這個 Entry 控制項的時候,有宣告某些屬性有進行資料綁定的行為,並且這個屬性值有變更的時候,就會觸發這了 Lambda 事件。

另外,還針對 ViewModel 內的 Person 物件,同樣的也訂閱了 PropertyChanging 與 PropertyChanged 這兩個事件,一旦這些事件被觸發之後,將會顯示訊息文字在 Visual Studio 輸出視窗內。

進行測試與觀察

由於 .NET MAUI 專案具有單一專案,但是可以在不同平台下執行的特性,因此,在這裡將會選擇最簡單的執行方式,那就是選擇使用 Windows 平台下來執行

  • 點選最上方中間工具列區域的 [Windows Machine] 按鈕

  • 這個專案將會產生出一個 WinUI 3 的應用程式

  • 執行結果畫面如下

  • 點選 [變更屬性] 按鈕

    此時將會觸發 ViewModel 內的 ChangeData() 方法,在這個方法內,會變更 Person 物件內的 Name 與 Age 這兩個屬性值

  • 從 Visual Studio 2022 [輸出] 視窗內,將會看到底下輸出內容

ViewModel.Person : Name 正在進行變更完成
姓名控制項 Text 正在進行變更完成
姓名控制項 Text 已經變更完成
ViewModel.Person :  Name 已經變更完成
ViewModel.Person : Age 正在進行變更完成
姓名控制項 Height 正在進行變更完成
姓名控制項 Height 已經變更完成
ViewModel.Person :  Age 已經變更完成
姓名控制項 CursorPosition 正在進行變更完成
姓名控制項 CursorPosition 已經變更完成

從輸出內容可以看出,一旦按鈕被點擊後,就會變更 ViewModel 內的 Person.Name 的屬性值,在變更之前,將會顯示 [ViewModel.Person : Name 正在進行變更完成] ,緊接著因為 Name 這個屬性有綁定到 Entry.Text 這個可綁定屬性上,因此,將會觸發其事件,所以,將會顯示出 [姓名控制項 Text 正在進行變更完成],當都變更完成後,就會顯示出 [姓名控制項 Text 已經變更完成] 與 [ViewModel.Person : Name 已經變更完成] 這些內容

從上述的執行結果,可以看到透過 PropertyChanged 這樣的屬性變更通知機制,使得 XAML 的資料綁定作業可以無縫的運作起來。