Update a ViewModel when an item in an observable collection is updated - c#

Source code is here:
https://github.com/djangojazz/BubbleUpExample
The problem is I am wanting an ObservableCollection of a ViewModel to invoke an update when I update a property of an item in that collection. I can update the data that it is bound to just fine, but the ViewModel that holds the collection is not updating nor is the UI seeing it.
public int Amount
{
get { return _amount; }
set
{
_amount = value;
if (FakeRepo.Instance != null)
{
//The repo updates just fine, I need to somehow bubble this up to the
//collection's source that an item changed on it and do the updates there.
FakeRepo.Instance.UpdateTotals();
OnPropertyChanged("Trans");
}
OnPropertyChanged(nameof(Amount));
}
}
I basically need the member to tell the collection where ever it is called: "Hey I updated you, take notice and tell the parent you are a part of. I am just ignorant of bubble up routines or call backs to achieve this and the limited threads I found were slightly different than what I am doing. I know it could possible be done in many ways but I am having no luck.
In essence I just want to see step three in the picture below without having to click on the column first.

Provided that your underlying items adhere to INotifyPropertyChanged, you can use an observable collection that will bubble up the property changed notification such as the following.
public class ItemObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public event EventHandler<ItemPropertyChangedEventArgs<T>> ItemPropertyChanged;
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs args)
{
base.OnCollectionChanged(args);
if (args.NewItems != null)
foreach (INotifyPropertyChanged item in args.NewItems)
item.PropertyChanged += item_PropertyChanged;
if (args.OldItems != null)
foreach (INotifyPropertyChanged item in args.OldItems)
item.PropertyChanged -= item_PropertyChanged;
}
private void OnItemPropertyChanged(T sender, PropertyChangedEventArgs args)
{
if (ItemPropertyChanged != null)
ItemPropertyChanged(this, new ItemPropertyChangedEventArgs<T>(sender, args.PropertyName));
}
private void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
OnItemPropertyChanged((T)sender, e);
}
}

You should do two things to get it to work:
first: you should refactor the RunningTotal property so it can raise the property changed event. Like so:
private int _runningTotal;
public int RunningTotal
{
get => _runningTotal;
set
{
if (value == _runningTotal)
return;
_runningTotal = value;
OnPropertyChanged(nameof(RunningTotal));
}
}
Second thing you should do is calling the UpdateTotals after you add a DummyTransaction to the Trans. An option could be to refactor the AddToTrans method in the FakeRepo
public void AddToTrans(int id, string desc, int amount)
{
Trans.Add(new DummyTransaction(id, desc, amount));
UpdateTotals();
}

Related

How to get edited values in ObservableCollectionin WPF(MVVM)

public class PricingGrpModel
{
public string Name { get; set; }
public string Description { get; set; }
}
private ObservableCollection<PricingGrpModel> _myCollection;
public ObservableCollection<PricingGrpModel> myCollection
{
get { return _myCollection; }
set { _myCollection= value; OnPropertyChanged("myCollection"); }
}
myCollection.CollectionChanged += new NotifyCollectionChangedEventHandler(myCollection_CollectionChanged);
void myCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
try
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
break;
case NotifyCollectionChangedAction.Remove:
break;
}
}
catch(Exception exception)
{
}
}
I have bound 'myCollection' ObservableCollection to DataGrid's ItemsSource. The Collection Change event fired while adding or removing the Row. But I couldn't track the changes of existing row value. how can I get a notification when an item's property in an ObservableCollection has been changed?
you can use NewItems and OldItems properties of the NotifyCollectionChangedEventArgs.
When you call ObservableCollection.Remove(), the removed items will be present in OldItems property:
void myCollection_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.OldItems != null)
{
foreach (var removedItem in e.OldItems)
{
}
}
}
The only problem is, that when you call ObservableCollection.Clear() method, the OldItems property is null.
If you want to access also cleared items, you will have to create your custom class by inheriting from ObservableCollection and override RemoveItem protected method. RemoveItem method is invoked also when you call ObservableCollection.Clear() You can use my ExtendedObservableCollection from this answer: Trigger InotifyPropertyChanged/CollectionChanged on ObservableCollection
You can't directly track the item Changes via the CollectionChanged Event.
What you can do is:
Add and Remove an EventHandler to the PricingGrpModel.PropertyChanged in the CollectionChanged EventHandler for each item in the ObservableCollection.

Notify a ViewModel that an object in a collection has been selected

Consider the following object, part of a WPF MVVM application:
public class MyObject : INotifyPropertyChanged
{
// INotifyPropertyChanged gubbins
private bool _isSelected;
public bool IsSelected
{
get
{
return _isSelected;
}
set
{
_isSelected = value;
OnPropertyChanged("IsSelected");
}
}
}
And its use in the following ViewModel:
public class MyViewModel : INotifyPropertyChanged
{
// INotifyPropertyChanged gubbins
private List<MyObject> _myObjects;
public List<MyObject> MyObjects
{
get
{
return _myObjects;
}
set
{
_myObjects = value;
OnPropertyChanged("MyObjects");
}
}
public bool CanDoSomething
{
get
{
return MyObjects.Where(d => d.IsSelected).Count() > 0;
}
}
}
In this situation, I can track which of my objects have been selected, and selecting them will fire OnPropertyChanged and so can notify the parent view.
However, CanDoSomething will always be false because there's nowhere I can fire an OnPropertyChanged to create a notification. If I put it in MyObject, it doesn't know anything about the property and so does nothing. There's nowhere to put it in the ViewModel because there's nothing that reacts when an object in the list is selected.
I've tried substituting the List for an ObservableCollection and a custom "TrulyObservableCollection" (see Notify ObservableCollection when Item changes ) but neither work.
How can I get round this, without resorting to click events?
I feel like if I had a better idea of what your end goal was I might be able to recommend a better approach. There is some stuff going on that just feels a little out place. Like maybe 'CanDoSomething' should be part of a command object. And I am wondering if more than one MyObject be selected at a time? If not then I would approach this in an entirely differnt way.
So anyway, you want to update CanDoSomething any time the IsSelected property of one of the items in MyObjects changes. It sounds like you were using an ObservableCollection at one point and then abandoned it. That was a mistake. You need to update CanDoSomething any time one of two events occur; the first is when items are added to or removed from MyObjects and the second is when the IsSelected property of any of the objects in MyObjects changes. For the first event you need something that implements INotifyCollectionChanged, i.e. an ObservableCollection. You already have the second event covered because the objects implement INotifyPropertyChanged. So you just have to combine those two things.
In the following example I have taken your code and made some changes. To start with I changed MyObjects back to an ObservableCollection<MyObject>. It does not have a setter because I have found that there usually is not good reason to change an observable collection; just add and remove objects as necessary. Then in the viewmodel's constructor I am register for the CollectionChanged event of MyObjects. In that handler I am grabbing items that are added to the collection and hooking up their PropertyChanged event to the OnIsSelectedChanged event handler and I am unhooking the PropertyChanged event from OnIsSelectedChanged for any objects that were removed from the collection. Because items have been added or removed we have no idea what the state of IsSelected may be of the objects in MyObjects so this is a good opportunity to update CanDoSomething, and I do at the bottom of the event handler. Finally, the OnIsSelectedChanged is where the other half of the magic happens. Every object in MyObjects will have their PropertyChanged event hooked up to this event handler. Whenever the IsSelected property on any of these objects changes the event handler will update CanDoSomething.
public class MyViewModel : INotifyPropertyChanged
{
// INotifyPropertyChanged gubbins
public MyViewModel()
{
this._myObjects.CollectionChanged += (o, e) =>
{
if (e.NewItems != null)
{
foreach (var obj in e.NewItems.OfType<MyObject>())
{
obj.PropertyChanged += this.OnIsSelectedChanged;
}
}
if (e.OldItems != null)
{
foreach (var obj in e.OldItems.OfType<MyObject>())
{
obj.PropertyChanged -= this.OnIsSelectedChanged;
}
}
if (e.PropertyName == "IsSelected")
{
this.CanDoSomething = this.MyObjects.Any(x => x.IsSelected);
}
};
}
private readonly ObservableCollection<MyObject> _myObjects =
new ObservableCollection<MyObject>();
public ObservableCollection<MyObject> MyObjects
{
get
{
return _myObjects;
}
}
private void OnIsSelectedChanged(object o, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsSelected")
{
this.CanDoSomething = this.MyObjects.Any(x => x.IsSelected);
}
}
private bool _canDoSomething;
public bool CanDoSomething
{
get { return this._canDoSomething; }
private set
{
if (_canDoSomething != value)
{
_canDoSomething = value;
OnPropertyChanged("CanDoSomething");
}
}
}
}
First create a class that defines this attached property:
public static class ItemClickCommand
{
public static readonly DependencyProperty CommandProperty =
DependencyProperty.RegisterAttached("Command", typeof(ICommand),
typeof(ItemClickCommand), new PropertyMetadata(null, OnCommandPropertyChanged));
public static void SetCommand(DependencyObject d, ICommand value)
{
d.SetValue(CommandProperty, value);
}
public static ICommand GetCommand(DependencyObject d)
{
return (ICommand)d.GetValue(CommandProperty);
}
private static void OnCommandPropertyChanged(DependencyObject d,
DependencyPropertyChangedEventArgs e)
{
var control = d as ListViewBase;
if (control != null)
control.ItemClick += OnItemClick;
}
private static void OnItemClick(object sender, ItemClickEventArgs e)
{
var control = sender as ListViewBase;
var command = GetCommand(control);
if (command != null && command.CanExecute(e.ClickedItem))
command.Execute(e.ClickedItem);
}
}
Then just bind this attached property to a delegate command in your view model: helper:ItemClickCommand.Command="{Binding MyItemClickCommand}"
You can find more detail in this blog post: https://marcominerva.wordpress.com/2013/03/07/how-to-bind-the-itemclick-event-to-a-command-and-pass-the-clicked-item-to-it/
Let me know if it works

ObservableCollection Collection Changed event not firing

I have an observable collection in my ViewModel, bound to a datagrid. I want to implement some logic for refreshing the data in other windows based on changes to the collection/ updates to the database (using LINQ to SQL).
Here is my view model code:
public FTViewModel(int JobID)
{
_windowCloseAction = new DelegateCommand(OnWindowClose);
_oFTrn = new ObservableFilesTransmitted(_dataDc, JobID);
_oFTrn.CollectionChanged += oFTrnCollectionChanged;
}
void oFTrnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (FilesTransmitted f in e.NewItems)
f.PropertyChanged += FilesTransmitted_PropertyChanged;
}
if (e.OldItems != null)
{
foreach (FilesTransmitted f in e.OldItems)
f.PropertyChanged -= FilesTransmitted_PropertyChanged;
}
}
void FilesTransmitted_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "DocumentNumber")
{
_filesTransmittedChange = true;
}
_refreshViews = true;
}
and the ObservableCollection constructor:
class ObservableFilesTransmitted : ViewableCollection<FilesTransmitted>
{
public ObservableFilesTransmitted(DocControlDC dataDc, int ID)
{
foreach (FilesTransmitted ftran in dataDc.FilesTransmitteds.Where(x=>x.JobID==ID).OrderByDescending(x => x.TransmittalName))
{
this.Add(ftran);
}
}
}
The debugger does not stop in the oFTrnCollectionChanged. I think because the call to create the observable collection happens before I add the CollectionChanged event. But obvously I can't switch those two lines. I've looked at various StackOverflow and CodeProject topics on this, and it seems like what I have should work. Do I need to add and remove a dummy item just to get the CollectionChanged hander called? What am I missing?
It seems like perhaps I should have a constructor (for the observable collection) that does not add any members, and a function that adds the members from the database. Then I can call new, add the collectionchanged handler, and then fill the collection. I am hoping to avoid that level of rewrite though, but perhaps it's the only reasonable way.
When I run in to this the easiest way to solve it is just subscribe manually at the start.
public FTViewModel(int JobID)
{
_windowCloseAction = new DelegateCommand(OnWindowClose);
_oFTrn = new ObservableFilesTransmitted(_dataDc, JobID);
foreach(var item in _oFTrn)
{
item.PropertyChanged += FilesTransmitted_PropertyChanged;
}
_oFTrn.CollectionChanged += oFTrnCollectionChanged;
}
However a even better solution is instead of using a class derived from ObserveableCollection<T> use a class derived from BindingList<T>. Any member raising their PropertyChanged event will cause the collection to raise ListChanged with the change type of ItemChanged
public FTViewModel(int JobID)
{
_windowCloseAction = new DelegateCommand(OnWindowClose);
_oFTrn = new ObservableFilesTransmitted(_dataDc, JobID);
_oFTrn.CollectionChanged += oFTrnListChanged;
}
void oFTrnListChanged(object sender, ListChangedEventArgs e)
{
if (e.ListChangedType == ListChangedType.ItemChanged)
{
if (e.PropertyDescriptor.Name == "DocumentNumber")
{
_filesTransmittedChange = true;
}
}
_refreshViews = true;
}
I have simply changed the ObservableCollection constructor and added a populate function:
New view model code:
public FTViewModel(int JobID)
{
_oFTrn = new ObservableFilesTransmitted(_dataDc, JobID);
_oFTrn.CollectionChanged += oFTrnCollectionChanged;
_oFTrn.FillCollection();
}
new ObservableCollection class:
class ObservableFilesTransmitted : ViewableCollection<FilesTransmitted>
{
DocControlDC _dc = null;
int _jobID = 0;
public ObservableFilesTransmitted(DocControlDC dataDc, int ID)
{
_dc = dataDc;
_jobID = ID;
}
public void FillCollection()
{
foreach (FilesTransmitted ftran in _dc.FilesTransmitteds.Where(x=>x.JobID==_jobID).OrderByDescending(x => x.TransmittalName))
{
this.Add(ftran);
}
}
}
And it all works as expected. But it gets called for each item added. I may play with the idea that I simply loop through the collection and add the propertychanged handler for each item in the viewmodel constructor. Seems like less of a performance hit that way.

Get changed item from CollectionChanged event using TrulyObservableCollection

I'm using a TrulyObservableCollection as a datasource in a WPF DataGrid. My class implements the PropertyChange event properly (I get notification when a property changes). The CollectionChanged event gets triggered as well. However, my issue lies in the connection between the PropertyChanged event and CollectionChanged event. I can see in the PropertyChanged event which item is being changed (in this case the sender object), however I can't seem to find a way to see which one is changed from within the CollectionChanged event. The sender object is the whole collection. What's the best way to see which item has changed in the CollectionChanged event? The relevant code snippets are below. Thank you for your help, and let me know if there needs to be some clarification.
Code for setting up the collection:
private void populateBret()
{
bretList = new TrulyObservableCollection<BestServiceLibrary.bretItem>(BestClass.BestService.getBretList().ToList());
bretList.CollectionChanged += bretList_CollectionChanged;
dgBretList.ItemsSource = bretList;
dgBretList.Items.Refresh();
}
void bretList_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
//Do stuff here with the specific item that has changed
}
Class that is used in the collection:
public class bretItem : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
private int _blID;
public string _blGroup;
[DataMember]
public int blID
{
get { return _blID; }
set
{
_blID = value;
OnPropertyChanged("blID");
}
}
[DataMember]
public string blGroup
{
get { return _blGroup; }
set
{
_blGroup = value;
OnPropertyChanged("blGroup");
}
}
protected void OnPropertyChanged (String name)
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(name));
}
}
}
TrulyObservableCollection class
public class TrulyObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
public TrulyObservableCollection()
: base()
{
CollectionChanged += new NotifyCollectionChangedEventHandler(TrulyObservableCollection_CollectionChanged);
}
public TrulyObservableCollection(List<T> list)
: base(list)
{
foreach (var item in list)
{
item.PropertyChanged += new PropertyChangedEventHandler(item_PropertyChanged);
}
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);
}
}
EDIT:
In the item_PropertyChanged event the NotifyCollectionChangedEventArgs are set with NotifyCollectionChangedAction.Reset. This causes the OldItems and NewItems to be null, therefore I can't get the changed item in that case. I can't use .Add as the Datagrid is updated with an additional item. I can't appear to get .Replace to work either to get the changed item.
How about this:
In your ViewModel that contains the ObservableCollection of bretItem, the ViewModel subscribes to the CollectionChanged event of the ObservableCollection.
This will prevent the need of a new class TrulyObservableCollection derived from ObservableCollection that is coupled to the items within its collection.
Within the handler in your ViewModel, you can add and remove the PropertyChanged event handler as you are now. Since it is now your ViewModel that is being informed of the changes to objects within the collection, you can take the appropriate action.
public class BretListViewModel
{
private void populateBret()
{
bretList = new ObservableCollection<BestServiceLibrary.bretItem>(BestClass.BestService.getBretList().ToList());
bretList.CollectionChanged += bretList_CollectionChanged;
}
void bretList_CollectionChanged(object sender, System.Collections.Specialized.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)
{
var bret = sender as bretItem;
//Update the database now!
//One note:
//The ObservableCollection raises its change event as each item changes.
//You should consider a method of batching the changes (probably using an ICommand)
}
}
A Thing of Note:
As an aside, it looks like you are breaking the MVVM pattern based upon this snippet:
dgBretList.ItemsSource = bretList;
dgBretList.Items.Refresh();
You probably should consider loading your ViewModel and binding your View to it instead of coding logic in the code-behind of your View.
It's not appropriate to use the collection changed event in this way because it's only meant to be fired when adding/removing items from the collection. Which is why you've hit a wall. You're also in danger of breaking the Liskov substitution principle with this approach.
It's probably better to implement the INotifyPropertyChanged interface on your collection class and fire that event when one of your items fires its property changed event.

EntityFramework EntityCollection Observing CollectionChanged

I'm using EntityFramework database first in an application. I would like somehow to be notified of changes to an EntityCollection in my ViewModel. It doesn't directly support INotifyCollectionChanged (why?) and I haven't been successful in finding another solution.
Here's my latest attempt, which doesn't work because the ListChanged event doesn't appear to get raised:
public class EntityCollectionObserver<T> : ObservableCollection<T>, INotifyCollectionChanged where T : class
{
public event NotifyCollectionChangedEventHandler CollectionChanged;
public EntityCollectionObserver(EntityCollection<T> entityCollection)
: base(entityCollection)
{
IBindingList l = ((IBindingList)((IListSource)entityCollection).GetList());
l.ListChanged += new ListChangedEventHandler(OnInnerListChanged);
}
private void OnInnerListChanged(object sender, ListChangedEventArgs e)
{
if (CollectionChanged != null)
CollectionChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
Does anyone have any ideas how I might observe changes to an EntityCollection?
Dan
have you tried handling
AssociationChanged Occurs when a change is made to a related end. (Inherited from RelatedEnd.)
It gives arguments showing whether an element was added or deleted and also exposes the element.
Whilst it worked in the simple use case noted by #Aron, I couldn't get it to work properly in my actual application.
As it turns out, and for reasons I'm not sure - the inner IBindingList of an EntityCollection somehow, somewhere, can get changed. The reason my observers weren't being called is because they were looking for changes on an old IBindingList that wasn't even being used by the EntityCollection any more.
Here is the hack that got it working for me:
public class EntityCollectionObserver<T> : ObservableCollection<T> where T : class
{
private static List<Tuple<IBindingList, EntityCollection<T>, EntityCollectionObserver<T>>> InnerLists
= new List<Tuple<IBindingList, EntityCollection<T>, EntityCollectionObserver<T>>>();
public EntityCollectionObserver(EntityCollection<T> entityCollection)
: base(entityCollection)
{
IBindingList l = ((IBindingList)((IListSource)entityCollection).GetList());
l.ListChanged += new ListChangedEventHandler(OnInnerListChanged);
foreach (var x in InnerLists.Where(x => x.Item2 == entityCollection && x.Item1 != l))
{
x.Item3.ObserveThisListAswell(x.Item1);
}
InnerLists.Add(new Tuple<IBindingList, EntityCollection<T>, EntityCollectionObserver<T>>(l, entityCollection, this));
}
private void ObserveThisListAswell(IBindingList l)
{
l.ListChanged += new ListChangedEventHandler(OnInnerListChanged);
}
private void OnInnerListChanged(object sender, ListChangedEventArgs e)
{
base.OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
How are you mapping the event? Pasting your code and mapping the event like follows works for me.
static void Main(string[] args)
{
EntityCollection<string> col = new EntityCollection<string>();
EntityCollectionObserver<string> colObserver = new EntityCollectionObserver<string>(col);
colObserver.CollectionChanged += colObserver_CollectionChanged;
col.Add("foo");
}
static void colObserver_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
Console.WriteLine("Entity Collection Changed");
}

Categories