2017/6/13

Xamarin.Forms 教學系列文(十八.壹 - 1) MVVM - INPC 縮寫



接續上一小節,單純想獨立出來以方便資料查找與引用~

前一小節的兩支範例可以發現兩個問題:
  • OnPropertyChanged 使用 弱型別 當作參數 (程式常常死在這種地方,打錯一個字,幹)
  • set 有太多重複的程式碼,每次都要寫判斷值是否相同這件事


解決弱型別

一般來說寫 OnPropertyChanged 時會像前一小節這樣的寫法
public double Number
{
    set
    {
        if (number != value)
        {
            number = value;
            OnPropertyChanged("Number");
        }
    }
    get
    {
        return number;
    }
}

這種寫法有個很大的問題,就是當代入的參數打錯字時 (弱型別),程式不會產生例外錯誤,但就是無法正常運作。

這個問題可以藉由 C# 5.0 一個特殊的方法來解決,CallerMemberNameAttribute,可以讓你在參數取得 "呼叫此方法的屬性名稱"

改寫一下:
protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChangedEventHandler handler = PropertyChanged;
    if (handler != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

用法如下,就不用自行帶入弱型別的字串了:
public double Number
{
    set
    {
        if (number != value)
        {
            number = value;
            OnPropertyChanged();

            // Do something with the new value. 
        }
    }
    get
    {
        return number;
    }
}


接著為了讓我們的程式更精簡,

由於每個 set 都有重複的商業邏輯,不如直接寫一支泛型方法來使用,

這裡將 判斷新舊值是否更改 這件事改成泛型方法,並結合上方寫好的新 OnPropertyChanged:
bool SetProperty(ref T storage, T value, [CallerMemberName] string propertyName = null)
{
    if (Object.Equals(storage, value))
        return false;

    storage = value;
    OnPropertyChanged(propertyName);

    return true;
}

protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
    PropertyChangedEventHandler handler = PropertyChanged;

    if (handler != null)
    {
        PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
    }
}

用法如下:
public double Number
{
    set
    {
        if (SetProperty(ref number, value))
        {
            // Do something with the new value. }
        }
    }
    get
    {
        return number;
    }
}

如果沒有其他邏輯可以更縮減:
public double Number
{
    set { SetProperty(ref number, value); }
    get { return number; }
}


最後,乾脆將這兩個方法寫在 ViewModelBase 類別內,以便 ViewModel 繼承使用:
namespace Xamarin.FormsBook.Toolkit
{
    public class ViewModelBase : INotifyPropertyChanged
    {
        public event PropertyChangedEventHandler PropertyChanged;

        protected bool SetProperty< T>(ref T storage, T value, [CallerMemberName] string propertyName = null)
        {
            if (Object.Equals(storage, value))
                return false;
            storage = value;

            OnPropertyChanged(propertyName);

            return true;
        }

        protected void OnPropertyChanged([CallerMemberName] string propertyName = null)
        {
            PropertyChangedEventHandler handler = PropertyChanged;
            if (handler != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }
    }
}




14 則留言:

  1. 作者已經移除這則留言。

    回覆刪除
  2. 羅根大你好
    我在https://forums.xamarin.com/discussion/comment/371718#Comment_371718
    有發問,Picker相關的問題,同樣是用mvvm的架構,其中有一個citypicker可以binding到viewmodel的資料
    但是regionpicker則binding不到,想請教是為什麼呢?

    回覆刪除
    回覆
    1. 綁定的觀念不對,程式碼內 x:Reference 意思是將 RegionPicker 與 CityPicker 綁定,但是 CityPicker 並沒有 MyRegion 這個屬性

      刪除
    2. 所以思考要回到 ViewModel 內,我的 RegionPicker 的 ItemSource 應該要跟 ViewModel 內的某個屬性綁定,例如 MyRegion (注意這個屬性是能夠做雙向溝通的,意思就是當 MyRegion 更改時有辦法去通知 View),當我 CityPicker 的 SelectedItem 做更改時,同時去更改 MyRegion

      刪除
    3. 完整 ViewModel
      class CityViewModel : ViewModelBase
      {
      private City _selectedCity;
      public City SelectedCity
      {
      get { return _selectedCity; }
      set
      {
      if (_selectedCity != value)
      {
      _selectedCity = value;

      MyRegion = CitiesList
      .Where(x => x.Name == _selectedCity.Name).SelectMany(s => s.Regions).ToList();
      }
      }
      }

      List _myregion;
      public List MyRegion
      {
      set { SetProperty(ref _myregion, value); }
      get { return _myregion; }
      }

      public List CitiesList
      {
      get
      {
      return new List()
      {
      new City() { Key = 1, Name = "Keelung", Regions = { "CenterK", "EastK", "NorthK", "WestK", "SouthK" } },
      new City() { Key = 2, Name = "Hsinchu", Regions = { "HEast", "HNorth", "HCenter" } },
      new City() { Key = 3, Name = "Chiayi", Regions = { "CEast", "CWest" } }
      };
      }
      }
      }

      刪除
    4. View
      Picker x:Name="CityPicker"
      Title= "City"
      ItemsSource="{Binding CitiesList}"
      ItemDisplayBinding="{Binding Name}"
      SelectedItem="{Binding SelectedCity}"

      Picker x:Name="RegionPicker"
      Title="Region"
      ItemsSource="{Binding MyRegion}"
      ItemDisplayBinding="{Binding .}"

      刪除
    5. 感謝您的回覆,我會試試看。

      刪除
    6. 「CityPicker 的 SelectedItem 做更改時,同時去更改 MyRegion」我在程式碼使用foreach的方式將Regions = { "CenterK", "EastK", "NorthK", "WestK", "SouthK" }填入_myregion,在debug model中是可以看到public List MyRegion的值的確被正確填入,而在xaml的RegionPicker的ItemSource是Binding MyRegion,而ItemDisplayBinding是Binding RegionName(有宣告在MyRegion的屬性),我最不解的是,『在debugmodel底下,可以看到MyRegion的值,在xaml也宣告的沒問題,為何會Binding不到,有可能是被GC了嘛?

      刪除
    7. 羅根你好,我再次去詳讀了關於Picker的xamarin文件,在第二個段落的第一句「Xamarin.Forms 2.3.4,填入的程序之前 Picker 的資料「已加入的資料顯示為唯讀 Items 集合」,其中的型別IList. 集合中的每個項目必須是型別string。 」Prior to Xamarin.Forms 2.3.4, the process for populating a Picker with data was to add the data to be displayed to the 「read-only」 Items collection, which is of type IList.
      我解讀成Picker只能夠Binding readonly的資料。在我的行為中,MyRegion並不是readonly而是會跟著selcetitem改變的資料,因此無法藉由Picker完成,也許要用listView來取代RegionPicker才能夠完成。

      刪除
    8. 有試上面我給的程式碼嗎? 我實際執行後是沒問題的...

      刪除
    9. 羅根大我真的感到很不好意思!!!,先跟你說聲抱歉。
      我看了你的程式碼,發現一個我之前沒想過得語法
      ItemsSource="{Binding MyRegion}"
      ItemDisplayBinding="{Binding .}"
      就是ItemDisplayBinding裡面的「.」,我程式碼都沒更改只是在
      xaml的RegionPicker的Binding改成
      Picker x:Name="RegionPicker"
      Title="鄉鎮區選擇"
      ItemsSource="{Binding SelectedCity.Regions}"
      ItemDisplayBinding="{Binding .}"/>
      在之前我宣告一個model為
      public class City
      {
      public int Key { get; set; }
      public string Name { get; set; }
      public List Regions { get; set; }
      public City()
      {
      Regions = new List();
      }
      }
      問題解決了,非常感謝你。你的回覆是我思考的一個正確的方向!!

      刪除
    10. 有試過羅大的程式碼,但是MyRegion依然是binding不到資料

      刪除
    11. 羅根大你好,你的程式碼也是可以用的,但我之前試的時候,可能有多宣告一些model導致
      無法binding,在此跟你通知。在一次的感謝您的回覆及建議

      刪除
  3. 作者已經移除這則留言。

    回覆刪除