A MVVM pitfall using Master-Detail scenario
Asked Answered
T

2

6

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 Discount {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?

Thirtyone answered 27/2, 2010 at 19:53 Comment(0)
L
4

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.

Lynnell answered 27/2, 2010 at 20:1 Comment(8)
I have checked your code and must say it makes all too much complicated. WPF is nice. MVVM makes things too much complicated easy in WinForms. Entity Framework is not possible with MVVM a joke if you ask me. Try to make a Eager Loading with MVVM then you know what I mean. Every LOB has plenty of Master-Detail. Now I know why every MVVM sample out there is a stupid and plain Show all customers list demo... If you know of another MVVM Master-Detail sample I would appreciate a link :) For those interest in that matter too: codeproject.com/KB/WPF/WpfNhibernateToolkit.aspxThirtyone
MVVM deals alot with relation between View and ViewModel but about the relation VM to Model its totally silently guess why? You get Data from your DAL and read related customers, orders, products in 3 ObservableCollection<XXXViewModel> ? This is a big effort creating sort of entityViewModel context... seems too many devs played with VS2010 wpf RAD designer tools to realize real LOB apps need much more...Thirtyone
Ahh - but I disagree here. Yes, the code is complicated, but it's completely reusable. Using it is just dragging a behavior onto your master list, and it "just works".Lynnell
btw. I had problems with your probject. I added the interactivity .dll + had to rebuild to .net 4.0 using above dll with v4.0 and VS2010. Still can`t compile...the interactivity tag in xaml is not recognized. The old problem xaml has just occured again ;-)Thirtyone
ok forgot to convert all to .net 4.0 and add some more .dll`s works now, will have a deeper look at it now :)Thirtyone
Just found for those interest how Master-Detail could work with MVVM: "Scenarios like, dialog box, simple form, master – detail, complex master detail with several embedded ViewModels, etc." => karlshifflett.wordpress.com/mvvm Can`t wait to see this be done by a pro XDThirtyone
Just FYI - There are multiple approaches to this. Karl has some great ideas, but in his own words, he's "not a purist" when it comes to MVVM - so realize that may be true for his implementations.Lynnell
There are multiple approaches to this? Can you show me other approaches please? Links...Thirtyone
S
0

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.

Smaze answered 7/3, 2013 at 15:30 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.