۱۳۹۱ بهمن ۱, یکشنبه

استفاده از الگوی Proxy در MVVM - قسمت دوم


در پست قبل درباره نحوه پیاده سازی الگوی Proxy در یک ViewModel ساده صحبت کردم و گفتم که همیشه Model ها ساده نیستند. اجازه بدید یک Model جدید به مسئله اضافه کنیم و Person رو به این شکل تغییر بدیم:
  public class Account
  {
      public int Id { get; set; }
      public Person Owner { get; set; }
      public decimal Balance { get;set; }
      public string Description { get;set; }
  }

  public class Person
  {
      public Person()
      {
          this.Accounts = new HashSet<Account>();
      }
  
      public int Id { get; set; }
      public string FullName { get; set; }
  
      public ICollection<Account> Accounts { get;set; }
  }

حالا ما یه ارجاع یک به چند (1:n) داریم، یعنی به ازای هر Person، چندین Account داریم و هر Account فقط و فقط مرتبط با یک Person است. View مورد نظر باید چه شکلی باشه که بتونه با این Model ها کار کنه؟ احتمالا ما یک فرم کل-جزء (Master-Detail) خواهیم داشت. به این صورت که در یک TextBox، نام شخص و در یک DataGrid مشخصات چندین Account وی از کاربر دریافت میشه. اما از اونجایی که View ها معمولا به ViewModel ها Bind میشوند، پس باید PersonViewModel رو به این شکل تغییر بدیم:
  public class PersonViewModel : ViewModel<Person>
  {
      public PersonViewModel(Person model) : base(model)
      {
      }
  
      public PersonViewModel() : this(new Person())
      {
      }
  
      public int Id
      {
          get
          {
              return Model.Id;
          }
          set
          {
              if (Model.Id != value)
              {
                  Model.Id = value;
                  OnPropertyChanged("Id");
              }
          }
      }
  
      public string FullName
      {
          get
          {
              return Model.FullName;
          }
          set
          {
              if (Model.FullName != value)
              {
                  Model.FullName = value;
                  OnPropertyChanged("FullName");
              }
          }
      }
  
      private readonly ObservableCollection<Account> _accounts = new ObservableCollection<Account>();
      public ObservableCollection<Account> Accounts
      {
          get
          {
              return _accounts;
          }
      }
  }
ما Account رو به همون صورت Model در ViewModel استفاده کردیم. تا اینجا ما فقط لیستی داریم که می تونه بطور یک طرفه اطلاعات رو از کاربر بگیره. اما اگه بخاطر داشته باشید برای Bind شدن دو طرفه نیازه که واسط INotifyPropertyChanged برای کلاس مورد نظر پیاده سازی شده باشه و ما چنین کاری رو برای Account نکردیم چون قرار بود Model رو همیشه ساده نگه داریم. پس دست به کار میشیم و یک AccountViewModel پیاده سازی می کنیم:
  public class AccountViewModel : ViewModel<Account>
  {
      public AccountViewModel(Account model) : base(model)
      {
      }
  
      public AccountViewModel() : this(new Account())
      {
      }
  
      public int Id
      {
          get
          {
              return Model.Id;
          }
          set
          {
              if (Model.Id != value)
              {
                  Model.Id = value;
                  OnPropertyChanged("Id");
              }
          }
      }
  
      public decimal Balance
      {
          get
          {
              return Model.Balance;
          }
          set
          {
              if(Model.Balance != value)
              {
                  Model.Balance = value;
                  OnPropertyChanged("Balance");
              }
          }
      }
  
      public string Description
      {
          get
          {
              return Model.Description;
          }
          set
          {
              if (Model.Description != value)
              {
                  Model.Description = value;
                  OnPropertyChanged("Description");
              }
          }
      }
  }

تنها نکته ای که درباره این قسمت باید بگم اینه که نیازی به پیاده سازی مشخصه Account.Person نیست، چون قرار نیست در DataGrid نمایش داده بشه. حالا تغییرات رو به مشخصه Accounts در PersonViewModel اعمال می کنیم:
      private readonly ObservableCollection<AccountViewModel> _accounts = new ObservableCollection<AccountViewModel>();
      public ObservableCollection<AccountViewModel> Accounts
      {
          get
          {
              return _accounts;
          }
      }

کار ما به نظر تموم شده میاد، ولی اینطور نیست. هنوز مدل Account رو به مدل Person وصل نکردیم! ObservableCollection یک رویداد به نام CollectionChanged داره که وقتی آیتمی به اون اضافه یا حذف بشه فراخوانی میشه. این باعث میشه وقتی کاربر رکوردی رو به DataGrid اضافه یا حذف کردیم Model ما به روز بشه. همینطور باید وقتی PersonViewModel مقدار دهی اولیه میشه، Account های موجود در Person به Account های PersonViewModel اضافه بشه. دوباره کد PersonViewModel رو تغییر می دیم. پس از مقدار دهی اولیه Accounts، باید در تابع سازنده یک تابع رو به Event Handler معرفی کنیم:
    public PersonViewModel(Person model) : base(model)
    {
        foreach (var account in model.Accounts)
            _accounts.Add(new AccountViewModel(account));
        _accounts.CollectionChanged += new NotifyCollectionChangedEventHandler(AccountsCollectionChanged);
    }

دقت کنید که تابع سازنده دوم، تابع سازنده اول رو فراخوانی می کنه، پس نیازی نیست که در هر دوی ان ها، کد بنویسیم! حالا تابع AccountsCollecionChaged رو پیاده سازی می کنیم:
      private void AccountsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
      {
          switch (e.Action)
          {
              case NotifyCollectionChangedAction.Add:
                  foreach (var item in e.NewItems.Cast<AccountViewModel>())
                      Model.Accounts.Add(item.Model);
                  break;
  
              case NotifyCollectionChangedAction.Remove:
                  foreach (var item in e.OldItems.Cast<AccountViewModel>())
                      Model.Accounts.Remove(item.Model);
                  break;
          }
      }

در توضیح کد باید بگم، ابتدا باید تشخیص بدیم چه عملیاتی روی ObservableCollection ما رخ داده؟ به کمک e.Action می تونیم متوجه بشیم. در صورتی که آیتم یا آیتم هایی اضافه شده باشه در e.NewItems قرار داره و چون از نوع Object هست ما به نوع AccountViewModel تبدیل می کنیم. در صورتی که آیتم هایی حذف شده باشند در e.OldItems قرار می گیرند. همینطور هر کدوم از آیتم ها دارای مشخصه ای به نام Model از نوع Account هستند و می تونیم معادلش رو در Person.Accouns پیدا کنیم و عملیات مورد نظر رو روش انجام بدیم. بدین صورت همواره Model ما تحت تاثیر تغییرات در ViewModel قرار داره.