XAML in Xamarin.Forms 基礎篇 電子書

XAML in Xamarin.Forms 基礎篇 電子書
XAML in Xamarin.Forms 基礎篇 電子書

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書
顯示具有 XAML 標籤的文章。 顯示所有文章
顯示具有 XAML 標籤的文章。 顯示所有文章

2019/06/18

如何使用 On_PropertyName_Changed 與 附加屬性 Attached Properties 和行為 Behavior,來得知 Entry 與 Editor 已經輸入了多少文字

如何使用 On_PropertyName_Changed 與 附加屬性 Attached Properties 和行為 Behavior,來得知 Entry 與 Editor 已經輸入了多少文字

當在進行 Xamarin.Forms 專案開發的時候,有些時候需要得知 Entry 或者 Editor 這兩種檢視或者稱為控制向,使用者已經輸入了多少內容或者字數,通常可以透過這兩個控制項的 Entry.TextChanged 事件或者 Editor.TextChanged 事件來得知使用者已經輸入了甚麼內容,這樣就可以計算出使用者已經輸入多少文字內容,可是,若要使用事件的話,就需要使用該頁面的 Code Behind 的方式來撰寫 C# 事件委派方法,讓使用者輸入文字的時候,可以觸發這個事件,以便進行計算字數工作。
在使用 Prism 這類 MVVM 開發框架的設計模式的時候,幾乎所有的商業邏輯都會撰寫到 View (頁面) 的 ViewModel (檢視模型) 上,如此,要如何得知使用者已經在 Entry / Editor 檢視中輸入甚麼內容呢?這裡將會提供底下方式來解決此一需求。
這篇文章的範例程式原始碼,可以從 GitHub 取得。

PropertyChanged.Fody 套件提供的 屬性 變更觸發事件

在 PropertyChanged.Fody 的網頁中,將會有一篇關於 On_PropertyName_Changed 文章,告訴 Xamarin.Forms 開發者如何在 ViewModel 中來撰寫出當所綁定的屬性值有異動的時候,將會觸發一個 ViewModel 上的委派方法。
在此應用中,將會在頁面中使用底下 XAML 進行宣告一個 Entry 檢視,其中,當使用者輸入的文字內容,將會自動綁定到 ViewModel 類別內的 C# Property 屬性 EntryChangedInputText 上,而 ViewModel 類別內的 C# Property 屬性 EntryChangedInputTextLength 將會是該輸入文字的字數數值。
xaml
        <Label Text="{Binding EntryChangedInputTextLength}"/>
        <Entry
            Text="{Binding EntryChangedInputText}"/>
現在,請在該頁面的檢視模型 ViewModel 中,定義一個方法,其方法名稱必須為 On屬性名稱Changed ,在這裡將會定義一個 OnEntryChangedInputTextChanged 方法,當這個 Xamarin.Forms 專案執行的時候,使用者在該 Entry 輸入任何文字,將會觸發 OnEntryChangedInputTextChanged 方法執行,在此方法內,將會撰寫符合需求的計算字數商業邏輯程式碼,計算完成之後,就可以儲存到 EntryChangedInputTextLength 屬性內,如此,就可以在螢幕上看到該輸入字數數量了。
        public string EntryChangedInputText { get; set; }
        public int EntryChangedInputTextLength { get; set; } 
        public void OnEntryChangedInputTextChanged()
        {
            if(string.IsNullOrEmpty(EntryChangedInputText))
            {
                EntryChangedInputTextLength = 0;
            }
            else
            {
                EntryChangedInputTextLength = EntryChangedInputText.Length;
            }
        }

使用 Prism 提供的事件 To 命令的行為擴充功能

Prism 這個 MVVM 開發框架,提供了一個 Xamarin.Forms Behavior 行為的物件,其中一個加值行為就是可以設定當某個事件被觸發的時候,需要執行某個命令;如此,使用這個方法將會把相關商業處理邏輯寫道 ViewModel 上,而不在需要撰寫 Code Behind 程式碼了。
首先,請先在該 ContentPage 節點上宣告一個新的命名空間 xmlns:behavior="clr-namespace:Prism.Behaviors;assembly=Prism.Forms" ,這裡將會宣告一個前置詞 Prefix , behavior ,指向 Prims 行為擴充功能組件上,現在,可以在這個頁面上,加入一個 Label 與 Entry,其中 Entry 需要使用 Event.Behaviors 的屬性項目 Property Element 方式,指定新加入的行為物件;這裡將會使用 behavior:EventToCommandBehavior 這個型別,來使用 EventName 這個屬性來指定觸發事件的名稱與 Command 來指定要綁定到 ViewModel 中的一個 DelegateCommand 屬性物件。
xaml
        <Label Text="{Binding EntryBehaviorInputTextLength}"/>
        <Entry
            Text="{Binding EntryBehaviorInputText}">
            <Entry.Behaviors>
                <behavior:EventToCommandBehavior
                    EventName="TextChanged" Command="{Binding EntryChangedCommand}"/>
            </Entry.Behaviors>
        </Entry>
在 ViewModel 內,會在建構函式內建立一個 DelegateCommand 型別的物件到 EntryChangedCommand 屬性上,在建立這個 EntryChangedCommand 屬性的時候,將會傳入一個委派方法,這個委派方法將會用來轉寫計算使用者輸入字數的相關程式碼。
        public string EntryBehaviorInputText { get; set; }
        public int EntryBehaviorInputTextLength { get; set; }
        public DelegateCommand EntryChangedCommand { get; set; }
        public MainPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;
            EntryChangedCommand = new DelegateCommand(() =>
            {
                EntryBehaviorInputTextLength = EntryBehaviorInputText is null ? 0 : EntryBehaviorInputText.Length;
            });
        }

使用附加屬性來計算使用輸入的文字數量

對於想要針對某個控制項加入可以綁定的屬性,讓 ViewModel 可以根據 View 的屬性異動,進行相關的商業邏輯處理,第一個就是使用原來檢視上的屬性來做處理,若有特殊需求,需要額外的屬性,可以建立一個新類別,繼承原來使用的檢視類別,就可以在這個新控制項中加入一個新的可綁定屬性;第二個方式,就是使用附加屬性,針對想要使用該附加屬性來附加這些附加屬性。
最後一個,就是要建立兩個附加屬性,第一個是 EnableCharCount ,這是用來設定是否要啟用自動計算字數的附加屬性,第二個是 EditorInputTextLength 用來儲存使用者輸入字數的數量值,底下為設計出來的附加屬性 XAML 用法。
由於等下要設計的附加屬性將會限制這兩個附加屬性僅能夠套用到 Entry 或者 Editor 上,當這兩個附加屬性附加到其他檢視的時候,將不會有任何效果的。
這裡有一點要特別注意,對於 CharCountAttachedProperty.CharNumber 這個附加屬性需要在資料綁定延伸標記語法內,要使用 Mode=TwoWay ,這樣才會形成雙向綁定運作方式。
xaml
        <Label Text="{Binding EditorInputTextLength}"/>
        <Label Text="{Binding EntryInputTextLength}"/>
        <Editor
            Text="{Binding EditorInputText}"
            AutoSize="TextChanges"
            local:CharCountAttachedProperty.EnableCharCount="True"
            local:CharCountAttachedProperty.CharNumber="{Binding EditorInputTextLength, Mode=TwoWay}"
            />
        <Entry
            Text="{Binding EntryInputText}"
            local:CharCountAttachedProperty.EnableCharCount="True"
            local:CharCountAttachedProperty.CharNumber="{Binding EntryInputTextLength, Mode=TwoWay}"/>
現在,可以建立一個類別,在此類別上建立兩個附加屬性,在此,建立的類別名稱為 CharCountAttachedProperty,接著,在此類別內,使用程式碼片段 xfAttachedProperty 自動產生附加屬性的相關程式碼。
第一個附加屬性為 EnableCharCountProperty ,在這裡附加屬性將會用來控制是否要進行輸入文字內容的計算工作,在呼叫 BindableProperty.CreateAttached 方法的時候,將會使用 declaringType 來指定這個附加屬性可以附加到那些 XAML 項目上,這裡指定的是 typeof(View),代表任何使用者控制項都可以使用這個附加屬性。
在 EnableCharCountProperty 內最為重要的工作那就是要定義 propertyChanged 這個參數值,傳入一個委派方法 OnEnableCharCountChanged,在此委派方法終將會使用 if (bindable is Entry || bindable is Editor) 來檢查現在該附加屬性是否用在 Entry 或者 Editor 這兩個檢視上,若不是的話,則不會做任何處理。所附加的檢視為 Entry 或者 Editor ,將會檢查是否要啟用這個自動計算字數的屬性值,若為 True,則會訂閱 Entry.TextChanged 事件或者 Editor.TextChanged 事件,否則若為 False,則會取消 TextChanged 的事件訂閱。
記得,當訂閱視覺項目的事件的時候,一定要規劃解除訂閱事件的程式碼,否則,會有可能產生 記憶體遺失 Memory Leak 的問題。
在 OnEntryTextChanged 方法內,將會進行該自動計算使用者輸入文字的字數計算,將結果儲存到 CharNumberProperty 這個附加屬性上。
最後,還是使用相同的程式碼片段, xfAttachedProperty 自動建立 CharNumberProperty 附加屬性程式碼,而在這個 CharNumberProperty 附加屬性上,並不需要加入額外的其他城市碼。
public class CharCountAttachedProperty
{
    #region EnableCharCount 附加屬性 Attached Property
    public static readonly BindableProperty EnableCharCountProperty =
           BindableProperty.CreateAttached(
               propertyName: "EnableCharCount",   // 屬性名稱 
               returnType: typeof(bool), // 回傳類型 
               declaringType: typeof(View), // 宣告類型 
               defaultValue: false, // 預設值 
               propertyChanged: OnEnableCharCountChanged  // 屬性值異動時,要執行的事件委派方法
           );

    public static void SetEnableCharCount(BindableObject bindable, bool entryType)
    {
        bindable.SetValue(EnableCharCountProperty, entryType);
    }
    public static bool GetEnableCharCount(BindableObject bindable)
    {
        return (bool)bindable.GetValue(EnableCharCountProperty);
    }

    private static void OnEnableCharCountChanged(BindableObject bindable, object oldValue, object newValue)
    {
        if (bindable is Entry || bindable is Editor)
        {
            bool isEnable = (bool)newValue;
            if (isEnable)
            {
                if (bindable is Entry)
                {
                    (bindable as Entry).TextChanged += OnEntryTextChanged;
                }
                else if (bindable is Editor)
                {
                    (bindable as Editor).TextChanged += OnEntryTextChanged;
                }
            }
            else
            {
                if (bindable is Entry)
                {
                    (bindable as Entry).TextChanged -= OnEntryTextChanged;
                }
                else if (bindable is Editor)
                {
                    (bindable as Editor).TextChanged -= OnEntryTextChanged;
                }
            }
        }
        else
        {

        }
    }
    #endregion
    private static void OnEntryTextChanged(object sender, TextChangedEventArgs e)
    {
        int foo = CharCountAttachedProperty.GetCharNumber(sender as BindableObject);
        int length = e.NewTextValue.Length;
        CharCountAttachedProperty.SetCharNumber(sender as BindableObject, length);
        (sender as BindableObject).SetValue(CharNumberProperty, length);
        int foo2 = CharCountAttachedProperty.GetCharNumber(sender as BindableObject);
    }

    #region CharNumber 附加屬性 Attached Property
    public static readonly BindableProperty CharNumberProperty =
           BindableProperty.CreateAttached(
               propertyName: "CharNumber",   // 屬性名稱 
               returnType: typeof(int), // 回傳類型 
               declaringType: typeof(View), // 宣告類型 
               defaultValue: 0, // 預設值 
               propertyChanged: OnCharNumberChanged  // 屬性值異動時,要執行的事件委派方法
           );

    public static void SetCharNumber(BindableObject bindable, int entryType)
    {
        bindable.SetValue(CharNumberProperty, entryType);
    }
    public static int GetCharNumber(BindableObject bindable)
    {
        return (int)bindable.GetValue(CharNumberProperty);
    }

    private static void OnCharNumberChanged(BindableObject bindable, object oldValue, object newValue)
    {
    }

    #endregion

}



2019/04/08

Xamarin.Forms 的檢視 View 與版面配置 Layout 的手勢 Gesture 操作

Xamarin.Forms 的檢視 View 與版面配置 Layout 的手勢 Gesture 操作

就在上週, Visual Studio 2019 正式發表了,隨之而來的驚奇工作那就是要來體驗 Visual Studio 2019 這套新的開發工具所帶來的絕佳操作感覺,也就是說,在 2019年四月之後,我這裡所寫關於 Xamarin.Forms 的範例程式碼,將會全部改成使用 Xamarin.Forms 來設計。在這篇文章中,將會來測試 Xamarin.Forms 的手勢操作的設計上的問題,那就是可以在使用者控制項與版面配置上來使用 GestureRecognizers 屬性來指定其需要執行相對應手勢操作需要進行的綁定命令。可是,當在一個 Layout 上有指定 GestureRecognizers ,並且放置一個使用者控制項到這個版面配置上,此時,該使用者控制項若有指定 GestureRecognizers ,那麼,是版面配置的 GestureRecognizers 會被觸發,還是使用者控制項的 GestureRecognizers 會被觸發呢?
因此,在我心中存在者許多疑問,便想要寫可個範例程式碼來逐一檢測底下的問題?
  1. 當在一個 Layout 上有指定 GestureRecognizers ,並且放置一個使用者控制項到這個版面配置上(該使用者控制項並未完全佔用滿整個版面配置空間),此時,該使用者控制項若有指定 GestureRecognizers ,那麼,當使用者點選該使用者控制項的時候,是哪個 GestureRecognizers 會被觸發呢?
  2. 當在一個 Layout 上有指定 GestureRecognizers ,並且放置一個使用者控制項到這個版面配置上(該使用者控制項並未完全佔用滿整個版面配置空間),此時,該使用者控制項若沒有指定 GestureRecognizers ,那麼,當使用者點選該使用者控制項的時候,是哪個 GestureRecognizers 會被觸發呢?
  3. 當在一個 Layout 上有指定 GestureRecognizers ,並且放置一個按鈕到這個版面配置上(該使用者控制項並未完全佔用滿整個版面配置空間),此時,該按鈕若有指定 Command ,那麼,當使用者點選該按鈕的時候,是否會觸發 Layout 的GestureRecognizers呢?
  4. 當在一個 Layout 上有指定 GestureRecognizers ,並且放置一個按鈕到這個版面配置上(該使用者控制項並未完全佔用滿整個版面配置空間),此時,該按鈕若沒有指定 Command ,那麼,當使用者點選該按鈕的時候,是否會觸發 Layout 的GestureRecognizers呢?
  5. 當在一個 Layout 上有指定 GestureRecognizers ,並且放置一個文字輸入盒到這個版面配置上(該使用者控制項並未完全佔用滿整個版面配置空間),此時,若點選該文字輸入盒,是否會觸發 Layout 的GestureRecognizers呢?
為了要能夠了解這些疑問,所以,請依照底下步驟建立一個 Xamarin.Forms 測試專案,並且分別在 Android / iOS / UWP 底下來跑看看,看看這些問題的實際運作情況是如何,並且在不同作業系統平台下的表顯示否都是相同呢?
這篇文章所提到的範例程式碼,可以從 Github Xamarin2019 Repository 上取得。

使用 Visual Studio 產生一個 Xamarin.Forms for Prism 專案

  • 開啟 Visual Studio 2019 程式
  • 當 Visual Studio 2019 開始 視窗 出現之後,請點選左下角的 [建立新專案] 選項 
  • 當 [建立新專案] 對話窗出現之後,請在中間最上方的搜尋文字輸入盒中輸入 [prism] 關鍵字,搜尋所有與 Prism 有關的專案樣板 
  • 請選擇 [Prism Blank App (Xamarin.Forms)] 這個專案樣板
  • 當出現 [設定新的專案] 對話窗,請在 [專案名稱] 輸入 [LayoutGesture] 
  • 最後點選該對話窗右下方的 [建立] 按鈕
  • 現在將會看到 [PRISM PROJECT WIZARD] 對話窗,請勾選 ANDROID, iOS, UWP 三個行動裝置平台,接著在底下 [Container] 下拉選單,選擇 Unity 項目 
  • 最後,點選 [CREATE PROJECT] 按鈕,以便產生 Xamarin.Forms 專案
  • 當這個 Xamarin.Forms 專案建立成功之後,請在該方案中,找到 Xamarin.Forms 使用的專案,這裡是名為 [LayoutGesture] 專案名稱,請在該專案中,使用滑鼠右擊 [相依性] 節點,選擇 [管理 NuGet 套件] 選項
  • 在 [NuGet: LayoutGesture] 視窗中,點選 [瀏覽] 標籤頁次,並且在下方的搜尋文字輸入盒中,輸入 [propertychanged.fody] 關鍵字,搜尋出這個 NuGet 套件
  • 當出現 [PropertyChanged.Fody] NuGet 套件,請點選該套件,並且點選右方的 [安裝] 按鈕,將這個套件安裝到 Xamarin.Forms 專案內 
  • 請查看 Xamarin.Forms 專案內,並沒有 [FodyWeavers.xml] 這個檔案,因此,使用滑鼠右擊 [LayoutGesture] 專案節點,選擇 [建置] 選項
  • 當建置完成之後,在這個 Xamarin.Forms 專案內將會出現 [FodyWeavers.xml] 檔案 

修正 View 與 ViewModel

  • 在 Xamarin.Forms 專案內的 [Views] 資料夾內,找到 MainPage.xaml 檔案,並且打開它
  • 使用底下 XAML 語言替換掉這個檔案內的 XAML 內容
XAML
<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="LayoutGesture.Views.MainPage"
             Title="版面配置的手勢操作">

    <Grid
       RowSpacing="0" ColumnSpacing="0"
       >
        <Grid.RowDefinitions>
            <RowDefinition Height="50"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
            <RowDefinition Height="*"/>
        </Grid.RowDefinitions>
        <Grid.GestureRecognizers>
            <TapGestureRecognizer
                    Command="{Binding TapCommand}"
                    CommandParameter="透明的 Grid 觸發點選手勢"/>
        </Grid.GestureRecognizers>

        <Label
            Grid.Row="0" Grid.Column="0"
            HorizontalOptions="Center" VerticalOptions="Center"
            Text="{Binding Hint}"/>

        <StackLayout
            Grid.Row="1"
            >
            <StackLayout.GestureRecognizers>
                <TapGestureRecognizer
                    Command="{Binding TapCommand}"
                    CommandParameter="透明的 StackLayout 觸發點選手勢"/>
            </StackLayout.GestureRecognizers>
        </StackLayout>

        <StackLayout
            Grid.Row="2"
            BackgroundColor="LightBlue"
            >
            <StackLayout.GestureRecognizers>
                <TapGestureRecognizer
                    Command="{Binding TapCommand}"
                    CommandParameter="藍色背景的 StackLayout 觸發點選手勢"/>
            </StackLayout.GestureRecognizers>
        </StackLayout>

        <BoxView
            Grid.Row="3"
            Color="LightGreen"
            HorizontalOptions="Start" VerticalOptions="Center"
            WidthRequest="100" HeightRequest="50"/>

        <BoxView
            Grid.Row="3"
            Color="LightPink"
            HorizontalOptions="End" VerticalOptions="Center"
            WidthRequest="100" HeightRequest="50">
            <BoxView.GestureRecognizers>
                <TapGestureRecognizer
                    Command="{Binding TapCommand}"
                    CommandParameter="粉紅色背景的 BoxView 觸發點選手勢"/>
            </BoxView.GestureRecognizers>
        </BoxView>

        <Button
            Grid.Row="4"
            HorizontalOptions="Start" VerticalOptions="Center"
            Text="沒有設定 Command 的按鈕"/>

        <Button
            Grid.Row="4"
            HorizontalOptions="End" VerticalOptions="Center"
            Text="設定 Command 的按鈕"
            Command="{Binding TapCommand}"
            CommandParameter="設定 Command 的按鈕"/>

        <Entry
            Grid.Row="5"
            Placeholder="請在這裡輸入"
            HorizontalOptions="Center" VerticalOptions="Center"/>
    </Grid>

</ContentPage>
  • 在 Xamarin.Forms 專案內的 [ViewModels] 資料夾內,找到 MainPageViewModel.cs 檔案,並且打開它
  • 使用底下 C# 敘述替換掉這個檔案內的 C# 敘述
C Sharp / C#
namespace LayoutGesture.ViewModels
{
    using System.ComponentModel;
    using Prism.Events;
    using Prism.Navigation;
    using Prism.Services;
    public class MainPageViewModel : INotifyPropertyChanged, INavigationAware
    {
        public event PropertyChangedEventHandler PropertyChanged;
        public DelegateCommand<string> TapCommand { get; set; }
        private readonly INavigationService navigationService;
        public string Hint { get; set; }
        public MainPageViewModel(INavigationService navigationService)
        {
            this.navigationService = navigationService;
            TapCommand = new DelegateCommand<string>(x =>
            {
                Hint = x;
            });
        }

        public void OnNavigatedFrom(INavigationParameters parameters)
        {
        }

        public void OnNavigatedTo(INavigationParameters parameters)
        {
        }

        public void OnNavigatingTo(INavigationParameters parameters)
        {
        }

    }
}

建置與執行和測試結果

  • 切換 Android 專案為預設起始專案,並且執行這個 Xamarin.Forms 到模擬器或者實體手機上
  • 底下螢幕截圖為實際執行結果
現在,可以實際點選螢幕上的任何地方,並且觀察螢幕最上方的訊息文字,所以,可以得到底下結論:
  • Layout 版面配置 與 View 檢視 可以設定使用 GestureRecognizers 屬性,指定不同的手勢觸發命令
  • 若有多個 Layout 版面配置重疊,且這些版面配置都有設定使用 GestureRecognizers 屬性,當點選多個重疊在最上方版面配置區域的時候,最上方的版面配置之手勢命令將會被觸發,在底層的手勢命令不會被觸發
  • 若版面配置的背景顏色為 透明色 Transparent ,還是依照上述的運作規則
  • 若版面配置與檢視彼此重疊,也是依照上述的運作規則
  • 若該檢視本身就具有手勢感應功能,例如,按鈕可以接收到使用者是否有點選這個按鈕、文字輸入盒可以接受使用者輸入文字等等,就算這些檢視沒有使用 GestureRecognizers 屬性,在底層的版面配置或者檢視也不會觸發相關手勢命令