How to get nested collection change event to bubble up - c#

I have a series of nested collections that I need to detect a change anywhere from the uppermost to lowermost layer. First, I'll show my code:
public class CategoricalDataItem
{
public string XValue { get; set; }
public double YValue { get; set; }
public SolidColorBrush Color { get; set; }
public CategoricalDataItemCollection SimpleSubData { get; set; }
}
[Serializable]
public class CategoricalDataItemCollection : ObservableCollection<CategoricalDataItem>
{
public string Descriptor { get; set; }
}
This code is structured for drill-down charting. Basically, I am allowing 5 layers deep. A developer could create an instance of CategoricalDataItemCollection and for each CategoricalDataItem within that collection, a new instance of CategoricalDataItemCollection could be created and so on. I am needed to be made aware if any item is added or removed from any of these nested collections. CollectionChanged event only detects a change in the first layer. Suggestions would be appreciated.

The trick is monitoring the collection for changes. Below is an example for doing so.
I included a small test class that will spit out messages as new items are added.
The key thing to note is that you are essentially consuming your own inherited CollectionChanged event so that you can monitor the children coming in and out and listening for when their collection changes. Please pay special attention that this is purely an example and needs some polishing and testing when it comes to the MyCollectionChanged(...) as there are other NotifyCollectionChangedActions that need to be handled.
[Serializable]
public class CategoricalDataItemCollection : ObservableCollection<CategoricalDataItem>
{
public string Descriptor { get; set; }
public CategoricalDataItemCollection()
{
this.CollectionChanged += MyCollectionChanged;
}
void MyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// There are other actions to handle. This is purely an example.
if (args.OldItems != null)
{
foreach (var oldItem in args.OldItems.Cast<CategoricalDataItem>())
{
if (args.Action == NotifyCollectionChangedAction.Remove)
oldItem.SimpleSubData.CollectionChanged -= InvokeCollectionChanged;
}
}
if (args.NewItems != null)
{
foreach (var newItem in args.NewItems.Cast<CategoricalDataItem>())
{
if (args.Action == NotifyCollectionChangedAction.Add)
newItem.SimpleSubData.CollectionChanged += InvokeCollectionChanged;
}
}
}
void InvokeCollectionChanged(object sender, NotifyCollectionChangedEventArgs args)
{
// This is the tricky part. Nothing actually changed in our collection, but we
// have to signify that something did.
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
}
public class TestClass
{
public void TestNotify()
{
var parent = new CategoricalDataItemCollection();
parent.CollectionChanged += (sender, args) => Debug.Print("Parent Collection Changed");
var child = new CategoricalDataItem {SimpleSubData = new CategoricalDataItemCollection()};
child.SimpleSubData.CollectionChanged += (sender, args) => Debug.Print("Child Collection Changed");
var grandChild = new CategoricalDataItem { SimpleSubData = new CategoricalDataItemCollection()};
grandChild.SimpleSubData.CollectionChanged += (sender, args) => Debug.Print("Grand Child Collection Changed");
//Should only output "Parent"
parent.Add(child);
//Should only output "Child" and then "Parent"
child.SimpleSubData.Add(grandChild);
//Should now output "Grand Child" and then "Child" and then "Parent" messages.
grandChild.SimpleSubData.Add(new CategoricalDataItem(){SimpleSubData = new CategoricalDataItemCollection()});
}
}

Related

Calculate properties using items in List with conditions

I have a list object that has several properties that I want to use for calculations for an model properties.
something like:
List<Cars>
that has properties for Wheels/Windows/HasRoof/FuelType, etc.
I have a model for "Parts" (class example below) that I want to fill but I have a few rules to apply, I'm going to pseudoCode what I think I should do, but I'm not sure if this is the approach for this:
public class Parts
{
public int AmountOfWheels { get; set; }
public int AmountOfWheelsForFuelTypeGas { get; set; }
public AmountOfWindowsForCarsWithRoof Type { get; set; }
}
public Parts Parts { get; set; }
this is what I want to fill:
foreach (var item in Cars)
{
Parts.AmountOfWheels =+ item.Wheels;
Parts.AmountOfWheelsForFuelTypeGas // <-- This is what I don't know
Parts.AmountOfWindowsForCarsWithRoof // <-- This is what I don't know
}
Then later I want to show the user this Parts object in a webApp, but I'm not sure how to populate this object.
The part I'm not sure if it's ok to do the calculations like this or shall I do something in the object model with properties
I think I know what you're getting at, but tell me if I miss the mark!
For most UIs including web apps, the UI box that is displaying something like AmountOfWheels is bound to changes of that property using the INotifyPropertyChanged interface. If your Parts class implements that interface, then when this line in your code executes:
Parts.AmountOfWheels =+ item.Wheels;
that will change the value of AmountOfWheels property and that will fire a property changed event. The calculation in turn will set the other properties (like AmountOfWheelsForFuelTypeGas). That fires its own changed event and the UI just picks up on that property change and shows the value.
// A class that notifies when its properties change
class Part : INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// The private 'backing store' of the variable which is a Field
/// </summary>
private int _amountOfWheels = 1;
/// <summary>
/// The public property
/// </summary>
public int AmountOfWheels
{
get => _amountOfWheels;
set
{
if (_amountOfWheels != value)
{
_amountOfWheels = value;
OnPropertyChanged();
}
}
}
int _amountOfWheelsForFuelTypeGas = -1;
public int AmountOfWheelsForFuelTypeGas
{
get => _amountOfWheelsForFuelTypeGas;
set
{
if (_amountOfWheelsForFuelTypeGas != value)
{
_amountOfWheelsForFuelTypeGas = value;
OnPropertyChanged();
}
}
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
switch (propertyName)
{
case nameof(AmountOfWheels):
onAmountOfWheelsChanged();
break;
}
}
private void onAmountOfWheelsChanged()
{
// Perform some calculation based on AmountOfWheels.
AmountOfWheelsForFuelTypeGas = TestDataGenerator.Next(0,10);
}
private Random TestDataGenerator = new Random();
}
Test driver for it:
class Program
{
static void Main(string[] args)
{
var part = new Part();
part.PropertyChanged += (sender, e) =>
{
switch (e.PropertyName)
{
case nameof(Part.AmountOfWheelsForFuelTypeGas):
Console.WriteLine($"Amount of Wheels {part.AmountOfWheels} = (Rando test data){part.AmountOfWheelsForFuelTypeGas}");
break;
}
};
for (int i = 0; i < 5; i++)
{
part.AmountOfWheels = i;
}
}
}

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.

CollectionChanged - Name of Collection

I have implemented CollectionChanged for an ObservableCollection just like here:
Implementing CollectionChanged
Is there a possibility in the OnCollectionChanged method to find out what the name of the changed property is?
EDIT:
A history should be written if there is any change in the class.
There are three cases I want to achieve:
"Normal" property is changed (string, int, ...): this is already
working
Add and remove in collections: would be working if I know the name of the changed collection
Property inside a collection is changed: same problem as 2) I don't know the name (and index) of the ch
public class ParentClass : BaseModel
{
public ParentClass()
{
Children = new ObservableCollection<SomeModel>();
Children.CollectionChanged += Oberservable_CollectionChanged;
}
private string id;
public string Id
{
get
{
return id;
}
set
{
string oldId = id;
id = value;
OnPropertyChanged(oldArticleId,id);
}
}
public ObservableCollection<SomeModel> Children { get; set; }
protected virtual void OnPropertyChanged(object oldValue, object newValue, [CallerMemberName] string propertyName = null)
{
//1) Entry is added to history (this part is working)
}
protected void Oberservable_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
{
if (e.NewItems != null)
{
foreach (INotifyPropertyChanged added in e.NewItems)
{
added.PropertyChanged += OnPropertyChangedHandler;
OnCollectionChanged(CollectionChangedModel.Action.Added, added);
}
}
if (e.OldItems != null)
{
foreach (INotifyPropertyChanged removed in e.OldItems)
{
removed.PropertyChanged -= OnPropertyChangedHandler;
OnCollectionChanged(CollectionChangedModel.Action.Removed, removed);
}
}
}
protected virtual void OnCollectionChanged(CollectionChangedModel.Action action, object value)
{
//2) TODO: History should be written, but I don't have the property name
}
public void OnPropertyChangedHandler(object sender, PropertyChangedEventArgs propertyChangedEventArgs)
{
//3) TODO: History about changed property in child-class (not working because of missing property name and index)
}
}
This is correct:
Children = new ObservableCollection<SomeModel>();
Children.CollectionChanged += Oberservable_CollectionChanged;
Though you have to ensure nobody will change collection, e.g. like this:
public ObservableCollection<SomeModel> Children { get; }
or rather making it a full property and subscribing/unsubscribing in the setter.
And now regarding Oberservable_CollectionChanged handler:
Add and remove in collections: would be working if I know the name of the changed collection
Add sender to your event.
Wrap arguments into ...EventArgs class (make it immutable), see msdn.
public event EventHandler<MyCollectionChangedEventArgs> CollectionChanged;
protected virtual void OnCollectionChanged(object sender, MyCollectionChangedEventArgs e)
{
//2) TODO: History should be written, but I don't have the property name
// sender is name
// e.Action and e.Value are parameters
}
Property inside a collection is changed: same problem as 2) I don't know the name (and index) of the ch
Wrap event handlers into instance containing event handler and value you need (I am leaving that task for you). If unsubscribing is not required, this can be easily achieved by using lamdba closures:
foreach (var added in e.NewItems.OfType<INotifyPropertyChanged>)
{
added.PropertyChanged += (s, e) =>
{
// you have `sender` here in addition to `s`, clever?
};
}

MVVM : RaisePropertyChange on another item in the same list

I am using MVVM Light in a (pretty simple) WPF project.
I have a list of versions, and for each of them there is a button "activate" and "archive". Only one version can be active.
When clicking on "activate", the software must archive the currently active version, and activate the selected one.
How would you modelize this ? I'm currently using a pretty ugly solution : the selected version re-instantiates the "active version" and archives it, so obviously the previously-active version isn't "refreshed".
The main window contains a list of versions, displayed in a ListBox (see this question).
public class MainWindowViewModel : ViewModelBase
{
public MainWindowViewModel()
{
this.InstalledVersions = InstalledVersionViewModel.GetInstalledVersions();
}
public ObservableCollection<InstalledVersionViewModel> InstalledVersions { get; set; }
}
The InstalledVersionViewModel is (simplified) like this :
public class InstalledVersionViewModel : ViewModelBase
{
public InstalledVersionViewModel()
{
this.HandleActivateVersionCommand = new RelayCommand<RoutedEventArgs>(e => { this.ActivateVersion(); });
this.HandleArchiveVersionCommand = new RelayCommand<RoutedEventArgs>(e => { this.ArchiveVersion(); });
}
public string FolderPath { get; set; }
public RelayCommand<RoutedEventArgs> HandleActivateVersionCommand { get; private set; }
public RelayCommand<RoutedEventArgs> HandleArchiveVersionCommand { get; private set; }
public string VersionNumber { get; set; }
public static InstalledVersionViewModel GetCurrentVersion()
{
return GetVersionInfos(baseInstallPath); // returns the currently-active version
}
public static ObservableCollection<InstalledVersionViewModel> GetInstalledVersions()
{
var list = new ObservableCollection<InstalledVersionViewModel>();
// snip : fill the list from detected versions
return list;
}
private void ActivateVersion()
{
// snip
GetCurrentVersion().Archive();
// snip
}
private void ArchiveVersion()
{
// snip
}
}
The problem is in the ActivateVersion() method : I'm getting a new version instance to archive it, so obviously the version instance in the list is never aware of this change. But I don't know how to change the behavior to archive the version in the list instead. I'm pretty sure there should be either some kind of messaging system, a wrapper or an overarching structure, but I can't quite put my finger on it.
Thanks !
To me, it should be handled in the MainViewModel. For instance, add a property IsActive to your InstalledVersionViewModel, and subscribe to the PropertyChanged event from your MainViewModel. When the event is raised, browse your InstalledVersions list to find the previously active item, and call the Archive method on it.
To subscribe to the event, simply browse your list after creating it:
public MainWindowViewModel()
{
this.InstalledVersions = InstalledVersionViewModel.GetInstalledVersions();
foreach (var version in this.InstalledVersions)
{
version.PropertyChanged += this.VersionPropertyChanged;
}
}
Then, in the event, check which property has been changed:
private void VersionPropertyChanged(object sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == "IsActive")
{
var changedVersion = (Version)sender;
// Checks that the version has been activated
if (changedVersion.IsActive)
{
// Finds the previously active version and archive it
foreach (var version in this.InstalledVersions)
{
if (version.IsActive && version != changedVersion)
{
version.Archive();
}
}
}
}
}

Drag'n'drop data between instances

I'm attempting to allow my users to drag and drop certain rows of data from one custom list control to another, where the second list control is in another instance of the same application.
DoDragDrop(parameterTypedListView.SelectedObjects, DragDropEffects.Copy);
where parameterTypedListView.SelectedObjects is a generic IList where T is a custom class containing only valuetypes as fields/properties.
In the OnDragDrop event I try to extract this data but only get a System.__ComObject ... object which seems to inherit from System.MarshalByRefObject.
In short: How do I extract the data in an object oriented format I can actually use?
Edit: Setting my custom class as serializable has no discernible effect whatsoever. I can enumerate the __ComObject:
foreach (var dataObject in (IEnumerable) e.Data.GetData("System.Collections.ArrayList"))
{
// this actually enumerates the correct number of times, i.e. as many times as there are items in the list.
}
but every dataObject is, in itself, a System.__ComObject that I cannot cast to anything useful.
I was able to replicate your initial problem, but as soon as I added the [Serializable] attribute to the class in the array list, I was able to see the objects as their correct type.
Here is some example code, showing a small working example.
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
this.DragDrop += new System.Windows.Forms.DragEventHandler(this.Form1_DragDrop);
this.DragEnter += new System.Windows.Forms.DragEventHandler(this.Form1_DragEnter);
}
[Serializable]
class DragClass
{
public string Prop1 { get; set; }
public int Prop2 { get; set; }
}
private void label1_MouseDown(object sender, MouseEventArgs e)
{
System.Collections.ArrayList aDragClasses = new System.Collections.ArrayList();
aDragClasses.Add(new DragClass() { Prop1 = "Test1", Prop2 = 2 });
aDragClasses.Add(new DragClass() { Prop1 = "Test2", Prop2 = 3 });
aDragClasses.Add(new DragClass() { Prop1 = "Test3", Prop2 = 4 });
DoDragDrop(aDragClasses, DragDropEffects.Copy);
}
private void Form1_DragEnter(object sender, DragEventArgs e)
{
e.Effect = DragDropEffects.Copy;
}
private void Form1_DragDrop(object sender, DragEventArgs e)
{
foreach (var aData in (System.Collections.IEnumerable)e.Data.GetData(typeof(System.Collections.ArrayList)))
{
System.Diagnostics.Debug.WriteLine(((DragClass)aData).Prop1);
}
}
}
I think that the problem is that you are using the list directly to pass the data. I tried it several different ways to get it to fail and figured out a few ways that it doesn't work.
If you don't have the [Serializable] attribute on your custom classes it will not work correctly because this is how the classes are marshaled between the processes. Also, if I use a List directly to pass the data I get a null reference exception.
If you use a simple transport class to pass the data (and all the types are serializable) then everything worked fine for me.
[Serializable]
class Test
{
public string Name { get; set; }
public string Description { get; set; }
}
[Serializable]
class Transport
{
public Transport()
{
this.Items = new List<Test>();
}
public IList<Test> Items { get; private set; }
}
Then I can do this no problem and it works across instances...
private void Form1_DragDrop(object sender, DragEventArgs e)
{
foreach (var item in ((Transport)e.Data.GetData(typeof(Transport))).Items)
{
System.Diagnostics.Debug.WriteLine(item.Name + " " + item.Description);
}
}

Categories

Resources