使用Master-Detail场景的MVVM陷阱

时间:2010-02-27 19:53:29

标签: .net mvvm master-detail

要么我没有看到解决方案,要么我在使用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客户绑定到View via ... =“{Binding Customers}”,当客户从用户更改时,相关订单通过ItemsSource =“{Binding”显示在DataGrid中SelectedItem.Orders,ElementName = comboboxCustomer}“。

这可以通过MVVM实现:

我可以通过简单地(为简单起见)调用Customers.Add(new Customer(){...});来添加新客户。

添加后,我执行此操作:this.RaisePropertyChanged("Customers");。这将更新视图并立即在Customer-Combobox中显示客户。

现在MVVM出现了不可能的部分。

我可以按SelectedCustomer.Orders.Add(New Order(){...});

添加新订单

但我不能像以前那样通过订单上的客户提升CollectionChanged / PropertyChanged事件,因为订单属性未通过公共访问者绑定到View。

即使我将Orders bindable属性暴露给视图,视图本身也会关注Master-Detail切换而不是ViewModel ......

问题

如何使Master-Detail在Details-List中使用Add / Del对象并在View上立即更新?

2 个答案:

答案 0 :(得分:4)

使用主 - 详细视图时,这一直很困难。但是,一个选项通常是利用INotifyPropertyChanged和INotifyCollectionChanged,并在ViewModel中自己跟踪这些选项。通过在对象上跟踪这些属性,您可以正确处理通知。

blogged about a similar issue,我希望根据详细信息窗格中的值在“主”列表中进行聚合(即:显示总数为#的订单,总是最新的)。问题是一样的。

我放了一些working code up on the Expression Code Gallery来演示如何处理这种跟踪,让所有内容实时保持最新,同时在MVVM术语中保持“纯粹”。

答案 1 :(得分:0)

我们最近遇到了类似的问题,但附加要求该模型由普通的愚蠢POCO组成。

我们的解决方案是残酷地应用Model-ViewModel分离。模型和ViewModel都不包含ObservableCollection<ModelEntity>,而模型包含POCO集合,ViewModel包含ObservableCollection<DetailViewModel>

轻松解决添加,获取和更新问题。此外,如果只有Master从其集合中删除了一个细节,则会触发正确的事件。 但是,如果要删除的详细信息请求必须发信号通知主人(集合的所有者)。

这可以通过滥用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,这些ViewModel可能永远不需要超过它们的Name,所以你真的需要保持ViewModel的构造便宜,并确保列表不会爆炸。

从好处来看,您可以确保UI仅绑定到ViewModel对象,并且可以将大量的INotifyPropertyChanged goop保留在模型之外,从而在各层之间提供干净的切割。