C# Add event to value inside Dictionary<TKey, TValue> - c#

I'm trying to add an event to the value of a dictionary. I made my own ObservableDictionary with an event on the Add button, but now I'm trying to do the same for the value. The dictionary looks like Dictionary<string, ObservableCollection<string>>.
The calling function:
public void AddExtension(string key, string value)
{
if (_settings.ExtensionsPerFolder.ContainsKey(key))
{
_settings.ExtensionsPerFolder[key].Add(value);
var values = _settings.ExtensionsPerFolder[key];
values.Add(value);
_settings.ExtensionsPerFolder[key] = values;
}
else
_settings.ExtensionsPerFolder.Add(key, new ObservableCollection<string> { value });
}
My own dictionary implementation:
public class ObservableDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
public event EventHandler CollectionChanged;
public new TValue this[TKey key]
{
get => base[key];
set
{
base[key] = value;
OnCollectionChanged();
}
}
public new void Add(TKey key, TValue value)
{
base.Add(key, value);
OnCollectionChanged();
}
protected void OnCollectionChanged() => CollectionChanged?.Invoke(this, EventArgs.Empty);
}
The _settings.ExtensionsPerFolder[key].Add(value) doesn't trigger the OnCollectionChanged event, but the _settings.ExtensionsPerFolder[key] = values does.
[EDIT] The file in which the Dictionary is initialized:
public class Settings
{
[JsonIgnore]
public string SettingsPath { get; } = "Settings.json";
public ObservableCollection<string> Directories { get; set; } = new ObservableCollection<string>();
public ObservableDictionary<string, ObservableCollection<string>> ExtensionsPerFolder { get; set; } = new ObservableDictionary<string, ObservableCollection<string>>();
public Settings() { }
public void CollectionChanged()
{
Directories.CollectionChanged += CollectionChanged;
ExtensionsPerFolder.CollectionChanged += CollectionChanged;
}
private void CollectionChanged(object sender, EventArgs e)
{
try
{
using StreamWriter writer = new StreamWriter(SettingsPath);
writer.WriteLine(this.ToJson());
}
catch (Exception ex)
{
Logger log = new Logger();
log.Log($"Unable to save: {ex.ToString()}");
}
}
}
Is there any way that I can achieve this power of event control, or do I have to fiddle with the Add functionality within my own Dictionary for it to work.
Thanks in advance.

You have a collection of collections and you've only ever subscribed to observe the outer collection. You've subscribed to ExtensionsPerFolder.CollectionChanged which will tell you when you do something like ExtensionsPerFolder[key] = values, but you haven't subscribed to each inner collection. In this case values should be an ObservableCollection<string>.
Each time you create a new ObservableCollection<string>, you'll want to subscribe to it.
Here's where you created a new one:
_settings.ExtensionsPerFolder.Add(key, new ObservableCollection<string> { value });
You'll also want to add a CollectionChanged handler for this new collection at this point in your code. That way a statement like _settings.ExtensionsPerFolder[key].Add(value) will trigger a CollectionChanged event on that particular collection.
I'll leave it to you to decide what you want that event handler to do.

Related

List<> propertychanged event

I am working on a project in which I need to notify my GUI that my list has new items.
I tried this with an observablecollection, but I use timers, and when I try to add or remove items from the observablecollection, then an exception is thrown that the collection is being marshalled from another thread.
Therefore, I started thinking about using the PropertyChanged event. However, I have trouble with getting it to work. What I understand is that it is used for properties. I have tried the following code without success:
public class MyCollection : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
public ConcurrentQueue<PC_Info_Item> data;
public List<string> table;
public MyCollection()
{
data = new ConcurrentQueue<PC_Info_Item>();
table = new List<string>();
}
public void Add(PC_Info_Item item)
{
data.Enqueue(item);
OnPropertyChanged(nameof(table));
}
public void Add(string item)
{
table.Add(item);
OnPropertyChanged(nameof(table));
}
public void delete()
{
data.TryDequeue(out PC_Info_Item item);
OnPropertyChanged(nameof(table));
}
public void delete_string(string item)
{
table.Remove(item);
OnPropertyChanged(nameof(table));
}
protected void OnPropertyChanged(string name = "")
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
Is there a way to make this work for lists?
you can wrap the calls into lock, that will make other threads wait till the call is finished.
private readonly object CollectionLock = new object();
public void Add(PC_Info_Item item)
{
lock(CollectionLock) {
data.Enqueue(item);
}
}
public void Add(string item)
{
lock(CollectionLock) {
table.Add(item);
}
}
public void delete()
{
lock(CollectionLock) {
data.TryDequeue(out PC_Info_Item item);
}
}
public void delete_string(string item)
{
lock(CollectionLock) {
table.Remove(item);
}
}
I think you should call on property changed as below where we should get the invocation list and call invoke on each receiver with lock.
private object _lock = new object();
protected void OnPropertyChanged(string name = "")
{
var receivers = this.PropertyChanged.GetInvocationList();
foreach (EventHandler<PropertyChangedEventArgs> receiver in receivers)
{
lock (this._lock)
{
receiver?.Invoke(this, new PropertyChangedEventArgs(name));
}
}
}

MVVM Expose List from Model to ViewModel and View

I have a model which currently looks through a series of different log files and then makes an object for each item in those files and appends them to a list (ListOfLogs). Once the model is done parsing the log files it does a property changed event to notify the VM that the ListOfLogs is ready.
The Viewmodel then handles the property changed event and creates an ObservableCollection from the model's ListOfLogs. The view then binds to that observablecollection.
Now that I have switched from an ObservableCollection to a ICollectionView I get an invalid operation exception since the calling thread doesn't own ListOfLogs object. This makes me thing that the way I expose the List is not following the MVVM pattern
Added Code:
ViewModel.cs:
public class ViewModel : INotifyPropertyChanged {
#region Fields
#endregion // Fields
#region Properties
public Model myModel { get; private set; }
public ObservableCollection<MyObject> collectionView { get; set; }
#endregion // Properties
#region Constructor
public ViewModel() {
myModel = new Model();
myModel.PropertyChanged += propertyChanged;
}
public event PropertyChangedEventHandler PropertyChanged;
#endregion // Constructor
#region Methods
private void propertyChanged(object sender, PropertyChangedEventArgs e) {
switch (e.PropertyName ) {
case "Objects":
// Is there a better way to do this
collectionView = new ObservableCollection<MyObject>(myModel.Objects);
//
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("collectionView"));
break;
default:
Console.WriteLine(string.Format("No case for {0}, ", e.PropertyName));
break;
}
}
Model.cs:
Edit: fixed mistake when invoking the property changed event
namespace TestApp1 {
public class Model : INotifyPropertyChanged {
#region Fields
private IList<MyObject> _Objects;
public event PropertyChangedEventHandler PropertyChanged;
#endregion // Fields
#region Properties
public IList<MyObject> Objects { get => _Objects ?? (_Objects = new List<MyObject>()); private set { if (Objects != value) _Objects = value; } }
#endregion // Properties
#region Constructor
public Model() {
}
#endregion // Constructor
#region Methods
public void LoadObjects() {
// Parse through files normally for now just junk works
Parallel.For(0, 10000, dostuff => {
var myOb = new MyObject(){ dt = DateTime.Now, message = "Message" };
lock (Objects) {
Objects.Add(myOb);
}
});
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Objects"));
}
#endregion // Methods
}
public class MyObject {
public DateTime dt { get; set; }
public string message { get; set; }
public string stuff1 { get; set; }
public string stuff2 { get; set; }
}
}
The problem is, that you are modifying the Objects list while passing it to the constructor of the observable collection. (https://referencesource.microsoft.com/#system/compmod/system/collections/objectmodel/observablecollection.cs,cfaa9abd8b214ecb in the constructor where "copyfrom")
The InvalidOperationException belongs to your Objects.Add() call in the Parallel.For.
private void CopyFrom(IEnumerable<T> collection)
{
IList<T> items = Items;
if (collection != null && items != null)
{
using (IEnumerator<T> enumerator = collection.GetEnumerator())
{
while (enumerator.MoveNext())
{
items.Add(enumerator.Current);
}
}
}
}
In the delegate of Parallel.For you are using a lock. You could use this as well for the property changed event:
lock(myModel.Objects)
{
collectionView = new ObservableCollection<MyObject>(myModel.Objects);
}
Or add the event raising to the lock in the Parallel.For delegate
lock (Objects)
{
Objects.Add(myOb);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs("Objects"));
}
Or you could just wait until all items are read and then raise one property changed event after completing the Parallel.For.

call function when a list gets modified? [duplicate]

I created a Class EventList inheriting List which fires an Event each time something is Added, Inserted or Removed:
public class EventList<T> : List<T>
{
public event ListChangedEventDelegate ListChanged;
public delegate void ListChangedEventDelegate();
public new void Add(T item)
{
base.Add(item);
if (ListChanged != null
&& ListChanged.GetInvocationList().Any())
{
ListChanged();
}
}
...
}
At the Moment I use it as a Property like this:
public EventList List
{
get { return m_List; }
set
{
m_List.ListChanged -= List_ListChanged;
m_List = value;
m_List.ListChanged += List_ListChanged;
List_ListChanged();
}
}
Now my Problem is, can I somehow handle if a new Object is referred to it or prevent that, so I do not have to do the event wiring stuff in the setter?
Of course, I can change the property to "private set" but I would like to be able to use the class as variable as well.
You seldom create a new instance of a collection class in a class. Instantiate it once and clear it instead of creating a new list. (and use the ObservableCollection since it already has the INotifyCollectionChanged interface inherited)
private readonly ObservableCollection<T> list;
public ctor() {
list = new ObservableCollection<T>();
list.CollectionChanged += listChanged;
}
public ObservableCollection<T> List { get { return list; } }
public void Clear() { list.Clear(); }
private void listChanged(object sender, NotifyCollectionChangedEventArgs args) {
// list changed
}
This way you only have to hook up events once, and can "reset it" by calling the clear method instead of checking for null or equality to the former list in the set accessor for the property.
With the changes in C#6 you can assign a get property from a constructor without the backing field (the backing field is implicit)
So the code above can be simplified to
public ctor() {
List = new ObservableCollection<T>();
List.CollectionChanged += OnListChanged;
}
public ObservableCollection<T> List { get; }
public void Clear()
{
List.Clear();
}
private void OnListChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// react to list changed
}
ObservableCollection is a List with a CollectionChanged event
ObservableCollection.CollectionChanged Event
For how to wire up the event handler see answer from Patrick. +1
Not sure what you are looking for but I use this for a collection with one event that fires on add, remove, and change.
public class ObservableCollection<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(ObservableCollection<T> vm)
{
// this will fire on add, remove, and change
// if want to prevent an insert this in not the right spot for that
// the OPs use of word prevent is not clear
// -1 don't be a hater
vm.NotifyPropertyChanged("Ts");
}
public ObservableCollection()
{
ts.ListChanged += delegate(object sender, ListChangedEventArgs args)
{
OnListChanged(this);
};
}
}
If you do not want to or can not convert to an Observable Collection, try this:
public class EventList<T> : IList<T> /* NOTE: Changed your List<T> to IList<T> */
{
private List<T> list; // initialize this in your constructor.
public event ListChangedEventDelegate ListChanged;
public delegate void ListChangedEventDelegate();
private void notify()
{
if (ListChanged != null
&& ListChanged.GetInvocationList().Any())
{
ListChanged();
}
}
public new void Add(T item)
{
list.Add(item);
notify();
}
public List<T> Items {
get { return list; }
set {
list = value;
notify();
}
}
...
}
Now, for your property, you should be able to reduce your code to this:
public EventList List
{
get { return m_List.Items; }
set
{
//m_List.ListChanged -= List_ListChanged;
m_List.Items = value;
//m_List.ListChanged += List_ListChanged;
//List_ListChanged();
}
}
Why? Setting anything in the EventList.Items will call your private notify() routine.
I have a Solution for when someone calls the Generic method from IList.add(object). So that you also get notified.
using System;
using System.Collections;
using System.Collections.Generic;
namespace YourNamespace
{
public class ObjectDoesNotMatchTargetBaseTypeException : Exception
{
public ObjectDoesNotMatchTargetBaseTypeException(Type targetType, object actualObject)
: base(string.Format("Expected base type ({0}) does not match actual objects type ({1}).",
targetType, actualObject.GetType()))
{
}
}
/// <summary>
/// Allows you to react, when items were added or removed to a generic List.
/// </summary>
public abstract class NoisyList<TItemType> : List<TItemType>, IList
{
#region Public Methods
/******************************************/
int IList.Add(object item)
{
CheckTargetType(item);
Add((TItemType)item);
return Count - 1;
}
void IList.Remove(object item)
{
CheckTargetType(item);
Remove((TItemType)item);
}
public new void Add(TItemType item)
{
base.Add(item);
OnItemAdded(item);
}
public new bool Remove(TItemType item)
{
var result = base.Remove(item);
OnItemRemoved(item);
return result;
}
#endregion
# region Private Methods
/******************************************/
private static void CheckTargetType(object item)
{
var targetType = typeof(TItemType);
if (item.GetType().IsSubclassOf(targetType))
throw new ObjectDoesNotMatchTargetBaseTypeException(targetType, item);
}
#endregion
#region Abstract Methods
/******************************************/
protected abstract void OnItemAdded(TItemType addedItem);
protected abstract void OnItemRemoved(TItemType removedItem);
#endregion
}
}
If an ObservableCollection is not the solution for you, you can try that:
A) Implement a custom EventArgs that will contain the new Count attribute when an event will be fired.
public class ChangeListCountEventArgs : EventArgs
{
public int NewCount
{
get;
set;
}
public ChangeListCountEventArgs(int newCount)
{
NewCount = newCount;
}
}
B) Implement a custom List that inherits from List and redefine the Count attribute and the constructors according to your needs:
public class CustomList<T> : List<T>
{
public event EventHandler<ChangeListCountEventArgs> ListCountChanged;
public new int Count
{
get
{
ListCountChanged?.Invoke(this, new ChangeListCountEventArgs(base.Count));
return base.Count;
}
}
public CustomList()
{ }
public CustomList(List<T> list) : base(list)
{ }
public CustomList(CustomList<T> list) : base(list)
{ }
}
C) Finally subscribe to your event:
var myList = new CustomList<YourObject>();
myList.ListCountChanged += (obj, e) =>
{
// get the count thanks to e.NewCount
};

WPF/C# entirely programmatically binding an array of objects to a static ObservableCollection

Please assume this entire question deals in code, without any XAML.
I have a static ObservableCollection named myStaticList. It's a part of a non-static class named myClass.
public class myClass
{
public static ObservableCollection<CheckBoxStructure> myStaticList { get; set; }
static myClass()
{
myStaticList = new ObservableCollection<CheckBoxStructure>();
}
}
And the definition of CheckBoxStructure:
public class CheckBoxStructure
{
public string Description { get; set; }
public bool IsSelected { get; set; }
}
In addition, there's an array of checkboxes called checkBoxArray[], holding 3 elements. each checkbox has as content a textbox.
What I want to do is programmatically bind (two-way) these two, in such a manner that the IsChecked property of the checkboxes in the checkBoxArray[] array will bind to the IsSelected property of the myStaticList's CheckBoxStructure, and similarly so between the text of the textboxes inthe checkboxes' content and the Description property of the myStaticList's CheckBoxStructure.
In addition, I would like to avoid using loops, since it is preferable that this two lists will update each other if they change in size.
How is this possible?
Thanks!
Using XAML, an easy way would be to the declare an ItemsControl and a DataTemplate for it so that you can have a UserControl (CheckBox and TextBox inside) with its DataContext being a CheckBoxStructure. This way the bindings work between CheckBox.IsChecked and IsSelected property and between TextBox.Text and Description property.
If you need to this only in code then you would have to create same behavior (ItemsControl with a DataTemplate). You have at least 2 options
1.
DataTemplate template = new DataTemplate();
FrameworkElementFactory factory = new FrameworkElementFactory(typeof(StackPanel));
template.VisualTree = factory;
FrameworkElementFactory childFactory = new FrameworkElementFactory(typeof(CheckBox));
childFactory.SetBinding(CheckBox.IsChecked, new Binding("IsSelected"));
factory.AppendChild(childFactory);
childFactory = new FrameworkElementFactory(typeof(TextBox));
childFactory.SetBinding(Label.ContentProperty, new Binding("Description"));
factory.AppendChild(childFactory);
2.
MemoryStream sr = null;
ParserContext pc = null;
string xaml = string.Empty;
xaml = "<DataTemplate><StackPanel><TextBlock Text="{Binding Description"/><CheckBox IsChecked="{Binding IsSelected"/></StackPanel></DataTemplate>";
sr = new MemoryStream(Encoding.ASCII.GetBytes(xaml));
pc = new ParserContext();
pc.XmlnsDictionary.Add("", "http://schemas.microsoft.com/winfx/2006/xaml/presentation");
pc.XmlnsDictionary.Add("x", "http://schemas.microsoft.com/winfx/2006/xaml");
DataTemplate datatemplate = (DataTemplate)XamlReader.Load(sr, pc);
this.Resources.Add("dt", datatemplate);
Later edit, after discussion from comments; this example works only one way of binding but is easily to make it two ways. Please note that this is only a trivial example of a concept and is not complete: you need to modify the list classes to suit how you wish for objects to be paired, you may need to add more guards for corner cases, you may need to make it thread safe and so on...
First the basic binding objects:
class Binder
{
public Binder()
{
_bindings = new Dictionary<string, List<string>>();
}
private INotifyPropertyChanged _dataContext;
public INotifyPropertyChanged DataContext
{
get { return _dataContext; }
set
{
if (_dataContext != null)
{
_dataContext.PropertyChanged -= _dataContext_PropertyChanged;
}
_dataContext = value;
_dataContext.PropertyChanged += _dataContext_PropertyChanged;
}
}
void _dataContext_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (_bindings.ContainsKey(e.PropertyName))
{
var bindableType = _dataContext.GetType();
var bindableProp = bindableType.GetProperty(e.PropertyName);
if (bindableProp == null)
{
return;
}
var binderType = this.GetType();
foreach (var binderPropName in _bindings[e.PropertyName])
{
var binderProp = binderType.GetProperty(binderPropName);
if (binderProp == null)
{
continue;
}
var value = bindableProp.GetValue(_dataContext);
binderProp.SetValue(this, value);
}
}
}
Dictionary<string, List<string>> _bindings;
public void AddBinding(string binderPropertyName, string bindablePropertyName)
{
if (!_bindings.ContainsKey(bindablePropertyName))
{
_bindings.Add(bindablePropertyName, new List<string>());
}
_bindings[bindablePropertyName].Add(bindablePropertyName);
}
}
class Bindable : INotifyPropertyChanged
{
protected void NotifyPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public event PropertyChangedEventHandler PropertyChanged;
}
Then the holding lists for them:
class BindableList<T> : List<T> where T : Bindable
{
public event Action<T> ItemAdded;
public new void Add(T item)
{
base.Add(item);
NotifyItemAdded(item);
}
private void NotifyItemAdded(T item)
{
if (ItemAdded != null)
{
ItemAdded(item);
}
}
}
class BinderList<T> : List<T> where T : Binder
{
public BinderList()
{
_bindingRules = new Dictionary<string, string>();
}
private BindableList<Bindable> _dataContextList;
public BindableList<Bindable> DataContextList
{
get { return _dataContextList; }
set
{
if (_dataContextList != null)
{
_dataContextList.ItemAdded -= _dataContextList_ItemAdded;
}
_dataContextList = value;
_dataContextList.ItemAdded += _dataContextList_ItemAdded;
}
}
void _dataContextList_ItemAdded(Bindable obj)
{
foreach (var pair in _bindingRules)
{
this[Count-1].AddBinding(pair.Key, pair.Value);
this[Count - 1].DataContext = obj;
}
}
private Dictionary<string, string> _bindingRules;
public void AddBindingRule(string binderPropertyName, string bindablePropertyName)
{
_bindingRules.Add(binderPropertyName, bindablePropertyName);
}
}
Now the actual classes with properties:
class BinderElement : Binder
{
private string _description;
public string Description
{
get { return _description; }
set { _description = value; }
}
}
class BindableElement : Bindable
{
private string _description;
public string Description
{
get
{
return _description;
}
set
{
_description = value;
NotifyPropertyChanged("Description");
}
}
}
And an example to use them:
static void Main(string[] args)
{
var bindableList = new BindableList<Bindable>();
var binderList = new BinderList<BinderElement>()
{
new BinderElement(),
new BinderElement()
};
binderList.DataContextList = bindableList;
binderList.AddBindingRule("Description", "Description");
bindableList.Add(new BindableElement());
bindableList.Add(new BindableElement());
((BindableElement)bindableList[1]).Description = "This should arrive in BinderElement Description property";
Console.WriteLine(binderList[1].Description);
Console.ReadLine();
}

C# - How to make use of "INotifyPropertyChanged" to update calculated fields in a collection class?

I have an Item class with two properties "quantity" and "price" and implements INotifyPropertyChanged
public class Item:INotifyPropertyChanged
{
private event PropertyChangedEventHandler _propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
{
add { _propertyChanged += value; }
remove { _propertyChanged -= value; }
}
void NotifyPropertyChanged([CallerMemberName] String propertyName = "")
{
if (_propertyChanged != null)
{
_propertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
public int QuantityOnHand
{
get
{
return this._quantityOnHand;
}
set
{
if (value > 0)
{
this._quantityOnHand = value;
NotifyPropertyChanged();
}
else
{
throw new System.ArgumentException("Quantity must be a positive value!");
}
}
}
.....
}
And I have a collection class of items named "Inventory" with a property of TotalRetailPrice:
public class Inventory {
private List<Item> _inventoryList = new LinkedList<Item>();
public decimal TotalRetailPrice
{
get
{
decimal totalRetailPrice = 0M;
foreach (var item in _inventoryList)
{
totalRetailPrice += item.QuantityOnHand * item.RetailPrice;
}
return totalRetailPrice;
}
}
I am trying to find out a way to automatically update this property TotalRetailPrice, whenever I change either the quantity or the price of any item(s) in the list. How can I do that? Right now with my code, every time I tried to get this totalRetailPrice property, I will have to go through the list and recalculate it.
thanks!
Since the interface INotifyPropertyChanged exposes an event called PropertyChanged you can just subscribe to that in the 'inventory' class.
You will also want to listen for changed events in the list since you will need to know when items are added/removed so you can add/remove event handlers as necessary. I'd suggest using ObservableCollection<T> as this supports some 'collection changed' events. Is there any reason you are using LinkedList<T>?
e.g.
public class Inventory
{
private ObservableCollection<Item> _inventoryList = new ObservableCollection<Item>();
public decimal _total;
// You probably want INPC for the property here too so it can update any UI elements bound to it
public decimal Total { get { return _total; } set { _total = value; } }
// Constructor
public Inventory()
{
WireUpCollection();
}
private void WireUpCollection()
{
// Listen for change events
_inventoryList.CollectionChanged += CollectionChanged;
}
private void CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
// Check what was added - I'll leave this to you, the e.NewItems are the items that
// have been added, e.OldItems are those that have been removed
// Here's a contrived example for when an item is added.
// Don't forget to also remove event handlers using inpc.PropertyChanged -= Collection_PropertyChanged;
var inpc = e.NewItems[0] as INotifyPropertyChanged;
if(inpc != null)
inpc.PropertyChanged += Collection_PropertyChanged;
}
private void Collection_PropertyChanged(object sender, PropertyChangedEventArgs e)
{
RecalculateTotal();
}
private void RecalculateTotal()
{
// Your original code here which should feed the backing field
}
}
Check out the MSDN docs here:
http://msdn.microsoft.com/en-us/library/ms668604(v=vs.110).aspx
For info on ObservableCollection<T>. The events section is what you are after. Also note you can use anonymous functions to handle the events if you prefer the syntax or want to capture variables in a certain scope etc. It helps to understand them fully (not sure what's available for Java as I've not really touched it save a couple of Android mess-about projects) so it might be worth reading up as there are a small caveats to be aware of when capturing, but that's another story!
e.g.
_inventoryList.CollectionChanged += (o,e) =>
{
// Anonymous method body here
// o = the first param (object sender), e = args (NotifyCollectionChangedEventArgs e)
};
As it was told in another answer in order to "observe" collection items you have to use ObservableCollection which has a special event CollectionChanged. It is raised when the list is changed somehow (item added, removed, replaced). However, that event won't be raised (obviously) when some property of the existing item is changed, for example, inventory.InventoryList[0].QuantityOnHand = 8;.
So, to get the solution worked you need to observe both the collection changes (CollectionChanged event) and each collection item changes (PropertyChanged event). Though, implementing that logic correctly is not so easy. The Charlen answer is just a sketch of the solution but not the full solution.
It might be easier to use DependenciesTracking lib which solves the issue. See the example below:
public class Item : INotifyPropertyChanged
{
private int _quantityOnHand;
private decimal _retailPrice;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public int QuantityOnHand
{
get => this._quantityOnHand;
set
{
if (_quantityOnHand == value) return;
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), value, "QuantityOnHand must be a positive value");
_quantityOnHand = value;
OnPropertyChanged();
}
}
public decimal RetailPrice
{
get => _retailPrice;
set
{
if (_retailPrice == value) return;
if (value <= 0)
throw new ArgumentOutOfRangeException(nameof(value), value, "RetailPrice must be a positive value");
_retailPrice = value;
OnPropertyChanged();
}
}
}
public class Inventory : INotifyPropertyChanged
{
private static readonly IDependenciesMap<Inventory> _dependenciesMap =
new DependenciesMap<Inventory>()
.AddDependency(i => i.TotalRetailPrice,
i => i.InventoryList?.Sum(item => item.QuantityOnHand * item.RetailPrice) ?? 0.0m,
i => i.InventoryList.EachElement().QuantityOnHand, i => i.InventoryList.EachElement().RetailPrice);
private ObservableCollection<Item>? _inventoryList;
private decimal _totalRetailPrice;
public event PropertyChangedEventHandler? PropertyChanged;
private void OnPropertyChanged([CallerMemberName] string? propertyName = null) =>
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
public ObservableCollection<Item>? InventoryList
{
get => _inventoryList;
set
{
if (_inventoryList == value) return;
_inventoryList = value;
OnPropertyChanged();
}
}
public decimal TotalRetailPrice
{
get => _totalRetailPrice;
private set
{
if (value == _totalRetailPrice) return;
_totalRetailPrice = value;
OnPropertyChanged();
}
}
public Inventory()
{
_dependenciesMap.StartTracking(this);
}
}
public class Tests_SO_20767981
{
[SetUp]
public void Setup()
{
}
[Test]
public void Test_SO_20767981()
{
var inventory = new Inventory();
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(0.0m));
inventory.InventoryList = new ObservableCollection<Item>();
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(0.0m));
inventory.InventoryList.Add(new Item { QuantityOnHand = 3, RetailPrice = 5 });
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(15));
inventory.InventoryList.Add(new Item { QuantityOnHand = 1, RetailPrice = 7 });
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(22));
inventory.InventoryList[0].QuantityOnHand = 8;
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(47));
inventory.InventoryList[0].RetailPrice = 12;
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(103));
inventory.InventoryList.RemoveAt(1);
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(96));
var newInventoryList = new ObservableCollection<Item>
{
new Item() { QuantityOnHand = 10, RetailPrice = 0.5m},
new Item() { QuantityOnHand = 6, RetailPrice = 1.5m}
};
inventory.InventoryList = newInventoryList;
Assert.That(inventory.TotalRetailPrice, Is.EqualTo(14m));
}
}

Categories

Resources