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.