Question: What is the proper way to update a bounded UI element to show the values of property defined only in the ViewModel after a property in an individual item in list changes value.
When implementing the INotifyPropertyChanged in a class that are going to be the items in list, it only updates the UI element that specific piece of data is bound to. Like a ListView item or DataGrid cell. And that's fine, that's what we want. But what if we need a total's row, like in an Excel table. Sure there are several ways to go about that specific problem, but the underlying issue here is when the property is defined and calculated in the ViewModel based on data from the Model. Like for example:
public class ViewModel
{
public double OrderTotal => _model.order.OrderItems.Sum(item => item.Quantity * item.Product.Price);
}
When and how does that get notified/updated/called?
Let's try this with a more complete example.
Here's the XAML
<Grid>
<DataGrid x:Name="GrdItems" ... ItemsSource="{Binding Items}"/>
<TextBox x:Name="TxtTotal" ... Text="{Binding ItemsTotal, Mode=OneWay}"/>
</Grid>
This is the Model:
public class Item : INotifyPropertyChanged
{
private string _name;
private int _value;
public string Name
{
get { return _name; }
set
{
if (value == _name) return;
_name = value;
OnPropertyChanged();
}
}
public int Value
{
get { return _value; }
set
{
if (value.Equals(_value)) return;
_value = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName = null)
{
PropertyChanged?.Invoke(this, new propertyChangedEventArgs(propertyName));
}
}
public class Model
{
public List<Item> Items { get; set; } = new List<Item>();
public Model()
{
Items.Add(new Item() { Name = "Item A", Value = 100 });
Items.Add(new Item() { Name = "Item b", Value = 150 });
Items.Add(new Item() { Name = "Item C", Value = 75 });
}
}
And the ViewModel:
public class ViewModel
{
private readonly Model _model = new Model();
public List<Item> Items => _model.Items;
public int ItemsTotal => _model.Items.Sum(item => item.Value);
}
I know this is code looks over simplified, but it's part of a larger, frustratingly difficult application.
All I want to do is when I change an item's value in the DataGrid I want the ItemsTotal property to update the TxtTotal textbox.
So far the solutions I've found include using ObservableCollection and implementing CollectionChanged event.
The model changes to:
public class Model: INotifyPropertyChanged
{
public ObservableCollection<Item> Items { get; set; } = new ObservableCollection<Item>();
public Model()
{
Items.CollectionChanged += ItemsOnCollectionChanged;
}
.
.
.
private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (Item item in e.NewItems)
item.PropertyChanged += MyType_PropertyChanged;
if (e.OldItems != null)
foreach (Item item in e.OldItems)
item.PropertyChanged -= MyType_PropertyChanged;
}
void MyType_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "Value")
OnPropertyChanged(nameof(Items));
}
public event PropertyChangedEventHandler PropertyChanged;
.
.
.
}
And the viewmodel changes to:
public class ViewModel : INotifyPropertyChanged
{
private readonly Model _model = new Model();
public ViewModel()
{
_model.PropertyChanged += ModelOnPropertyChanged;
}
private void ModelOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
OnPropertyChanged(nameof(ItemsTotal));
}
public ObservableCollection<Item> Items => _model.Items;
public int ItemsTotal => _model.Items.Sum(item => item.Value);
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
This solution works, but it just seems like a work around hack that should have a more eloquent implementation. My project has several of these sum properties in the viewmodel and I as it stands, that's a lot of properties to update and a lot of code to write which just feels like more overhead.
I have more research to do, several interesting articles came up while I was writing this question. I'll update this post with links to other solutions as it seems this issues is more common than I thought.
While your project seems like it's MVVM, I don't think it actually is. Yeah, you have layers, but your model and viewmodels are trading responsibilities. One way to keep things pure in an MVVM situation is to never put INotifyPropertyChanged on anything but a viewmodel. If you find yourself putting it in a model, then your model is becoming corrupted by viewmodel responsibilities. Ditto views (though people are less prone to stack INotifyPropertyChanged onto views). It'll help, too, to break your assumption that a view is associated with a single viewmodel. That feels like a cross-over from MVC thinking.
So what I'm saying is that you have a structural problem that begins conceptually. There's no reason a viewmodel can't have a child viewmodel, for example. Indeed, I often find that useful when I have a strong hierarchy of objects. So you'd have Item and ItemViewModel. And whatever your parent object is (say, Parent) and ParentViewModel. The ParentViewModel would have the observable collection of type ItemViewModel and it would subscribe to the OnPropertyChanged events of its children (which would fire an OnPropertyChanged for the total property). That way the ParentViewModel can both alert the UI of property changes and determine if that change needs to be reflected in the Parent model as well (sometimes you'd want to store the aggregate in the parent data, and sometimes not). Calculated fields (like totals) are often present only in the ViewModel.
In short, your ViewModels handle the coordination. Your ViewModel is master of the model data and communication between objects should happen viewmodel to viewmodel rather than through the model. Which means your UI can have a view for the parent and a separately defined view for the child and keeping those isolated works because they communicate through their bound viewmodels.
Does that make sense?
It'd look something like:
public class ParentViewModel : INotifyPropertyChanged
{
private readonly Model _model;
public ParentViewModel(Model model)
{
_model = model;
Items = new ObservableCollection<ItemViewModel>(_model.Items.Select(i => new ItemViewModel(i)));
foreach(var item in Items)
{
item.PropertyChanged += ChildOnPropertyChanged;
}
Items.CollectionChanged += ItemsOnCollectionChanged;
}
private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (Item item in e.NewItems)
item.PropertyChanged += ChildOnPropertyChanged;
if (e.OldItems != null)
foreach (Item item in e.OldItems)
item.PropertyChanged -= ChildOnPropertyChanged;
OnPropertyChanged(nameof(ItemsTotal));
}
private void ChildOnPropertyChanged(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
if (e.PropertyName == "Value")
OnPropertyChanged(nameof(ItemsTotal));
}
public ObservableCollection<ItemViewModel> Items;
public int ItemsTotal => Items.Sum(item => item.Value);
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
Which is complicated, but at least all the complication is contained in your ViewModel and coordinated from there.
This seems to work well enough for my purposes. I'm sure this solution, in various other forms, can be found all over the internet.
public class ChildNotifier<T> : INotifyPropertyChanged where T : INotifyPropertyChanged
{
private ObservableCollection<T> _list;
public ObservableCollection<T> List
{
get { return _list; }
set
{
if (Equals(value, _list)) return;
_list = value;
foreach (T item in _list)
item.PropertyChanged += ChildOnPropertyChanged;
OnPropertyChanged();
}
}
protected ChildNotifier(IEnumerable<T> list)
{
_list = new ObservableCollection<T>(list);
_list.CollectionChanged += ItemsOnCollectionChanged;
foreach (T item in _list)
item.PropertyChanged += ChildOnPropertyChanged;
}
protected abstract void ChildOnPropertyChanged(object sender, propertyChangedEventArgs e);
private void ItemsOnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach (T item in e.NewItems)
item.PropertyChanged += ChildOnPropertyChanged;
if (e.OldItems != null)
foreach (T item in e.OldItems)
item.PropertyChanged -= ChildOnPropertyChanged;
OnPropertyChanged();
}
public event PropertyChangedEventHandler PropertyChanged;
[NotifyPropertyChangedInvocator]
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With