问题
I've tried looking at other topics on this but I haven't found a working implementation to my question. Basically, I have an ObservableCollection called "FruitBasket" that contains different kinds of fruit. FruitBasket itself contains ObservableCollections for each respective type of fruit that passes through so that they can be used as ItemSources for ListViews (Denoted by their names "AppleContainer" and "OrangeContainer"), each displaying one kind of fruit. Because the fruit classes themselves implement INotifyPropertyChanged, modifying their values triggers updates to the ListView controls just fine, however, FruitBasket has a "TotalWeight" property derived from the weights of all the other fruits in the collections. I want "TotalWeight" to update the Label control in the UI without me having to refresh the UI. Triggerering a notification on a property change of the actual ObservableCollection itself, and not simply its constituent members is more difficult and I haven't found any solutions that work so far (or that I've implemented correctly).
public class FruitBasket : ObservableCollection<IFruit>
{
private decimal _totalWeight;
public FruitBasket()
{
this.Add(new OrangeContainer(this));
this.Add(new AppleContainer(this));
}
public OrangeContainer Oranges
{
get { return (OrangeContainer)this.Items[0]; }
}
public AppleContainer Apples
{
get { return (AppleContainer)this.Items[1]; }
}
public decimal TotalWeight
{
get { return _totalWeight; }
set { _totalWeight = value; }
}
internal void UpdateWeight(IFruit caller)
{
_totalWeight = 0;
foreach (Orange orng in (OrangeContainer)this.Items[0])
{
_totalWeight += orng.Weight;
}
foreach (Apple appl in (AppleContainer)this.Items[1])
{
_totalWeight += appl.Weight;
}
}
回答1:
You need to call INotifyPropertyChanged.PropertyChanged event of your FruitBasket whenever items are added, removed or Weight property of any item has changed.
Let's split it into two tasks:
- TotalWeight should be recalculated when items are added, removed, or items' weight is changed. We need to handle those events.
- Raise FruitBasket.PropertyChanged event
I have splitted these two tasks into two classes in order to follow Single Responsibility Principle:
1) - this handles items' PropertyChanged events:
public abstract class ExtendedObservableCollection<T> : ObservableCollection<T> where T : INotifyPropertyChanged
{
protected override void ClearItems()
{
foreach (var item in Items) item.PropertyChanged -= ItemPropertyChanged;
base.ClearItems();
}
protected override void InsertItem(int index, T item)
{
item.PropertyChanged += ItemPropertyChanged;
base.InsertItem(index, item);
}
protected override void RemoveItem(int index)
{
this[index].PropertyChanged -= ItemPropertyChanged;
base.RemoveItem(index);
}
protected override void SetItem(int index, T item)
{
this[index].PropertyChanged -= ItemPropertyChanged;
item.PropertyChanged += ItemPropertyChanged;
base.SetItem(index, item);
}
abstract void ItemPropertyChanged(object sender, PropertyChangedEventArgs e);
}
2) - this recalculates TotalWeight when necessary
public class FruitBasket : ExtendedObservableCollection<IFruit>
{
protected override void ItemPropertyChanged(object sender, PropertyChangedEventArgs e){
UpdateWeight();
OnPropertyChanged("TotalWeight")
}
protected override void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
{
UpdateWeight();
OnPropertyChanged("TotalWeight")
base.OnCollectionChanged(e);
}
}
Of course your Fruit should implement INotifyPropertyChanged interface. You will find plenty of examples how to do it. It is very simple.
回答2:
I found the root(s) of the problem(s). I'll start with the most obvious:
I wasn't as diligent in assigning datacontext in the UI for the Fruit Basket observable collection object itself as I was the for the members of its collection (OrangeContainer and AppleContainer). In the initialization of the UI window, assigning datacontext to the ListView objects is second nature. I wasn't quite matching the right node's datacontext in the XAML to the Fruit Basket object in the initialization method in the code behind (I really should have checked that earlier).
Because of the misaligned assignments of datacontext/binding, between the XAML and initialization method, the propertychanged event was never firing for my fruit basket observable collection like it was for the Apple and Orange objects inside the OrangeContainer and AppleContainer collections that were members of FruitBasket. So, in the Orange class declaration we'd have this:
public class Orange : INotifyPropertyChanged, IFruit
And the implementation like so
public event PropertyChangedEventHandler PropertyChanged;
public void PropChange(string prop)
{
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, new PropertyChangedEventArgs(prop));
}
}
And when the PropChange method was called in the Weight property setter, this.PropertyChanged would not be null, and everything would work fine.
The FruitBasket class was a bit more tricky. Because of the aformentioned issue of improper matching in the UI Code, this.PropertyChanged would return null every time I tried to notify a change in property. However, it got a bit more confusing because unlike the Orange or Apple classes, it inherits ObservableCollection (ObservableCollection in the declaration if we want to be specific). I know ObservableCollection is really just a Collection class that implements INotifyPropertyChanged and INotifyCollectionChanged interfaces. It's really nice to see the plumbing now that .NET is open source (praise the lord)
http://referencesource.microsoft.com/#System/compmod/system/collections/objectmodel/observablecollection.cs
In any case, implementing this became more confusing, because I kept seeing this:
Warning 1 'TestingObsColNotify.FruitBasket.PropertyChanged' hides inherited member 'System.Collections.ObjectModel.ObservableCollection.PropertyChanged'. To make the current member override that implementation, add the override keyword. Otherwise add the new keyword. C:\Testing VS Project\TestingObsColNotify\TestingObsColNotify\FruitBasket.cs 60 50 TestingObsColNotify
I still see this, but my implementation works, because while it was a result of the inheritance from INotifyProperty changed via ObservableCollection as seen in my original class declaration
public class FruitBasket : ObservableCollection<IFruit>
This was just shy of the last element need to make everything work, which was adding the INotifyPropertyChanged to the class itself like so:
public class FruitBasket : ObservableCollection<IFruit>, INotifyPropertyChanged
It seems a bit redundant and inelegant but I didn't get very far trying to override and wrestle with the inheritance of INotifyPropertyChanged from ObservableCollection (Or as best as I can understand).
So there we have it, everything works now, sans MVVM. I'll certainly move on to that pattern later, but it's nice to have resolved this issue instead of lazily just re-assigning the contents of controls in the code behind methods on the UI side of things.
Thank you to those who came in here and contributed, I appreciate you taking the time to respond.
回答3:
If using binding, Add the interface INotifyPropertyChanged to your class. If you have ReSharper installed, accept the recommendation to implement the interface. Then, whenever you want to update any text box, call PropertyChanged with the name of the property TotalWeight, see https://softwareengineering.stackexchange.com/questions/228067/where-do-put-inotifypropertychanged-interface-in-model-or-viewmodel. Whenever you update any of the ObservableCollections, manually update the TotalWeight, then call the aforementioned PropertyChanged to tell the UI to update itself. I've used this technique to push updates from the ViewModel into the View (i.e. from the class into the XAML) for some fairly complex scenarios, it works very well.
I'd also recommend following the learning curve for MVVM, projects written in that way tend to be more scalable, are easier to maintain, and just easier to work with.
来源:https://stackoverflow.com/questions/30444874/trigger-inotifypropertychanged-collectionchanged-on-observablecollection