Notify property change defined in the ViewModel when underlying Model data changes - c#

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));
}
}

Related

Bind to Collection Count Where

My question is similar to this one: Bind to Count of List where Typeof
But how does this work for Classes?
In my MainWindow I have the following Collection and a Selected Count Property
private ObservableCollection<MyClass> _myClassCollection = new ObservableCollection<MyClass>();
public ObservableCollection<MyClass>
{
get => _myClassCollection;
set
{
if(_myClassCollection == value) return;
_myClassCollection = value;
OnPropertyChanged("MyClassCollection");
}
}
public int SelectedCount
{
get => MyClassCollection.Where(x => x.IsSelected == true).Count();
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
My MyClass:
public class MyClass : INotifyPropertyChanged
{
// .. Properties
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set
{
if(_isSelected == value) return;
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string name)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
So how can I "run" the SelectedCount Property, if the IsSelected Property of MyClass changed ? I want to show the number of Selected Items of the ObservableCollection in real time.
You can just add an OnPropertyChaned for SelectedCountin the setter of the other operations where a change may have occurred. For instance on setting the my class collection. This will tell stuff listening to that particular property something may have changed, get the value again.
set
{
if(_myClassCollection == value) return;
_myClassCollection = value;
OnPropertyChanged("MyClassCollection");
OnPropertyChanged("SelectedCount"); // Make the change Here.
}
From the comments it would seem you need to listen to each element's property changed explicitly. Here is an example of how that would look in your setter with an event handler.
set
{
// remove subscriptions
if(_myClassCollection != null)
{
foreach(var element in _myClassCollection)
{
element.PropertyChanged -= ElementChanged;
}
}
// set to new collection
_myClassCollection = value;
// subscribe to new elements.
if(_myClassCollection != null)
{
foreach(var element in _myClassCollection)
{
element.PropertyChanged += ElementChanged;
}
}
OnPropertyChanged("MyClassCollection");
OnPropertyChanged("SelectedCount"); // Make the change Here.
}
private void ElementChanged(object sender, PropertyChangedEventArgs e)
{
if(e.PropertyName == nameof(MyClass.IsSelected))
{
OnPropertyChanged("SelectedCount");
}
}
Now if you are adding or removing elements from your collection without creating a new collection, you will need to subscribe or remove subscriptions inside a CollectionChanged event handler.
You can use DynamicData to make it more readable:
var allObjects = new SourceList<MyClass>(); // this is what you populate with your objects
SelectedObjects = allObjects.Connect().Filter(x => x.IsSelected).AsObservableList();
If SelectedObjects is a public property, you can bind like:
<TextBloc Text="{Binding SelectedObjects.Count}"/>
You have to handle the CollectionChanged from your ObservableCollection.
There you have to call OnPropertyChanged("SelectedCount") like in the linked Question.
In your set:
_myClassCollection.CollectionChanged += Handle_CollectionChanged;
In the event handler:
private void Handle_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
OnPropertyChanged("SelectedCount");
}

Using INotifyPropertyChanged to calculate a total

I am having trouble with getting a total count calculation to run when the value of one of the items in a collection is changed. I do have the INotifyPropertChanged event on the class and the collection is an ObservableCollection<T>. I have also tried a BindingList<T> without luck.
public class Inventory
{
public ObservableCollection<Item> Items { get; set; }
public int TotalCount {
get { return Items.Select(i => i.Count).Sum(); }
}
}
public class Item : INotifyPropertyChanged {
private int count;
public int Count {
get { return count; }
set {
count = value;
NotifyPropertyChanged("Count");
}
}
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(string propertyName) {
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
My next though was to register an event handler for each item that is added to the collection, but I am not quite sure how to manage the events that may have already been registered. It seems I am missing something simple here, can anyone point it out?
BTW, I am data binding in WPF to this class.
You need to handle CollectionChanged event, subscribe to newly added items and unsubscribe from deleted items. This is a tedious task to do it every time, so you can subclass ObservableCollection and override OnCollectionChanged.
For an example implementation, see GitHub/Alba.Framework/NotifyingCollection(T).cs (it has some dependencies within the project, so you won't be able to copy-paste it though). There's a simpler implementation on StackOverflow somewhere, but I can't find it at the moment.
Here is the code for that "tedious task" ... first in the Inventory constructor:
Items.CollectionChanged += Items_CollectionChanged;
Then in the body of the Inventory class:
void Items_CollectionChanged(object s, NotifyCollectionChangedEventArgs e) {
if (e.OldItems != null)
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= ItemPropertyChanged;
if (e.NewItems != null)
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += ItemPropertyChanged;
}
void ItemPropertyChanged(object s, PropertyChangedEventArgs e) {
if (e.PropertyName == "Count")
NotifyPropertyChanged("TotalCount");
}

Notify/Bind a Parent Property to compute the sum for a Children property

I have two Classes, one for ViewModel and one for Product.
The Product class has a property called Line Total, and the ViewModel Class has a property called Total Amount. The Product class is bound to a DataGrid and the user
inserts the quantity which subsequently and automatically updates the Line Total.
Here is the ViewModel class:
public class ViewModel : INotifyPropertyChanged
{
public ObservableCollection<Product> products { get; set; }// the children
private decimal _TotalAmount;
public decimal TotalAmount // <=== has to hold sum of [products.LineTotal]
{
get
{
return totalAmount;
}
set
{
if (value != _TotalAmount)
{
_TotalAmount = value;
onPropertyChanged(this, "TotalAmount");
}
}
}
Here is the Product class which is a child:
public class Product : INotifyPropertyChanged
{
private decimal _LineTotal;
public decimal LineTotal
{
get
{
return _LineTotal;
}
set
{
if (value != _LineTotal)
{
_LineTotal = value;
onPropertyChanged(this, "LineTotal");
}
}
}
}
My question is: How the TotalAmount can compute the sum of all Products [Line Total] ? How the child Products can notify the parent ViewModel to update the TotalAmount?
Something like:
foreach(var product in Products)
{
TotalAmount += product.LineTotal;
}
A way to achieve this, would be to recalculate the total amount every time a line total has been edited by the user and every time a product is added or removed from the ObservableCollection.
Since Product implements INotifyPropertyChanged and raises the PropertyChanged event when a new line total is set, the ViewModel can handle that event and recalculate the total amount.
ObservableCollection has a CollectionChanged event that is raised when an item is added or removed from it, so the ViewModel can also handle that event and recalculate. (This part is not really necessary if products can only be changed and not added/removed by the user etc.).
You can try out this small program to see how it could be done:
Code-behind
public partial class MainWindow : Window
{
ViewModel vm = new ViewModel();
public MainWindow()
{
InitializeComponent();
vm.Products = new ObservableCollection<Product>
{
new Product { Name = "Product1", LineTotal = 10 },
new Product { Name = "Product2", LineTotal = 20 },
new Product { Name = "Product3", LineTotal = 15 }
};
this.DataContext = vm;
}
private void AddItem(object sender, RoutedEventArgs e)
{
vm.Products.Add(new Product { Name = "Added product", LineTotal = 50 });
}
private void RemoveItem(object sender, RoutedEventArgs e)
{
vm.Products.RemoveAt(0);
}
}
public class ViewModel : INotifyPropertyChanged
{
private ObservableCollection<Product> _products;
public ObservableCollection<Product> Products
{
get { return _products; }
set
{
_products = value;
// We need to know when the ObservableCollection has changed.
// On added products: hook up eventhandlers to their PropertyChanged events.
// On removed products: recalculate the total.
_products.CollectionChanged += (sender, e) =>
{
if (e.NewItems != null)
AttachProductChangedEventHandler(e.NewItems.Cast<Product>());
else if (e.OldItems != null)
CalculateTotalAmount();
};
AttachProductChangedEventHandler(_products);
}
}
private void AttachProductChangedEventHandler(IEnumerable<Product> products)
{
// Attach eventhandler for each products PropertyChanged event.
// When the LineTotal property has changed, recalculate the total.
foreach (var p in products)
{
p.PropertyChanged += (sender, e) =>
{
if (e.PropertyName == "LineTotal")
CalculateTotalAmount();
};
}
CalculateTotalAmount();
}
public void CalculateTotalAmount()
{
// Set TotalAmount property to the sum of all line totals.
TotalAmount = Products.Sum(p => p.LineTotal);
}
private decimal _TotalAmount;
public decimal TotalAmount
{
get { return _TotalAmount; }
set
{
if (value != _TotalAmount)
{
_TotalAmount = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("TotalAmount"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Product : INotifyPropertyChanged
{
public string Name { get; set; }
private decimal _LineTotal;
public decimal LineTotal
{
get { return _LineTotal; }
set
{
if (value != _LineTotal)
{
_LineTotal = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("LineTotal"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
XAML:
<Window x:Class="WpfApplication3.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<DataGrid ItemsSource="{Binding Products}" AutoGenerateColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Name}" />
<DataGridTextColumn Binding="{Binding LineTotal}" />
</DataGrid.Columns>
</DataGrid>
<Button Click="AddItem">Add item</Button>
<Button Click="RemoveItem">Remove item</Button>
<TextBlock>
<Run>Total amount:</Run>
<Run Text="{Binding TotalAmount}" />
</TextBlock>
</StackPanel>
</Window>
If the ParentViewModel cares about when a property on the ChildModel gets updated, it should subscribe to its PropertyChanged event.
However since you have a Collection of ChildModels, the handler that hooks up the PropertyChanged event should get added/removed in the CollectionChanged event.
// Hook up CollectionChanged event in Constructor
public MyViewModel()
{
Products = new ObservableCollection<Product>();
MyItemsSource.CollectionChanged += Products_CollectionChanged;
}
// Add/Remove PropertyChanged event to Product item when the collection changes
void Products_CollectionChanged(object sender, CollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach(Product item in e.NewItems)
item.PropertyChanged += Product_PropertyChanged;
if (e.OldItems != null)
foreach(Product item in e.OldItems)
item.PropertyChanged -= Product_PropertyChanged;
}
// When LineTotal property of Product changes, re-calculate Total
void Product_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "LineTotal")
{
TotalAmount = products.Sum(p => p.LineTotal);
// Or if calculation is in the get method of the TotalAmount property
//onPropertyChanged(this, "TotalAmount");
}
}
I believe bernd_rausch answer goes in the right direction. The basic question is why do you want to store the TotalAmount in your ViewModel? The only reason could be that you have so many products that it impacts performance. But even in this scenario you have to be carefull with keeping the value consistent.
The safest way would be to write a TotalAmount property that calculates the TotalAmount on the fly. Then chain the Changed events.
public class ViewModel : INotifyPropertyChanged
{
ViewModel()
{
Products = new ObservableCollection<Product>();
Products.CollectionChanged += OnProductsChanged;
}
public ObservableCollection<Product> Products { get; private set; }// the children
public decimal TotalAmount { get { return Products.Select(p => p.LineTotal).Sum(); } }
private void OnProductChanged(object sender, PropertyChangedEventArgs eventArgs)
{
if("LineTotal" != eventArgs.PropertyName)
return;
onPropertyChanged(this, "TotalAmount");
}
private void OnProductsChanged(object sender, NotifyCollectionChangeEventArgs eventArgs)
{
// This ignores a collection Reset...
// Process old items first, for move cases...
if (eventArgs.OldItems != null)
foreach(Product item in eventArgs.OldItems)
item.PropertyChanged -= OnProductChanged;
if (eventArgs.NewItems != null)
foreach(Product item in eventArgs.NewItems)
item.PropertyChanged += OnProductChanged;
onPropertyChanged(this, "TotalAmount");
}
}
I ignored the reset case. But i think this should give you the right direction. If you want to cache the calculation result i would still use this method an do the caching by an internal lazy value that gets reset in one of the change handlers.
I think the value of TotalAmount in the UI is only updated when you set TotalAmount so that a NotifyPropertyChanged event is fired. For this to work, you have to listen to the PropertyChangedEvent of all Products and when the collection changes or LineTotal of a product changes, you have to set TotalAmount to some value different from _TotalAmount.
But this code is really hard to understand: It is not clear why you store a value, that is calculated each time it is read (TotalAmount) in a variable (_TotalAmount). And since _TotalAmount is not set to zero is not the correct value.

Notify ObservableCollection when Item changes

I found on this link
ObservableCollection not noticing when Item in it changes (even with INotifyPropertyChanged)
some techniques to notify a Observablecollection that an item has changed. the TrulyObservableCollection in this link seems to be what i'm looking for.
public class TrulyObservableCollection<T> : ObservableCollection<T>
where T : INotifyPropertyChanged
{
public TrulyObservableCollection()
: base()
{
CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
}
void TrulyObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (Object item in e.NewItems)
{
(item as INotifyPropertyChanged).PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
}
if (e.OldItems != null)
{
foreach (Object item in e.OldItems)
{
(item as INotifyPropertyChanged).PropertyChanged -= new PropertyChangedEventHandler(item_PropertyChanged);
}
}
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
NotifyCollectionChangedEventArgs a = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
OnCollectionChanged(a);
}
}
But when I try to use it, I don't get notifications on the collection. I'm not sure how to correctly implement this in my C# Code:
XAML :
<DataGrid AutoGenerateColumns="False" ItemsSource="{Binding MyItemsSource, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}">
<DataGrid.Columns>
<DataGridCheckBoxColumn Binding="{Binding MyProperty, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</DataGrid.Columns>
</DataGrid>
ViewModel :
public class MyViewModel : ViewModelBase
{
private TrulyObservableCollection<MyType> myItemsSource;
public TrulyObservableCollection<MyType> MyItemsSource
{
get { return myItemsSource; }
set
{
myItemsSource = value;
// Code to trig on item change...
RaisePropertyChangedEvent("MyItemsSource");
}
}
public MyViewModel()
{
MyItemsSource = new TrulyObservableCollection<MyType>()
{
new MyType() { MyProperty = false },
new MyType() { MyProperty = true },
new MyType() { MyProperty = false }
};
}
}
public class MyType : ViewModelBase
{
private bool myProperty;
public bool MyProperty
{
get { return myProperty; }
set
{
myProperty = value;
RaisePropertyChangedEvent("MyProperty");
}
}
}
public class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void RaisePropertyChangedEvent(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChangedEventArgs e = new PropertyChangedEventArgs(propertyName);
PropertyChanged(this, e);
}
}
}
When i run the program, i have the 3 checkbox to false, true, false as in the property initialisation.
but when i change the state of one of the ckeckbox, the program go through item_PropertyChanged but never in MyItemsSource Property code.
The spot you have commented as // Code to trig on item change... will only trigger when the collection object gets changed, such as when it gets set to a new object, or set to null.
With your current implementation of TrulyObservableCollection, to handle the property changed events of your collection, register something to the CollectionChanged event of MyItemsSource
public MyViewModel()
{
MyItemsSource = new TrulyObservableCollection<MyType>();
MyItemsSource.CollectionChanged += MyItemsSource_CollectionChanged;
MyItemsSource.Add(new MyType() { MyProperty = false });
MyItemsSource.Add(new MyType() { MyProperty = true});
MyItemsSource.Add(new MyType() { MyProperty = false });
}
void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Handle here
}
Personally I really don't like this implementation. You are raising a CollectionChanged event that says the entire collection has been reset, anytime a property changes. Sure it'll make the UI update anytime an item in the collection changes, but I see that being bad on performance, and it doesn't seem to have a way to identify what property changed, which is one of the key pieces of information I usually need when doing something on PropertyChanged.
I prefer using a regular ObservableCollection and just hooking up the PropertyChanged events to it's items on CollectionChanged. Providing your UI is bound correctly to the items in the ObservableCollection, you shouldn't need to tell the UI to update when a property on an item in the collection changes.
public MyViewModel()
{
MyItemsSource = new ObservableCollection<MyType>();
MyItemsSource.CollectionChanged += MyItemsSource_CollectionChanged;
MyItemsSource.Add(new MyType() { MyProperty = false });
MyItemsSource.Add(new MyType() { MyProperty = true});
MyItemsSource.Add(new MyType() { MyProperty = false });
}
void MyItemsSource_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
foreach(MyType item in e.NewItems)
item.PropertyChanged += MyType_PropertyChanged;
if (e.OldItems != null)
foreach(MyType item in e.OldItems)
item.PropertyChanged -= MyType_PropertyChanged;
}
void MyType_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "MyProperty")
DoWork();
}
A simple solution is to use BindingList<T> instead of ObservableCollection<T> . Indeed the BindingList relay item change notifications. So with a binding list, if the item implements the interface INotifyPropertyChanged then you can simply get notifications using the ListChanged event.
See also this SO answer.
I solved this case by using static Action
public class CatalogoModel
{
private String _Id;
private String _Descripcion;
private Boolean _IsChecked;
public String Id
{
get { return _Id; }
set { _Id = value; }
}
public String Descripcion
{
get { return _Descripcion; }
set { _Descripcion = value; }
}
public Boolean IsChecked
{
get { return _IsChecked; }
set
{
_IsChecked = value;
NotifyPropertyChanged("IsChecked");
OnItemChecked.Invoke();
}
}
public static Action OnItemChecked;
}
public class ReglaViewModel : ViewModelBase
{
private ObservableCollection<CatalogoModel> _origenes;
CatalogoModel.OnItemChecked = () =>
{
var x = Origenes.Count; //Entra cada vez que cambia algo en _origenes
};
}
You could use an extension method to get notified about changed property of an item in a collection in a generic way.
public static class ObservableCollectionExtension
{
public static void NotifyPropertyChanged<T>(this ObservableCollection<T> observableCollection, Action<T, PropertyChangedEventArgs> callBackAction)
where T : INotifyPropertyChanged
{
observableCollection.CollectionChanged += (sender, args) =>
{
//Does not prevent garbage collection says: http://stackoverflow.com/questions/298261/do-event-handlers-stop-garbage-collection-from-occuring
//publisher.SomeEvent += target.SomeHandler;
//then "publisher" will keep "target" alive, but "target" will not keep "publisher" alive.
if (args.NewItems == null) return;
foreach (T item in args.NewItems)
{
item.PropertyChanged += (obj, eventArgs) =>
{
callBackAction((T)obj, eventArgs);
};
}
};
}
}
public void ExampleUsage()
{
var myObservableCollection = new ObservableCollection<MyTypeWithNotifyPropertyChanged>();
myObservableCollection.NotifyPropertyChanged((obj, notifyPropertyChangedEventArgs) =>
{
//DO here what you want when a property of an item in the collection has changed.
});
}
I know it's late, but maybe this helps others. I have created a class NotifyObservableCollection, that solves the problem of missing notification to item itself, when a property of the item changes. The usage is as simple as ObservableCollection.
public class NotifyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
private void Handle(object sender, PropertyChangedEventArgs args)
{
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset, null));
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null) {
foreach (object t in e.NewItems) {
((T) t).PropertyChanged += Handle;
}
}
if (e.OldItems != null) {
foreach (object t in e.OldItems) {
((T) t).PropertyChanged -= Handle;
}
}
base.OnCollectionChanged(e);
}
While Items are added or removed the class forwards the items PropertyChanged event to the collections PropertyChanged event.
usage:
public abstract class ParameterBase : INotifyPropertyChanged
{
protected readonly CultureInfo Ci = new CultureInfo("en-US");
private string _value;
public string Value {
get { return _value; }
set {
if (value == _value) return;
_value = value;
OnPropertyChanged();
}
}
}
public class AItem {
public NotifyObservableCollection<ParameterBase> Parameters {
get { return _parameters; }
set {
NotifyCollectionChangedEventHandler cceh = (sender, args) => OnPropertyChanged();
if (_parameters != null) _parameters.CollectionChanged -= cceh;
_parameters = value;
//needed for Binding to AItem at xaml directly
_parameters.CollectionChanged += cceh;
}
}
public NotifyObservableCollection<ParameterBase> DefaultParameters {
get { return _defaultParameters; }
set {
NotifyCollectionChangedEventHandler cceh = (sender, args) => OnPropertyChanged();
if (_defaultParameters != null) _defaultParameters.CollectionChanged -= cceh;
_defaultParameters = value;
//needed for Binding to AItem at xaml directly
_defaultParameters.CollectionChanged += cceh;
}
}
public class MyViewModel {
public NotifyObservableCollection<AItem> DataItems { get; set; }
}
If now a property of an item in DataItems changes, the following xaml will get a notification, though it binds to Parameters[0] or to the item itself except to the changing property Value of the item (Converters at Triggers are called reliable on every change).
<DataGrid CanUserAddRows="False" AutoGenerateColumns="False" ItemsSource="{Binding DataItems}">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding Parameters[0].Value}" Header="P1">
<DataGridTextColumn.CellStyle>
<Style TargetType="DataGridCell">
<Setter Property="Background" Value="Aqua" />
<Style.Triggers>
<DataTrigger Value="False">
<!-- Bind to Items with changing properties -->
<DataTrigger.Binding>
<MultiBinding Converter="{StaticResource ParameterCompareConverter}">
<Binding Path="DefaultParameters[0]" />
<Binding Path="Parameters[0]" />
</MultiBinding>
</DataTrigger.Binding>
<Setter Property="Background" Value="DeepPink" />
</DataTrigger>
<!-- Binds to AItem directly -->
<DataTrigger Value="True" Binding="{Binding Converter={StaticResource CheckParametersConverter}}">
<Setter Property="FontWeight" Value="ExtraBold" />
</DataTrigger>
</Style.Triggers>
</Style>
</DataGridTextColumn.CellStyle>
</DataGridTextColumn>
All the solutions here are correct,but they are missing an important scenario in which the method Clear() is used, which doesn't provide OldItems in the NotifyCollectionChangedEventArgs object.
this is the perfect ObservableCollection .
public delegate void ListedItemPropertyChangedEventHandler(IList SourceList, object Item, PropertyChangedEventArgs e);
public class ObservableCollectionEX<T> : ObservableCollection<T>
{
#region Constructors
public ObservableCollectionEX() : base()
{
CollectionChanged += ObservableCollection_CollectionChanged;
}
public ObservableCollectionEX(IEnumerable<T> c) : base(c)
{
CollectionChanged += ObservableCollection_CollectionChanged;
}
public ObservableCollectionEX(List<T> l) : base(l)
{
CollectionChanged += ObservableCollection_CollectionChanged;
}
#endregion
public new void Clear()
{
foreach (var item in this)
if (item is INotifyPropertyChanged i)
i.PropertyChanged -= Element_PropertyChanged;
base.Clear();
}
private void ObservableCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
foreach (var item in e.OldItems)
if (item != null && item is INotifyPropertyChanged i)
i.PropertyChanged -= Element_PropertyChanged;
if (e.NewItems != null)
foreach (var item in e.NewItems)
if (item != null && item is INotifyPropertyChanged i)
{
i.PropertyChanged -= Element_PropertyChanged;
i.PropertyChanged += Element_PropertyChanged;
}
}
}
private void Element_PropertyChanged(object sender, PropertyChangedEventArgs e) => ItemPropertyChanged?.Invoke(this, sender, e);
public ListedItemPropertyChangedEventHandler ItemPropertyChanged;
}
The ObservableCollection and its derivatives raises its property changes internally. The code in your setter should only be triggered if you assign a new TrulyObservableCollection<MyType> to the MyItemsSource property. That is, it should only happen once, from the constructor.
From that point forward, you'll get property change notifications from the collection, not from the setter in your viewmodel.
One simple solution to this is to replace the item being changed in the ObservableCollection which notifies the collection of the changed item. In the sample code snippet below Artists is the ObservableCollection and artist is an item of the type in the ObservableCollection:
var index = Artists.IndexOf(artist);
Artists.RemoveAt(index);
artist.IsFollowed = true; // change something in the item
Artists.Insert(index, artist);

ObservableCollection and Item PropertyChanged

I've seen lots of talk about this question but maybe I'm just too much of a newbie to get it. If I have an observable collection that is a collection of "PersonNames" as in the msdn example (http: //msdn.microsoft.com/en-us/library/ms748365.aspx), I get updates to my View if a PersonName is added or removed, etc. I want to get an update to my View when I change a property in the PersonName as well. Like if I change the first name. I can implement OnPropertyChanged for each property and have this class derive from INotifyPropertyChanged and that seems to get called as expected.
My question is, how does the View get the updated data from the ObservableCollection as the property changed does not cause any event for the ObservableCollection?
This is probably something really simple but why I can't seem to find an example surprises me. Can anyone shed any light on this for me or have any pointers to examples I would greatly appreciate it. We have this scenario in multiple places in our current WPF app and are struggling with figuring it out.
"Generally, the code responsible for displaying the data adds a PropertyChanged event handler to each object currently displayed onscreen."
Could someone please give me an example of what this means? My View binds to my ViewModel which has a ObservableCollection. This collection is made up of a RowViewModel which has properties that support the PropertiesChanged event. But I can't figure out how to make the collection update itself so my view will be updated.
Here is how you would attach/detach to each item's PropertyChanged event.
ObservableCollection<INotifyPropertyChanged> items = new ObservableCollection<INotifyPropertyChanged>();
items.CollectionChanged += items_CollectionChanged;
static void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= item_PropertyChanged;
}
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += item_PropertyChanged;
}
}
static void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
throw new NotImplementedException();
}
We wrote this in the WPF-chat:
public class OcPropertyChangedListener<T> : INotifyPropertyChanged where T : INotifyPropertyChanged
{
private readonly ObservableCollection<T> _collection;
private readonly string _propertyName;
private readonly Dictionary<T, int> _items = new Dictionary<T, int>(new ObjectIdentityComparer());
public OcPropertyChangedListener(ObservableCollection<T> collection, string propertyName = "")
{
_collection = collection;
_propertyName = propertyName ?? "";
AddRange(collection);
CollectionChangedEventManager.AddHandler(collection, CollectionChanged);
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
AddRange(e.NewItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Remove:
RemoveRange(e.OldItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Replace:
AddRange(e.NewItems.Cast<T>());
RemoveRange(e.OldItems.Cast<T>());
break;
case NotifyCollectionChangedAction.Move:
break;
case NotifyCollectionChangedAction.Reset:
Reset();
break;
default:
throw new ArgumentOutOfRangeException();
}
}
private void AddRange(IEnumerable<T> newItems)
{
foreach (T item in newItems)
{
if (_items.ContainsKey(item))
{
_items[item]++;
}
else
{
_items.Add(item, 1);
PropertyChangedEventManager.AddHandler(item, ChildPropertyChanged, _propertyName);
}
}
}
private void RemoveRange(IEnumerable<T> oldItems)
{
foreach (T item in oldItems)
{
_items[item]--;
if (_items[item] == 0)
{
_items.Remove(item);
PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName);
}
}
}
private void Reset()
{
foreach (T item in _items.Keys.ToList())
{
PropertyChangedEventManager.RemoveHandler(item, ChildPropertyChanged, _propertyName);
_items.Remove(item);
}
AddRange(_collection);
}
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void ChildPropertyChanged(object sender, PropertyChangedEventArgs e)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
handler(sender, e);
}
private class ObjectIdentityComparer : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return object.ReferenceEquals(x, y);
}
public int GetHashCode(T obj)
{
return System.Runtime.CompilerServices.RuntimeHelpers.GetHashCode(obj);
}
}
}
public static class OcPropertyChangedListener
{
public static OcPropertyChangedListener<T> Create<T>(ObservableCollection<T> collection, string propertyName = "") where T : INotifyPropertyChanged
{
return new OcPropertyChangedListener<T>(collection, propertyName);
}
}
Weak events
Keeps track of the same item being added multiple times to the collection
It ~bubbles~ up the property changed events of the children.
The static class is just for convenience.
Use it like this:
var listener = OcPropertyChangedListener.Create(yourCollection);
listener.PropertyChanged += (sender, args) => { //do you stuff}
Bill,
I'm sure that you have found a workaround or solution to your issue by now, but I posted this for anyone with this common issue. You can substitute this class for ObservableCollections that are collections of objects that implement INotifyPropertyChanged. It is kind of draconian, because it says that the list needs to Reset rather than find the one property/item that has changed, but for small lists the performance hit should be unoticable.
Marc
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
namespace WCIOPublishing.Helpers
{
public class ObservableCollectionWithItemNotify<T> : ObservableCollection<T> where T: INotifyPropertyChanged
{
public ObservableCollectionWithItemNotify()
{
this.CollectionChanged += items_CollectionChanged;
}
public ObservableCollectionWithItemNotify(IEnumerable<T> collection) :base( collection)
{
this.CollectionChanged += items_CollectionChanged;
foreach (INotifyPropertyChanged item in collection)
item.PropertyChanged += item_PropertyChanged;
}
private void items_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if(e != null)
{
if(e.OldItems!=null)
foreach (INotifyPropertyChanged item in e.OldItems)
item.PropertyChanged -= item_PropertyChanged;
if(e.NewItems!=null)
foreach (INotifyPropertyChanged item in e.NewItems)
item.PropertyChanged += item_PropertyChanged;
}
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
var reset = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
this.OnCollectionChanged(reset);
}
}
}
As you found out, there is no collection-level event that indicates that a property of an item in the collection has changed. Generally, the code responsible for displaying the data adds a PropertyChanged event handler to each object currently displayed onscreen.
Instead of ObservableCollection simply use the BindingList<T>.
The following code shows a DataGrid binding to a List and to item's properties.
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<DataGrid ItemsSource="{Binding}" AutoGenerateColumns="False" >
<DataGrid.Columns>
<DataGridTextColumn Header="Values" Binding="{Binding Value}" />
</DataGrid.Columns>
</DataGrid>
</Window>
using System;
using System.ComponentModel;
using System.Windows;
using System.Windows.Threading;
namespace WpfApplication1 {
public partial class MainWindow : Window {
public MainWindow() {
var c = new BindingList<Data>();
this.DataContext = c;
// add new item to list on each timer tick
var t = new DispatcherTimer() { Interval = TimeSpan.FromSeconds(1) };
t.Tick += (s, e) => {
if (c.Count >= 10) t.Stop();
c.Add(new Data());
};
t.Start();
}
}
public class Data : INotifyPropertyChanged {
public event PropertyChangedEventHandler PropertyChanged = delegate { };
System.Timers.Timer t;
static Random r = new Random();
public Data() {
// update value on each timer tick
t = new System.Timers.Timer() { Interval = r.Next(500, 1000) };
t.Elapsed += (s, e) => {
Value = DateTime.Now.Ticks;
this.PropertyChanged(this, new PropertyChangedEventArgs("Value"));
};
t.Start();
}
public long Value { get; private set; }
}
}
Following is the code giving a simple explanation of answer by #Stack and showing how BindingList is observing if it has a item changed and shows ObservableCollection will not observe the change inside an item.
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
namespace BindingListExample
{
class Program
{
public ObservableCollection<MyStruct> oc = new ObservableCollection<MyStruct>();
public System.ComponentModel.BindingList<MyStruct> bl = new BindingList<MyStruct>();
public Program()
{
oc.Add(new MyStruct());
oc.CollectionChanged += CollectionChanged;
bl.Add(new MyStruct());
bl.ListChanged += ListChanged;
}
void ListChanged(object sender, ListChangedEventArgs e)
{
//Observe when the IsActive value is changed this event is triggered.
Console.WriteLine(e.ListChangedType.ToString());
}
void CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Observe when the IsActive value is changed this event is not triggered.
Console.WriteLine(e.Action.ToString());
}
static void Main(string[] args)
{
Program pm = new Program();
pm.bl[0].IsActive = false;
}
}
public class MyStruct : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private bool isactive;
public bool IsActive
{
get { return isactive; }
set
{
isactive = value;
NotifyPropertyChanged("IsActive");
}
}
private void NotifyPropertyChanged(String PropertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(PropertyName));
}
}
}
}

Categories

Resources