5

I am working with WPF+MVVM.

I have a VM which contains a Customer property. The Customer has an ObservableCollection of Orders. Each Order has an ObservableCollection of Items. Each Items has a Price.

Now, I have the following property on my VM:

public double TotalPrice
{
    return Customer.Orders.Sum(x => x.Items.Sum(y => y.Price));
}

The problem is whenever a change occurs at any point in this graph of objects - the UI should be notified that TotalPrice had changed - but it doesn't...

For example if the Customer will be altered from A to B, or an order will be added, or an item will be deleted, or an item's price will be altered etc.

Does anyone has an elegant solution for this?

Thanks.

Dave Clemmer
  • 3,741
  • 12
  • 49
  • 72
  • Where are the changes to these objects originating from? Is there some kind of hierarchical GUI layout and the user is modifying orders? Or do the changes come from peristence/domain/other non GUI sources? – Erik Dietrich Dec 06 '11 at 16:41
  • Hi Erik, the changes are comming from the UI itself, meaning, the user can change the price for an item, add/delete item, add/delete order etc. – Mosi Altman Dec 06 '11 at 19:59
  • In that case, I'd say that sll has a nice solution. The VM can listen to the collection changed event on Orders and Items and that handler can raise property changed on TotalPrice. As for price being changed, you'd also need to listen to property changed on Item.Price, whose handler would also raise property changed on total. – Erik Dietrich Dec 06 '11 at 20:05
  • Hi Eirk, you should also take a look at the solution ItzikSaban gave me, since it seems like it's encapsulating all the logic you just mentioned. – Mosi Altman Dec 07 '11 at 07:37
  • a little heavy on the wireup for my taste, but an interesting concept. I personally use a NotifyChange(Expression> propertyExpression) paradigm, which lets you invoke NotifyChange(() => PropertyName). If you then parameterize this, you can do NotifyChange(() => Prop1, () => Prop2, etc). The fluent interface of Itzik's solution is cool, but I don't know if I'd want to do all that wireup work in the constructor of the VM for properties the user may never even invoke. Maybe I'll try it out myself and come to like it better, though. – Erik Dietrich Dec 07 '11 at 07:50

4 Answers4

3

Have you supported INotifyPropertyChanged / INotifyCollectionChanged interfaces in ViewModels? You should be able trigger any property manually, for instance in setter of any property you can trigger OnPropertyChanged("TotalPrice") so UI bindings for TotalPrice would be updated as well.

To handle dependent objects changes you can provide events or something like that so ViewModel would be able to subscribe and handle underlying object changes, for instance you have some service which is in chanrge of reloading of the Orders from a database, so as soo as new changes come you would update UI as well. In this case OrdersService should expose event OrdersUpdated and ViewModel can subscribe for this event and in trigger PropertyChanged events for affected properties.

Let's consider some case as an example, for instance Order price has been changed. Who is in charge of this changes? Is this done via UI by an user?

sll
  • 61,540
  • 22
  • 104
  • 156
  • or, a somewhat cleaner way is to use some kind of PropertyObserver like here: http://joshsmithonwpf.wordpress.com/2009/07/11/one-way-to-avoid-messy-propertychanged-event-handling/ – stijn Dec 06 '11 at 16:12
  • I used some kind of observer in unit tests, for isntance you can subscribe for `PropertyChanged` event and then check proeprty name in event handler to ensure that specific property was changed. – sll Dec 06 '11 at 16:15
1

You can find here an interesting post written by me few days ago and it talks exactly about this problem (and its solution...)

ItzikSaban
  • 286
  • 1
  • 5
0

You might implement "accumulator" properties which store the sum of values in a collection of objects. Listen for changes to those values and update the accumulator appropriately.
(BTW - I forgot to mention that this one is really just for situations where the value is expensive to calculate. Otherwise Sll's answer is definitely the way to go.)

Something like this, with the boring stuff left out:

    class BasketOfGoods : INotifyPropertyChanged
{
    ObservableCollection<Good> contents = new ObservableCollection<Good>();

    public decimal Total
    {
        get { /* getter code */ }
        set { /*setter code */ }
    }

    public BasketOfGoods()
    {
        contents.CollectionChanged += contents_CollectionChanged;
    }

    void contents_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        foreach (var newGood in e.NewItems) ((Good)newGood).PropertyChanged += BasketOfGoods_PropertyChanged;
    }

    void BasketOfGoods_PropertyChanged(object sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "Price") Total = contents.Select(x => x.Price).Sum();
    }

}

class Good : INotifyPropertyChanged
{
    public decimal Price
    {
    {
        get { /* getter code */ }
        set { /*setter code */ }
    }
}
Sean U
  • 6,730
  • 1
  • 24
  • 43
0

I think my latest WPF endeavor MadProps handles this scenario pretty well. Take a look at this example master-detail scenario. As long as there a path from the Item being edited to the VM (for example, VM.TheCustomer.SelectedOrder.SelectedItem or simply VM.SelectedItem), the PropChanged event will propogate up to the VM and you can write:

public readonly IProp<Customer> TheCustomer;
public readonly IProp<double> TotalPrice;

protected override void OnPropChanged(PropChangedEventArgs args)
{
    if (args.Prop.IsAny(TheCustomer, Item.Schema.Price))
    {
        TotalPrice.Value = TheCustomer.Value.Orders
            .Sum(order => order.Items.Sum(item => item.Price.Value));
    }
}

Regarding adding and removing Items or Orders, I would just put an explicit update in the ICommand code, something like VM.UpdateTotalPrice();. Managing subscriptions and unsubscriptions to ObservableCollections and the items in them can be tricky.

default.kramer
  • 5,943
  • 2
  • 32
  • 50