Ловушка MVVM с использованием сценария Master-Detail - PullRequest
6 голосов
/ 27 февраля 2010

Либо я не вижу решения, либо обнаружил ловушку при использовании MVVM.

У меня есть этот образец Master-Detail:

class Customer
{
    int CustomerID {get;set}
    string Name {get;set}
    ObservableCollection<Order> Orders {get;set}
}

class Order
{
    int OrderID {get;set}
    int Quantity {get;set}
    double Discount {get;set}
}

Предположим, что в моей CustomerOrdersViewModel мои клиенты ObservableCollection привязаны к представлению через ... = "{Binding Customers}", и когда клиент меняется от пользователя, соответствующие заказы отображаются в DataGrid через ItemsSource = "{Binding SelectedItem.Orders, ElementName = comboboxCustomer} ".

Это возможно с MVVM:

Я могу добавить нового Клиента, просто (для простоты), позвонив Customers.Add(new Customer(){...});.

После добавления я делаю это: this.RaisePropertyChanged("Customers");. Это обновит представление и сразу же покажет клиента в поле со списком клиентов.

Теперь появилась невозможная часть с MVVM.

Я могу добавить новый заказ по SelectedCustomer.Orders.Add(New Order(){...});

НО я не могу вызвать событие CollectionChanged / PropertyChanged, как раньше, с клиентами, находящимися в настоящее время в Заказах, поскольку свойство Orders не привязано к представлению через общедоступный метод доступа.

Даже если бы я выставил привязываемое свойство Orders к представлению, само представление заботится о переключении Master-Detail, а не ViewModel ...

ВОПРОС

Как можно заставить Master-Detail работать с объектами Add / Del в Details-List и немедленным обновлением в View?

Ответы [ 2 ]

4 голосов
/ 27 февраля 2010

Это всегда сложно при работе с представлениями мастер-детали. Однако один из вариантов, как правило, заключается в том, чтобы воспользоваться преимуществами INotifyPropertyChanged и INotifyCollectionChanged и отслеживать их самостоятельно в ViewModel. Отслеживая эти свойства на ваших объектах, вы можете правильно обрабатывать уведомления.

Я написал в блоге о подобной проблеме , где я хотел, чтобы агрегация происходила в «главном» списке на основе значений в области сведений (т. Е. Показывать общее количество заказов, которое всегда будет до настоящего времени). Проблемы идентичны.

Я поместил несколько рабочих кодов в галерею кодов выражений , демонстрирующих, как вы можете справиться с этим отслеживанием, и сделайте все в актуальном состоянии в реальном времени, при этом оставаясь "чистым" в терминах MVVM. 1009 *

0 голосов
/ 07 марта 2013

Недавно мы столкнулись с аналогичной проблемой, но с дополнительным требованием, чтобы Модель состояла из простых глупых POCO.

Наше решение - жестоко применить разделение Model-ViewModel. Ни Модель, ни ViewModel не содержат ObservableCollection<ModelEntity>, вместо этого Модель содержит коллекцию POCO, а ViewModel содержит ObservableCollection<DetailViewModel>.

Это легко решает Добавить, Получить и Обновить. Также, если только Мастер удаляет детали из своей коллекции, запускаются соответствующие события. Однако, если деталь запрашивает удаление, он обязательно должен подать сигнал мастеру (владельцу коллекции).

Это можно сделать, злоупотребив событием PropertyChanged:

class MasterViewModel {
  private MasterModel master;
  private ISomeService service;
  private ObservableCollection<DetailViewModel> details;

  public ObservableCollection<DetailViewModel> Details { 
    get { return this.details; }
    set { return this.details ?? (this.details = LoadDetails()); }
  }

  public ObservableCollection<DetailViewModel> LoadDetails() {
    var details = this.service.GetDetails(master);
    var detailVms = details.Select(d => 
      {
        var vm = new DetailViewModel(service, d) { State = DetailState.Unmodified };
        vm.PropertyChanged += this.OnDetailPropertyChanged;
        return vm;
      });

    return new ObservableCollection<DetailViewModel>(detailVms);
  }

  public void DeleteDetail(DetailViewModel detailVm) {
    if(detailVm == null || detailVm.State != DetailState.Deleted || this.details == null) {
      return;
    }

    detailVm.PropertyChanged -= this.OnDetailPropertyChanged;

    this.details.Remove(detailVm);
  }

  private void OnDetailPropertyChanged(object s, PropertyChangedEventArgs a) {
    if(a.PropertyName == "State" & (s as DetailViewModel).State == DetailState.Deleted) {
      this.DeleteDetail(s as DetailViewModel);
    }
  }
}

class DetaiViewModel : INotifyPropertyChanged {
  public DetailState State { get; private set; } // Notify in setter..

  public void Delete() {
    this.State = DetailState.Deleted;
  }

  public enum DetailState { New, Unmodified, Modified, Deleted }
}

Вместо этого вы можете ввести public event Action<DetailViewModel> Delete; в DetailViewModel, связать его непосредственно с MasterViewModel::Delete и т. Д.

Недостатком этого подхода является то, что вам нужно создать множество моделей ViewModel, которые могут никогда не понадобиться для большего, чем их Name, поэтому вам действительно нужно сохранять дешевую конструкцию моделей ViewModel и следить за тем, чтобы список не взрывался.

С другой стороны, вы можете быть уверены, что пользовательский интерфейс привязывается только к объектам ViewModel, и вы можете не допускать большую часть INotifyPropertyChanged goop из вашей модели, обеспечивая вам чистый разрыв между слоями.

...