How to propagate property change notifications of objects within collections - c#

Lets say I have classes like this
public class R
{
protected string name;
protected List<S> listOfObjectS;
}
public class S
{
private string name, ID;
private A objectA;
}
public class A
{
private string name;
private int count;
}
If a user has two views open, one displaying instances of R and another allowing users to modify an instance of A, I need the view of R to change when the user changes any instance of A.
If the user changes a property of an instance of A, what is the best way to propagate that change (through instances of S) so that all instances of R display the new state of A?

EDIT: Overhauling this answer to be more specific to the question since the tags show you already knew about INotifyPropertyChanged.
You need to implement INotifyPropertyChanged in class A and in class S. Make it so objectA can only be set through a property that will raise the PropertyChanged event on S whenever a property is changed in A. Example:
public class A : INotifyPropertyChanged
{
private string name;
public string Name
{
get { return name; }
set { name = value; OnPropertyChanged("Name"); }
}
private int count;
public int Count
{
get { return count; }
set { count = value; OnPropertyChanged("Count"); }
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
... and class S...
public class S : INotifyPropertyChanged
{
private string name, ID;
private A objectA;
public A ObjectA
{
get { return objectA; }
set
{
var old = objectA;
objectA = value;
// Remove the event subscription from the old instance.
if (old != null) old.PropertyChanged -= objectA_PropertyChanged;
// Add the event subscription to the new instance.
if (objectA != null) objectA.PropertyChanged += objectA_PropertyChanged;
OnPropertyChanged("ObjectA");
}
}
void objectA_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
// Propagate the change to any listeners. Prefix with ObjectA so listeners can tell the difference.
OnPropertyChanged("ObjectA." + e.PropertyName);
}
public event PropertyChangedEventHandler PropertyChanged;
protected void OnPropertyChanged(string propertyName)
{
if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
For class R, use ObservableCollection<S> instead of List<S>, and subscribe to its CollectionChanged event, and monitor when objects are added or removed to listOfObjectS. When they are added, subscribe to S's PropertyChanged events. Then updated R's view. Example:
public class R
{
protected string name;
protected System.Collections.ObjectModel.ObservableCollection<S> ListOfObjectS { get; private set; }
public R()
{
// Use ObservableCollection instead.
ListOfObjectS = new ObservableCollection<S>();
// Subscribe to all changes to the collection.
ListOfObjectS.CollectionChanged += listOfObjectS_CollectionChanged;
}
void listOfObjectS_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Remove)
{
// When items are removed, unsubscribe from property change notifications.
var oldItems = (e.OldItems ?? new INotifyPropertyChanged[0]).OfType<INotifyPropertyChanged>();
foreach (var item in oldItems)
item.PropertyChanged -= item_PropertyChanged;
}
// When item(s) are added, subscribe to property notifications.
if (e.Action == NotifyCollectionChangedAction.Add)
{
var newItems = (e.NewItems ?? new INotifyPropertyChanged[0]).OfType<INotifyPropertyChanged>();
foreach (var item in newItems)
item.PropertyChanged += item_PropertyChanged;
}
// NOTE: I'm not handling NotifyCollectionChangedAction.Reset.
// You'll want to look into when this event is raised and handle it
// in a special fashion.
}
void item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName.StartsWith("ObjectA."))
{
// Refresh any dependent views, forms, controls, whatever...
}
}
}

Let's say you have a form1 where you use an instance of class R to display a list of instances from class A. You than press edit and you send the instance of that same class A from the class R instance towards the new form.
This will than be a reference to the object contained in the instance of R and therefore be updated within form2. The only thing you than have to do is refresh the instance of class A in the list of form1.
To explain: when you are calling a form or method with the an object instance of a class, this will create a reference, not a clone and therefore can be updated from the second form2.

Related

Manipulating collection from the inside

I have a collection of panels which are highlighted when user clicks on them. I want to force them to behave as a set of radio buttons so only the one that is clicked on is highlighted and others aren't.
I guess that there must be a way to manipulate whole collection (set property to false) from the inside, because the event is triggered by one item from the collection. Is there a way for the one item to manipulate whole collection? This is such a common feature in applications so I guess there must be a pattern how to do it properly. Thanks.
You may store collection of your panels and handle required functionality as in following code snippet:
List<Panel> Panels;
private void Initialization()
{
Panels = new List<Panel>();
Panels.Add(pnl1);
Panels.Add(pnl2);
//add all your panels into collection
foreach(Panel Item in this.Panels)
{
//add handle to panel on click event
Item.Click += OnPanelClick;
}
}
private void OnPanelClick(object sender, EventArgs e)
{
foreach(Panel Item in this.Panels)
{
//remove highlight from your panels, real property should have other name than Panel.HighlightEnabled
Item.HighlightEnabled = false;
}
((Panel)sender).HighlightEnabled = true; //add highlight to Panel which invoked Click event
Application.DoEvents(); //ensure that graphics redraw is completed immediately
}
private void AddNewPanelIntoLocalCollection(Panel panel)
{
//here you can add new items to collection during program lifecycle
panel.Click += OnPanelClick;
this.Panels.Add(panel);
}
This is how I do it
public class SelectOne : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected void NotifyPropertyChanged(String info)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(info));
}
}
private bool isSelected = false;
private HashSet<SelectOne> selecteOnes = null;
public bool IsSelected
{
get { return isSelected; }
set
{
if (isSelected == value) return;
if (isSelected && selecteOnes != null)
{
foreach (SelectOne so in selecteOnes)
{
if (so == this) continue;
so.IsSelected = false;
}
}
NotifyPropertyChanged("IsSelected");
}
}
public SelectOne() { }
public SelectOne(bool IsSelected) { isSelected = IsSelected; }
public SelectedOne(bool IsSelected, HashSet<SelectOne> SelecteOnes)
{
isSelected = IsSelected;
selecteOnes = SelecteOnes;
}
}
Eventually I did find a way to do this properly with only one delegate.
In class A I have a collection of objects B
List<B> b = new List<B>
class B, needs to have an unique ID and delegete for void metod with Id parameter
delegate void DeleteItemDelegate(int id);
class B
{
public int ID {get; set;}
public DeleteItemDeleate deleteThis {set; get;}
}
class A has a metod like this:
public void RemoveItem(int id)
{
for (int x = 0; x < b.Count; x++)
{
if (b[x].id == id)
{
b.RemoveAt(x);
}
}
}
when adding a new B object into List just add metod RemoveItem to B.deleteThis delegate
B bObject = new B();
bObject.deleteThis = RemoveItem;
b.Add(bObject);
Now all you need to do is add DeleteMe metod in B class
void DeleteMe()
{
// and call local delegate - pointing to metod which actually can manipulate the collection
deleteThis(id);
}

C# Nested Class

I have a class Animal. Within the class Animal I have a list of another class called Dogs.
class Animal
{
List<Dog> Dogs;
int CountNeutedDogs;
class Dog
{
// some properties
boolean neuted;
}
}
It's possible that the list can contain about 500 dogs at any one time. I want to have an event so that if the value of neuted property in a dog changes the CountNeutedDogs is informed and updated. How do I do this?
Edit
I should explain the list Dogs is bound to a datagrid. The user can change the neuted property from false to true and vice versa. I have a get & set for the property. So the neuted value can change and I need to update the CountNeutedDogs value which is why I was thinking of using an event but not sure how to do it.
If you make CountNeutedDogs a property like so, it will always be correct without events updating:
int CountNeutedDogs
{
get
{
return this.Dogs.Where(d => d.neuted).Count();
}
}
This is a follow-up on the Comments:
I made an example, (not tested!!) how you can solve this by implementing the ObservableCollection and INotifyPropertyChanged
public class Dog : INotifyPropertyChanged
{
private bool _isNeutered;
public bool IsNeutered
{
get { return _isNeutered; }
set
{
if (_isNeutered != value)
{
_isNeutered = value;
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("IsNeutered"));
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
public class Animals : INotifyPropertyChanged
{
private ObservableCollection<Dog> _dogs = new ObservableCollection<Dog>();
public Animals()
{
_dogs.CollectionChanged += Dogs_CollectionChanged;
}
// NOTE, I haven't checked this!! But I think it should be something like this:
private void Dogs_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
foreach (INotifyPropertyChanged item in e.NewItems.OfType<INotifyPropertyChanged>())
item.PropertyChanged += Item_PropertyChanged;
break;
case NotifyCollectionChangedAction.Reset:
case NotifyCollectionChangedAction.Remove:
foreach (INotifyPropertyChanged item in e.OldItems.OfType<INotifyPropertyChanged>())
item.PropertyChanged -= Item_PropertyChanged;
break;
}
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("NeuteredDogsCount"));
}
private void Item_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs("NeuteredDogsCount"));
}
public IEnumerable<Dog> Dogs
{
get { return _dogs; }
}
public int NeuteredDogsCount
{
get
{
return Dogs.Where(dog => dog.IsNeutered).Count();
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
So when the collection changes, the notify of the NeuteredDogsCount property is sent. This way the bounded controls will re-evaluate the NeuteredDogsCount property.
Instead of 'normal' properties, the IsNeutered can be changed to a DependencyProperty.

WPF / C# Updating property change of an item in an ObservableCollection to the ListBox

I have a list box:
<ListBox x:Name="lbxAF" temsSource="{Binding}">
that gets its data from this from this modified Observable Collection:
public ObservableCollectionEx<FileItem> folder = new ObservableCollectionEx<FileItem>();
which is created within a class that uses FileSystemWatcher to monitor a specific folder for addition, deletion and modification of files.
The ObservableCollection was modified (hence the Ex at the end) so that I can modify it from an outside thread (code is not mine, I actually did some searching through this website and found it, works like a charm):
// This is an ObservableCollection extension
public class ObservableCollectionEx<T> : ObservableCollection<T>
{
// Override the vent so this class can access it
public override event System.Collections.Specialized.NotifyCollectionChangedEventHandler CollectionChanged;
protected override void OnCollectionChanged(System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
using (BlockReentrancy())
{
System.Collections.Specialized.NotifyCollectionChangedEventHandler eventHanlder = CollectionChanged;
if (eventHanlder == null)
return;
Delegate[] delegates = eventHanlder.GetInvocationList();
// Go through the invocation list
foreach (System.Collections.Specialized.NotifyCollectionChangedEventHandler handler in delegates)
{
DispatcherObject dispatcherObject = handler.Target as DispatcherObject;
// If the subscriber is a DispatcherObject and different thread do this:
if (dispatcherObject != null && dispatcherObject.CheckAccess() == false)
{
// Invoke handler in the target dispatcher's thread
dispatcherObject.Dispatcher.Invoke(DispatcherPriority.DataBind, handler, this, e);
}
// Else, execute handler as is
else
{
handler(this, e);
}
}
}
}
}
The collection is made up of these:
public class FileItem
{
public string Name { get; set; }
public string Path { get; set; }
}
which allow me to store names and paths of files.
Everything works great as far as deletion and addition of files, and the List Box gets updated flawlessly with respect to those two... however, if I change the name of any of the files, it doesn't update the list box.
How would I notify list box of the changes in FileItem's properties? I assumed that ObservableCollection would handle that, but apparently it raises flag only when FileItem is added or deleted, not when its contents are changed.
Your FileItem class should implement INotifyPropertyChanged. Below is a simple working implementation of it.
public class FileItem : INotifyPropertyChanged
{
private string _Name;
public string Name
{
get { return _Name; }
set {
if (_Name != value)
{
_Name = value;
OnPropertyChanged("Name");
}
}
}
private string _Path;
public string Path
{
get { return _Path; }
set {
if (_Path != value)
{
_Path = value;
OnPropertyChanged("Path");
}
}
}
public event PropertyChangedEventHandler PropertyChanged;
public void OnPropertyChanged(String propertyName)
{
if (PropertyChanged != null)
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
That's how the ObservableCollection works - it monitors only insertion/deletion/moving of items.
To update the View when each item (or FileItem, in your case) changes, the FileItem must implement INotifyPropertyChanged and fire the appropriate event when you set each property that you want to observe.
Here's an example of how to do this: http://msdn.microsoft.com/en-us/library/system.componentmodel.inotifypropertychanged.aspx
Try this simple one:
public class NotifyObservableCollection<TItem> : ObservableCollection<TItem>
where TItem : class , INotifyPropertyChanged, new()
{
#region Fields
private Action _itemPropertyChanged;
#endregion
#region Constructor
public NotifyObservableCollection(Action itemPropertyChanged)
{
_itemPropertyChanged = itemPropertyChanged;
}
#endregion
#region Methods
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
if (e.Action == NotifyCollectionChangedAction.Add)
{
foreach (var item in e.NewItems)
{
var notifyItem = item as INotifyPropertyChanged;
if (notifyItem != null)
{
notifyItem.PropertyChanged += ItemPropertyChanged;
}
}
}
else if (e.Action == NotifyCollectionChangedAction.Remove)
{
foreach (var item in e.OldItems)
{
var notifyItem = item as INotifyPropertyChanged;
if (notifyItem != null)
{
notifyItem.PropertyChanged -= ItemPropertyChanged;
}
}
}
base.OnCollectionChanged(e);
}
#endregion
#region Private Methods
private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if(_itemPropertyChanged!=null)
{
_itemPropertyChanged();
}
}
#endregion
}

call converter in XAML if property of a object in the observable collection changes

I have a converter that accepts an ObservableCollection as a parameter, and I'd like to re-evaluate it whenever a specific property on any item in the collection changes
For example: lets say I have bound a label to a collection of Person objects with a converter. The job of the converter is to count the number of Persons in the list that are female, and return "valid" for 1 female or "accepted" for 2. I'd like the converter to get called again anytime the Gender property on any Person object gets changed.
How can I accomplish this?
That's a classic problem you end up having if you play around WPF long enough.
I've tried various solutions, but the one that works best is to use a BindingList like so:
public class WorldViewModel : INotifyPropertyChanged
{
private BindingList<Person> m_People;
public BindingList<Person> People
{
get { return m_People; }
set
{
if(value != m_People)
{
m_People = value;
if(m_People != null)
{
m_People.ListChanged += delegate(object sender, ListChangedEventArgs args)
{
OnPeopleListChanged(this);
};
}
RaisePropertyChanged("People");
}
}
}
private static void OnPeopleListChanged(WorldViewModel vm)
{
vm.RaisePropertyChanged("People");
}
public event PropertyChangedEventHandler PropertyChanged;
void RaisePropertyChanged(String prop)
{
PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(prop));
}
}
}
Then just bind to the People collection like you would do with an ObservableCollection, except bindings will be re-evaluated when any property in its items change.
Also, please note that OnPeopleListChanged is static, so no memory leaks.
And Person should implement INotifyPropertyChanged.
A CollectionChanged event is only thrown when an item is added or removed from the collection (not when an item in the collection is changed). So the converter is not called when an item is changed.
One option:
In the Gender Property Set include logic to evaluate the collection and set a string Property that you bind the label to.
Wrote generic version of the answer from Baboon
public class ObservalbeList<T>: INotifyPropertyChanged
{
private BindingList<T> ts = new BindingList<T>();
public event PropertyChangedEventHandler PropertyChanged;
// This method is called by the Set accessor of each property.
// The CallerMemberName attribute that is applied to the optional propertyName
// parameter causes the property name of the caller to be substituted as an argument.
private void NotifyPropertyChanged( String propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public BindingList<T> Ts
{
get { return ts; }
set
{
if (value != ts)
{
Ts = value;
if (Ts != null)
{
ts.ListChanged += delegate(object sender, ListChangedEventArgs args)
{
OnListChanged(this);
};
}
NotifyPropertyChanged("Ts");
}
}
}
private static void OnListChanged(ObservalbeList<T> vm)
{
vm.NotifyPropertyChanged("Ts");
}
public ObservalbeList()
{
ts.ListChanged += delegate(object sender, ListChangedEventArgs args)
{
OnListChanged(this);
};
}
}

Mvvm nested propertychanged in model

I use model class A that contains a list of model class B. Each of the models (A and B) implement the INotifyPropertyChanged and the events are fired correctly when a propertey changes. Model A has subscribed to this event from models B and if one occurs model A fires another property changed.
My problem is that I want to listen to changes within model A (including nestend model properties) in a service class which should write the changes to a database. But when model A fires propertychanged, in my service class I cannot know if the property that has changed belongs to model A or to one of the models B.
public class Model B : ObservableObject
{
...
public int Property1
{
get{return property1;}
set{ this.property1 = value;
OnPropertyChanged("Property1");
}
}
}
public class ModelA : ObservableObject
{
...
ObservableCollection<ModelB> modelsB;
...
public ObservableCollection<ModelB> ModelsB
{
get
{
if(modelsB == null)
{
this.modelsB = new ObservableCollection<ModelB>();
this.modelsB.CollectionChanged+= ModelBListChanged;
}
return modelsB;
}
}
private int property2;
public int Property2
{
get{return property2;}
set{ this.property2 = value;
OnPropertyChanged("Property2");
}
}
private void ModelBListChanged(object sender, NotifyCollectionChangedEventArgs args)
{
if(e.NewItems != null)
{
foreach(ObservableObject item in e.NewItems)
{
item.PropertyChanged += NotifyPropertyChanged;
}
}
if (e.OldItems != null)
{
foreach (ObservableObject item in e.OldItems)
{
item.PropertyChanged -= NotifyPropertyChanged;
}
}
}
...
}
public class SomeService
{
...
ObservableCollection<ModelA> modelsA;
public ObservableCollection<ModelA> ModelsA
{
get
{
if(modelsA == null)
{
modelsA = new ObservableCollection<ModelA>();
//fill with data...
....
foreach(...)
{
ModelB mb = new ModelB();
//fill mb ...
...
mb.PropertyChanged += ModelBChanged;
modelsA.ModelsB.Add(mb);
}
....
modelsA.PropertyChanged += ModelsAChanged;
}
return modelsA;
}
}
...
private void ModelsAChanged(object sender, PropertyChangedEventArgs args)
{
// determine the property that has changed and call the correct
// update function for this property to write the data to database.
var ma = (ModelA) sender;
switch(args.PropertyName)
{
...
case ModelA.Property1:
//update database with function for property 1
break;
case ModelA.Property2:
//update database with function for property 2
break;
...
}
}
private void ModelBChanged(object sender, PropertyChangedEventArgs args)
{
// How do I know to which ModelA this ModelB sender belongs?
var mb = (ModelB) sender;
switch(args.PropertyName)
{
...
case ModelB.Property1:
//update database with function for property 1 of model B
break;
...
}
}
}
How could this be solved?
Regards,
tabina
I'm not sure your current code even compiles? You are turning a CollectionChanged event from your B collection into a PropertyChanged event on your A class, but you are referencing PropertyName which doesn't exist in a NotifyCollectionChangedEventArgs.
Even if you could get that to work, it makes little sense since CollectionChanged events can reference multiple items, whereas PropertyChanged only ever references one.
Your B collection is already a public property of A. Why can't you subscribe directly to the changes in your service? eg.
public class SomeService
{
ObservableCollection<ModelA> modelsA;
public ObservableCollection<ModelA> ModelsA
{
get
{
if(modelsA == null)
{
modelsA = new ObservableCollection<ModelA>();
//fill with data...
modelsA.CollectionChanged += ModelsAChanged;
foreach (ModelA A in modelsA)
A.ModelsB.CollectionChanged += ModelsBChanged;
}
return modelsA;
}
}
private void ModelsAChanged(object sender, NotifyCollectionChangedEventArgs args)
{
//remember to subscribe to ModelsB in any new ModelA instance that get added
//to the modelsA collection too, and unsubscribe when they get removed.
}
private void ModelsBChanged(object sender, NotifyCollectionChangedEventArgs args)
{
}
}

Categories