A MVVM pitfall using Master-Detail scenario
Either I do not see the solution or I found a pitfall in using MVVM.
I have this sample 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 Disc开发者_运维百科ount {get;set}
}
Lets assume in my CustomerOrdersViewModel my ObservableCollection Customers is bound to the View via ...="{Binding Customers}" and when the customer is changed from the user the relating Orders are shown in the DataGrid via ItemsSource="{Binding SelectedItem.Orders, ElementName=comboboxCustomer}".
This is possible with MVVM:
I can add a new Customer by simply (for simplicity's sake) calling Customers.Add(new Customer(){...});
.
After the adding I do this: this.RaisePropertyChanged("Customers");
. This will update the view and immediately show the Customer in the Customer-Combobox.
Now comes the impossible part with MVVM.
I can add a new Order by SelectedCustomer.Orders.Add(New Order(){...});
BUT I cannot raise a CollectionChanged/PropertyChanged event like before with the Customers now on the Orders because the Orders Property is not bound to the View via public accessor.
Even if I would expose Orders bindable property to the view, the view itself cares for the Master-Detail switching not the ViewModel...
QUESTION
How is it possible to make Master-Detail work with Add/Del objects in Details-List and immediate update on the View?
This is always difficult, when working with master-detail views. However, one option is typically to take advantage of INotifyPropertyChanged and INotifyCollectionChanged, and track these yourself in the ViewModel. By tracking these properties on your objects, you can handle notifications correctly.
I blogged about a similar issue, where I wanted to have aggregation happening in the "master" list based on values in the details pane (ie: show a total # of orders, that would always be up to date). The issues are identical.
I put some working code up on the Expression Code Gallery demonstrating how you can handle this tracking, and make everything stay up to date in real time, while still staying "pure" in MVVM terms.
We have recently faced a similar issue, but with the additional requirement that the Model consists of plain stupid POCOs.
Our solution is to brutally apply the Model-ViewModel separation. Neither the Model, nor the ViewModel contain an ObservableCollection<ModelEntity>
, instead the Model contains a POCO collection and the ViewModel contains an ObservableCollection<DetailViewModel>
.
That easily solves the Add, Get and Update. Also if only the Master deletes a detail from it's collection the proper events are fired. However, if the detail requests to be deleted it necessarily needs to signal the master (the owner of the collection).
This can be done by abusing a PropertyChanged
event:
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 }
}
Instead you could introduce an public event Action<DetailViewModel> Delete;
in the DetailViewModel
, bind that directly to MasterViewModel::Delete
, etc.
The downside to this approach is that you have to construct a lot of ViewModels which might never be needed for more than their Name, so you really need to keep construction of the ViewModels cheap and make sure that the list does not explode.
On the upside you can ensure that the UI only binds to ViewModel objects and you can keep a lot of the INotifyPropertyChanged goop out of your model, giving you a clean cut between the layers.
精彩评论