XAML in Xamarin.Forms 基礎篇 電子書

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

Xamarin.Forms 快速入門 電子書

Xamarin.Forms 快速入門 電子書
Xamarin.Forms 快速入門 電子書

2017/04/29

Xamarin.Forms 不同的View處理邏輯:Code Behind/INPC/Prism/Fody

對於第一次接觸 Xamarin.Forms 的開發者,或者從未使用過 XAML 來開發過的開發者,第一此要下的決策,那就是,對於頁面的處理邏輯之程式碼編寫,要使用哪種方法呢?
在網路上所看到的文章,或者 Xamarin 官方上看到的 Sample,大多使用 Code Behind + INPC 的方式來演練,不過,我自己是比較偏好使用 MVVM + Data Binding 的方式來進行設計每個頁面的處理邏輯。
在這篇文章中,將會說明不同的技術,如何用來寫出相同頁面的處理邏輯。
這個頁面中,將會有
  • 一個 Entry 控制項,可以讓使用輸入文字
  • 一個 Label 控制項,顯示出使用者輸入的文字
  • 一個按鈕,當使用者按下這個按紐之後,會將使用者輸入的文字,設定到 Label 控制項並顯示出來。
若要參考這篇文章的原始碼,可以參考這裡

使用 Code Behind 方式來開發

在這個方式,將僅需要使用 Page.xaml & Page.xaml.cs 這兩個檔案,進行開發。
您需要在頁面的 XAML 宣告中,對於需要處理的各個控制項、版面配置等等,使用 x:Name 延伸標記,標示出這個視覺項目(Visual Element)可以在 Code Behind 存取的物件名稱。
如此,您就可以在這個頁面中的 Code Behind (在這裡是 CodeBehindPage.xaml.cs) 內,使用這些 x:Name所標示的物件名稱,存取這些視覺項目。
當然,對於按鈕這類控制項,您需要使用該控制項的事件,設定當使用者按下這個按鈕之後,相對應的 Clicked 事件要做的事情。

CodeBehindPage.xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="XFDataBinding.Views.CodeBehindPage"
             Title="使用 Code Behind">

    <StackLayout
        Margin="20,0"
        >
        <Entry x:Name="MyEntry"
            HorizontalOptions="FillAndExpand"
            Text=""/>

        <Label x:Name="MyLabel"
            HorizontalOptions="FillAndExpand"
            Text=""/>

        <Button x:Name="btn登入"
            Text="登入"/>
    </StackLayout>

</ContentPage>

CodeBehindPage.xaml.cs

    public partial class CodeBehindPage : ContentPage
    {
        public CodeBehindPage()
        {
            InitializeComponent();

            btn登入.Clicked += (s, e) =>
            {
                MyLabel.Text = MyEntry.Text;
            };
        }
    }

使用 自己實作 INPC 方式來開發

INPC = INotifyProPertyChanged
也就是底下的三個方法,都是使用 MVVM 的架構下來進行開發,因此,在您的專案中,就會有 View (宣告 XAML 標記的地方,也就是每個葉面要顯示的視覺項目)、ViewModel(用來撰寫這個頁面的所有處理邏輯程式碼地方)。
在 ViewModel 內的 .NET 屬性 (Property),將會透過 INPC 介面實作出來的事件,通知 View 綁定的控制項,更新最新的資料內容;當然,當 View 中的控制項屬性內容有異動的時候,當然,也會即時性的更新到 ViewModel 所綁定的 .NET 屬性物件。
在頁面中的 XAML 中,首先須要宣告一個命名空間,這裡是 ViewModel,指向這個頁面所在的 C# 命名空間位置。
接著,您需要透過 XAML 或者 Code Behind 程式碼,定義這個頁面 BindingContext 屬性值,也就是要指向到這個頁面的 ViewModel 物件上。
若您使用 XAML 的方式,需要使用底下方法,這裡將會產生一個 INPCPageViewModel 物件,並且設定給 ContentPage.BindingContext。
<ContentPage.BindingContext>
    <ViewModel:INPCPageViewModel/>
</ContentPage.BindingContext>
若您使用 Code Behind 的方式,您需要在這個頁面的建構式內,使用底下程式碼,來設定這個頁面的 BindingContext 屬性值。
這兩種方式,您可以任選其一來實作。
    public partial class INPCPage : ContentPage
    {
        public INPCPage()
        {
            InitializeComponent();

            //由於不是使用 Prism 框架,所以,BindingContext 要用到的 ViewModel,必須要自己來設定,當然,也可以在 Code Behind 端來設定
            BindingContext = new INPCPageViewModel();
        }
    }
對於按鈕控制項,我們將不再使用按鈕的事件來撰寫處理邏輯,而是使用這些控制項所提供的 Command 屬性。當我們將 .NET 的 ICommand 物件綁訂到這些 Command 屬性上之後,一旦使用者按下按鈕之後,就會執行這些命令。
而我們,僅需要在這些命令中,處理 ViewModel 內宣告的各個 .NET 屬性物件;只要您變更了這些 .NET 屬性物件值,就會透過了 INPC 機制,送出變動事件到 View 中,而所綁定這個 .NET 屬性的控制項之屬性值,也就會更著做出更新。

INPCPage.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"
             xmlns:ViewModel="clr-namespace:XFDataBinding.ViewModels"
             x:Class="XFDataBinding.Views.INPCPage"
             Title="自己實作 INPC">

    <!--
    由於不是使用 Prism 框架,所以,BindingContext 要用到的 ViewModel,
    必須要自己來設定,當然,也可以在 Code Behind 端來設定
    -->
    <ContentPage.BindingContext>
        <ViewModel:INPCPageViewModel/>
    </ContentPage.BindingContext>

    <StackLayout
        Margin="20,0"
        >
        <Entry 
            HorizontalOptions="FillAndExpand"
            Text="{Binding MyEntry}"/>

        <Label
            HorizontalOptions="FillAndExpand"
            Text="{Binding MyLabel}"/>

        <Button Text="登入" Command="{Binding 登入Command}"/>
    </StackLayout>


</ContentPage>
在 ViewModel 類別中,您需要使用與實作這個 INotifyPropertyChanged 介面。
接著,您需要在每個 ViewModel 類別內,撰寫一個 OnPropertyChanged 方法,這個方法會用於,當您綁定於 View 中的 .NET 屬性有異動的時候,需要呼叫這個方法,發出一個事件,通知 View 中的視覺項目之屬性要更新其內容。
另外,您需要使用 Visual Studio 所提供的程式碼片段 propfull,產生一個完整的 .NET 屬性定義,這裡將會包含了屬性與儲存這個屬性的欄位物件。
接著,修改這個屬性的 set 存取子 (Accessor),判斷當該屬性值有異動的時候,需要呼叫 OnPropertyChanged 方法。
關於 C# 的屬性,可以參考 使用屬性 (C# 程式設計手冊)

INPCPageViewModel.cs

    public class INPCPageViewModel : INotifyPropertyChanged
    {
        #region Repositories (遠端或本地資料存取)

        #endregion

        #region ViewModel Property (用於在 View 中作為綁定之用)

        #region 基本型別與類別的 Property
        private string _MyEntry;

        public string MyEntry
        {
            get { return _MyEntry; }
            set
            {
                if (_MyEntry != value)
                {
                    _MyEntry = value;
                    OnPropertyChanged("MyEntry");
                }
            }
        }

        private string _MyLabel;

        public string MyLabel
        {
            get { return _MyLabel; }
            set {
                if (_MyLabel != value)
                {
                    _MyLabel = value;
                    OnPropertyChanged("MyLabel");
                }
            }
        }

        public ICommand 登入Command { get; set; }
        #endregion

        #region 集合類別的 Property

        #endregion

        #endregion

        #region Field 欄位

        #region ViewModel 內使用到的欄位
        public event PropertyChangedEventHandler PropertyChanged;
        #endregion

        #region 命令物件欄位
        #endregion

        #region 注入物件欄位
        #endregion

        #endregion

        #region Constructor 建構式
        public INPCPageViewModel()
        {

            #region 頁面中綁定的命令
            登入Command = new Command(() =>
            {
                MyLabel = MyEntry;
            });
            #endregion
        }

        #endregion

        #region 設計時期或者執行時期的ViewModel初始化
        #endregion

        #region 相關事件
        #endregion

        #region 相關的Command定義
        #endregion

        #region 其他方法

        protected virtual void OnPropertyChanged(string propertyName)
        {
            if (PropertyChanged != null)
            {
                // 若 PropertyChanged 有被綁定,則將會執行這個事件,
                // 以進行頁面控制項的內容更新
                PropertyChanged(this,
                    new PropertyChangedEventArgs(propertyName));
            }
        }
        #endregion
    }

使用 Prism 的 BindableBase 類別 方式來開發

當您使用 Prism 作為您 MVVM 開發技術的框架,就可以大幅降低使用 INPC 方法的開發複雜度,最起碼不用每個頁面都要自己撰寫一個 OnPropertyChanged 方法與實作 INPC 介面。
Prism 使用了 BindableBase 類別,只要您的 ViewModel 繼承了這個類別,Prism 自動幫忙處理相關 INPC 需要實作的相關內容。
這個時候,當您要宣告一個用於綁定的 .NET Property,僅需要在set 存取子呼叫 BindableBase 類別的 SetProperty 方法即可。
不過,每個 .NET Property 都需要使用一個欄位來儲存這個 .NET 屬性的值,並且,需要修改 set 存取子的方法程式碼。
        private string _MyEntry;
        /// <summary>
        /// MyEntry
        /// </summary>
        public string MyEntry
        {
            get { return this._MyEntry; }
            set { this.SetProperty(ref this._MyEntry, value); }
        }
另外,由於 Prism 自動幫我們設定了這個頁面所需要用到的 ViewModel,並設定到這個頁面的 BindingContext 屬性內。
這歸咎於頁面 XAML 中,有了這個宣告
prism:ViewModelLocator.AutowireViewModel="True"
上面的宣告,使用了 Prism 提供的一個 ViewModelLocator 機制,自動依據現在 View 的名稱,找到相對應的 ViewModel 類別,並且使用這個類別產生出一個物件,最後,設定到這個頁面的 BindingContext 上。

PrismPage.xaml

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFDataBinding.Views.PrismPage"
             Title="使用 Prism 的 BindableBase 類別">

    <StackLayout
        Margin="20,0"
        >
        <Entry 
            HorizontalOptions="FillAndExpand"
            Text="{Binding MyEntry}"/>

        <Label
            HorizontalOptions="FillAndExpand"
            Text="{Binding MyLabel}"/>

        <Button Text="登入" Command="{Binding 登入Command}"/>
    </StackLayout>

</ContentPage>

PrismPageViewModel.cs

    public class PrismPageViewModel : BindableBase, INavigationAware
    {
        #region Repositories (遠端或本地資料存取)

        #endregion

        #region ViewModel Property (用於在 View 中作為綁定之用)

        #region 基本型別與類別的 Property

        #region MyEntry
        private string _MyEntry;
        /// <summary>
        /// MyEntry
        /// </summary>
        public string MyEntry
        {
            get { return this._MyEntry; }
            set { this.SetProperty(ref this._MyEntry, value); }
        }
        #endregion

        #region MyLabel
        private string _MyLabel;
        /// <summary>
        /// MyLabel
        /// </summary>
        public string MyLabel
        {
            get { return this._MyLabel; }
            set { this.SetProperty(ref this._MyLabel, value); }
        }
        #endregion

        #endregion

        #region 集合類別的 Property

        #endregion

        #endregion

        #region Field 欄位

        #region ViewModel 內使用到的欄位
        #endregion

        #region 命令物件欄位

        public DelegateCommand 登入Command { get; set; }

        #endregion

        #region 注入物件欄位
        private readonly INavigationService _navigationService;
        #endregion

        #endregion

        #region Constructor 建構式
        public PrismPageViewModel(INavigationService navigationService)
        {

            #region 相依性服務注入的物件

            _navigationService = navigationService;
            #endregion

            #region 頁面中綁定的命令
            登入Command = new DelegateCommand(() =>
            {
                MyLabel = MyEntry;
            });
            #endregion

            #region 事件聚合器訂閱

            #endregion
        }

        #endregion

        #region Navigation Events (頁面導航事件)
        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public async void OnNavigatedTo(NavigationParameters parameters)
        {
            await ViewModelInit();
        }
        #endregion

        #region 設計時期或者執行時期的ViewModel初始化
        #endregion

        #region 相關事件
        #endregion

        #region 相關的Command定義
        #endregion

        #region 其他方法

        /// <summary>
        /// ViewModel 資料初始化
        /// </summary>
        /// <returns></returns>
        private async Task ViewModelInit()
        {
            await Task.Delay(100);
        }
        #endregion

    }

使用 Fody 方式來開發

對於使用 Prism 的框架下,已經大幅簡化了資料綁定的程式碼做法,不過,還有一個方法,可以讓您的 ViewModel 程式碼更加的清爽。
您需要在核心 PCL 專案內,安裝這個 PropertyChanged.Fody NuGet 套件。
之後,您僅需要在您的 ViewMdoel 類別外,使用 C# 屬性 ImplementPropertyChanged,如此,當您需要宣告要綁定於 XAML 中的 .NET 屬性的時候,就可以使用底下的程式碼寫法(可以使用 Visual Studio 提供的 prop 程式碼片段來快速產生這些程式碼,這樣,是不是更加清爽與簡單了呢?
        public string MyEntry { get; set; }
        public string MyLabel { get; set; }

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="XFDataBinding.Views.FodyPage"
             Title="使用 Fody">

    <StackLayout
        Margin="20,0"
        >
        <Entry 
            HorizontalOptions="FillAndExpand"
            Text="{Binding MyEntry}"/>

        <Label
            HorizontalOptions="FillAndExpand"
            Text="{Binding MyLabel}"/>

        <Button Text="登入" Command="{Binding 登入Command}"/>
    </StackLayout>

</ContentPage>

#

    [ImplementPropertyChanged]
    public class FodyPageViewModel : INavigationAware
    {
        #region Repositories (遠端或本地資料存取)

        #endregion

        #region ViewModel Property (用於在 View 中作為綁定之用)

        #region 基本型別與類別的 Property
        public string MyEntry { get; set; }
        public string MyLabel { get; set; }
        #endregion

        #region 集合類別的 Property

        #endregion

        #endregion

        #region Field 欄位

        #region ViewModel 內使用到的欄位
        #endregion

        #region 命令物件欄位

        public DelegateCommand 登入Command { get; set; }

        #endregion

        #region 注入物件欄位
        private readonly INavigationService _navigationService;
        #endregion

        #endregion

        #region Constructor 建構式
        public FodyPageViewModel(INavigationService navigationService)
        {

            #region 相依性服務注入的物件

            _navigationService = navigationService;
            #endregion

            #region 頁面中綁定的命令
            登入Command = new DelegateCommand(() =>
            {
                MyLabel = MyEntry;
            });
            #endregion

            #region 事件聚合器訂閱

            #endregion
        }

        #endregion

        #region Navigation Events (頁面導航事件)
        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatingTo(NavigationParameters parameters)
        {

        }

        public async void OnNavigatedTo(NavigationParameters parameters)
        {
            await ViewModelInit();
        }
        #endregion

        #region 設計時期或者執行時期的ViewModel初始化
        #endregion

        #region 相關事件
        #endregion

        #region 相關的Command定義
        #endregion

        #region 其他方法

        /// <summary>
        /// ViewModel 資料初始化
        /// </summary>
        /// <returns></returns>
        private async Task ViewModelInit()
        {
            await Task.Delay(100);
        }
        #endregion

    }