Prism MVVM:屬性改變通知 - INotifyPropertyChanged

連續一個多月都在學習由《前端》到《後端》有整體關聯的各項實作,在切換到另一項主題:《使用者驗證與授權》之前,或許切出幾個獨立小專案的基本功系列,也算是複習先前所學的。

問題:為什麼在 ViewModel 裡的屬性宣告是這樣:

private string _title;
public string Title
{
    get { return _title; }
    set { SetProperty(ref _title, value); }
}

而不是像 Entity Model 的宣告:

public string Title {get; set;}

呢?

建立 Prism Xamarin.Forms 專案

阿源哥哥不直接說出答案,請讀者跟著文章說明動手實作一次,到最後聰明的讀者應該可以自行找到答案(找不到答案也沒關係,文章最後會公佈答案),當然已經知道答案的讀者也鼓勵你跟著實作一次,因為本篇文章主要也要用來說明 MVVM 的一個要點。

好吧!開始動手了,請新增一個 Prism Xamarin.Forms 專案:

視覺元件與資料繫結

接著請在原有的 MainPage.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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PropertyChanged.Views.MainPage"
             Title="MainPage">
    <StackLayout HorizontalOptions="Center" VerticalOptions="Center">
        <Label Text="{Binding Title}" />
        <Entry Text="{Binding MyEntry}" Placeholder="請輸入通關密語"/>
        <Button Text="{Binding ButtonText}" Command="{Binding GotoNextPageCommand}"/>
    </StackLayout>
</ContentPage>

上述程式碼加入了:

  • 一個 Entry 元件,其內文(Text)繫結對應 ViewModel 的 MyEntry 屬性。
  • 一個 Button 元件,其按鈕文字(Text)繫結對應 ViewModel 的 ButtonText 屬性,而命令(Command)繫結 GotoNextPageCommand 命令。

接下來就是在對應 ViewModel 中實作被繫結的兩個屬性和命令,請回想一下命名規則 MainPage 的對應 ViewModel 是 MainPageViewModel 所以實作的程式碼要加在 MainPageViewModel.cs 中。

程式碼片段

在實作屬性和命令之前,先介紹 Prism Template Pack 所提供的兩個程式碼片段:

  • propp
  • cmd

善用程式碼片段(往後的文章也會介紹如何做自己的程式碼片段),可以結省很多打字的時間,至於程式碼片段如何使用,請看下列圖示說明:

propp 的用法:

註:
打完 propp 後,連按兩次【tab】鍵,打完對應型別或名稱後,按一下【tab】鍵。順道一提,請留意一下 Field Name 與 Property Name 的命名原則,雖然該命名原則不一定要遵守,但是同一個團隊有相同的命名原則,將來程式交接時,一看名稱立刻就能了解含意,可省下許多解說的時間。

cmd 的用法:

加入 ViewModel 的屬性後,程式碼如下所示:

namespace PropertyChanged.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        private INavigationService _navigationService;

        private string _title;
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }
        private string _myEntry;
        public string MyEntry
        {
            get { return _myEntry; }
            set { SetProperty(ref _myEntry, value); }
        }
        private string _buttonText;
        public string ButtonText
        {
            get { return _buttonText; }
            set { SetProperty(ref _buttonText, value); }
        }

        ......
        ......
        ......
      }
}

實作命令

加入 ViewModel 內的命令後,程式碼如下所示:

namespace PropertyChanged.ViewModels
{
    public class MainPageViewModel : BindableBase, INavigationAware
    {
        .....
        .....
        .....

        private DelegateCommand _gotoNextPageCommand;
        public DelegateCommand GotoNextPageCommand =>
            _gotoNextPageCommand ?? (_gotoNextPageCommand = new DelegateCommand(GotoNextPage, CanGotoNextPage).ObservesProperty(() => MyEntry));

        private bool CanGotoNextPage()
        {
            if (MyEntry == "Keigen is a good man.")
            {
                ButtonText = "答對了,過關!";
                return true;
            }
            return false;
        }
        private void GotoNextPage()
        {
            var p = new NavigationParameters
            {
                { "password", MyEntry }
            };
            _navigationService.NavigateAsync("NextPage", p);
        }


        ......
        ......
      }
}

上述程式碼比較值得留意的是:new DelegateCommand(GotoNextPage, CanGotoNextPage) 其中的 GotoNextPage 指出當命令被執行時,實際上是去執行哪一個方法,以目前的案例,該方法是將頁面導到名稱為 NextPage 的頁面,並將 MyEntry 的內容,以名稱為 password 的參數帶到下一頁。

而 CanGotoNextPage 指出哪一個方法負責檢查該命令在何種情況下可被執行(按鈕可被按下),當回傳 true 時,可被執行,回傳 false 時,不可被執行。

另一個值得留意的是 .ObservesProperty(() => MyEntry)  指出 MyEntry 這個屬性值要被觀查,一有變化就要被處理。目前的處理方式是檢查該 MyEntry 的值是否為 Keigen is a good man. 如果是,改變按鈕文字,並回傳 true (也就是命令可被執行了)。

實作承接參數的頁面

接著在新增一個 Prism ContentPage 如下圖所示:

接著在新增的頁面加入視覺元件,並將 ContentPage 的 Title 繫結 ViewModel 的 Title 屬性,程式碼如下所示:

<?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:prism="clr-namespace:Prism.Mvvm;assembly=Prism.Forms"
             prism:ViewModelLocator.AutowireViewModel="True"
             x:Class="PropertyChanged.Views.NextPage"
             Title="{Binding Title}">
    <StackLayout 
        HorizontalOptions="Center"
        VerticalOptions="Center">
        <Label Text="天啊!你怎麼知道這個密秘。" />
    </StackLayout>  
</ContentPage>

接著在 ViewModel 加入屬性,以及在頁面導入的同時,接收參數,並指定給 Title 屬性。

using Prism.Mvvm;
using Prism.Navigation;

namespace PropertyChanged.ViewModels
{
    public class NextPageViewModel : BindableBase, INavigatedAware
    {
        private string _title;
        public string Title
        {
            get { return _title; }
            set { SetProperty(ref _title, value); }
        }
        public NextPageViewModel()
        {

        }

        public void OnNavigatedFrom(NavigationParameters parameters)
        {

        }

        public void OnNavigatedTo(NavigationParameters parameters)
        {
            if (parameters.ContainsKey("password"))
            {
                Title = (string)parameters["password"];
            }
        }
    }
}

執行程式,如果沒意外的話,程式的執行結果應該如下圖所示:

公佈答案

聰明的讀者應該想到了吧,因為 Prism MVVM 的 BindableBase 實作了 INotifyPropertyChanged 介面,所以當 MyEntry 內容一有變動(使用者輸入字串)就會隨時觸發 SetProperty() 方法,通知相關聯的元件(屬性或方法),應該要做出適當的對應,如果只是 public string MyEntry {get; set;}  就不會有通知功能了。

public class MainPageViewModel : BindableBase
{
      
    private string _myEntry;
    public string MyEntry
    {
        get { return _myEntry; }
        set { SetProperty(ref _myEntry, value); }
    }
}
註:
還是有一個稱為 PropertyChanged.Fody 的套件,用一般的屬性宣告方式即可達到通知功能,但是阿源哥哥不喜歡,因為善用程式碼片段,也不會多打幾個字,也不會太麻煩。

學到了什麼

雖然這個範例有點搞笑,但是這個練習除了複習一下先前所學的,主要也是用來說明,使用者介面也有商業邏輯存在(或許不稱為商業邏輯,反正就是有邏輯啦),用來控制使用者的操作行為,比如說使用者在填寫表單時出貨日期不能早於訂貨日期,商品售價不能低於進貨價格(賠錢生意應該沒人要做吧)。範例中說明邏輯判斷要寫在哪裡(邏輯的內容是將來接案時與客戶談完後才知道的),以及如何觸發邏輯驗證。

 

好吧,今天就複習到這裡,明天再見了。