0

I have a ClassWrapper class and a BaseClassWrapper class. The BaseClassWrapper has an object of type ClassDTO and inside it has an ObservableCollection that is what I want to "observe". When I create an object of type "ClassWrapper" and add an item to the collection ClassWrapper.ClassDTO.MyCollection.Add(new OtherClass()) the observer does not work.

But if I create ClassDTO or an ObservableCollection inside ClassWrapper (not in BaseWrapper) it works perfectly. Why does this happen?

public class ClassWrapper : BaseClassWrapper
{
    public ClassWrapper()
    {
        Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>
                (x => ClassDTO.MyCollection.CollectionChanged += x, x => ClassDTO.MyCollection.CollectionChanged -= x)
            .Where(x => x.EventArgs.Action == NotifyCollectionChangedAction.Add ||
                        x.EventArgs.Action == NotifyCollectionChangedAction.Replace ||
                        x.EventArgs.Action == NotifyCollectionChangedAction.Remove)
            .Throttle(TimeSpan.FromMilliseconds(250))
            .Subscribe(x =>
            {
                RaisePropertyChanged(SomeProperty);
            });
    }
}

public abstract class BaseClassWrapper : ObservableObject // MVVM Light
{
    public ClassDTO ClassDTO { get; set; } = new ClassDTO();
}

public class ClassDTO
{
    public ObservableCollection<OtherClass> MyCollection { get; set; } = new ObservableCollection<OtherClass>();
}
Kirk Woll
  • 76,112
  • 22
  • 180
  • 195
avechuche
  • 1,470
  • 6
  • 28
  • 45
  • First, how did you test this? Second, check [this question](https://stackoverflow.com/questions/7260746/using-rx-reactive-extensions-to-watch-for-specific-item-in-observablecollectio) and the quote `The generic ObservableCollection has nothing to do with the IObservable interface` – Panagiotis Kanavos Jan 17 '19 at 16:17
  • If you load multiple items at once you could use a plain-old or array list and raise the notification event when setting that list. If your UI can add items one at a time, you could raise the notification right where the change happens, eg at the top ViewModel, in response to whatever command triggered the change. Otherwise it's a good idea to use `ReactiveUI` – Panagiotis Kanavos Jan 17 '19 at 16:21
  • 1) I create a ClassWrapper object, I add it to an observablecollection that is binded to the view. Then I access that object with var obj = ObservableCollection.FirstOrDefault () and then obj.ClassDTO.MyCollection.Add (...). That does not work (if ClassDTO is in BaseClassWrapper) but if I do this obj.MyCollection.Add (...) or if I add ClassDTO to ClassWrapper, if it works. 2) The generic ObservableCollection has nothing to do with the IObservable interface Exactly, that's why I use Observable from System.Reactive – avechuche Jan 17 '19 at 16:26
  • But how do you *test* it? How do you add items? When I tried a timer that adds an item every 100ms I found that Throttle didn't behave the way I thought it would. It won't fire if events keep coming inside a 250ms window – Panagiotis Kanavos Jan 17 '19 at 16:31
  • What do you want to do in any case? *Prevent* events until there's a 250 ms lull, or fire if there's an event once every 250 ms? – Panagiotis Kanavos Jan 17 '19 at 16:32
  • What I need is that if I add 1 or 1000 objects at the same time, the properties are updated 250 ms after the last one was added – avechuche Jan 17 '19 at 16:37
  • Then I can't repro this. I *do* get a single event 250ms after the *last* item is added. That's why I ask how this is tested. *If* you load all items at once though, or if you don't mind rendering all items again, the code would be a lot simpler if you raised the event just once for the entire collection, the same as if a new list was assigned to the property – Panagiotis Kanavos Jan 17 '19 at 16:43
  • I do not get any event. I added a breakpoint inside Throttle and never enter. – avechuche Jan 17 '19 at 16:46
  • Post your test code. How do you *add* items? `Throttle(TimeSpan.From(..))` will only be called once when the pipeline gets constructed. It's the subscriber callback that should be called after 250ms – Panagiotis Kanavos Jan 17 '19 at 16:52
  • It's very strange that you do not work for me. public ObservableCollection MyBindingList { get; set; } = new ObservableCollection(); private void UpdateList() { MyBindingList.FirstOrDefault().ClassDTO.MyCollection.Add(new OtherClass()); } I call UpdateList many times from a button in the UI. Later I could create a functional example – avechuche Jan 17 '19 at 17:14
  • Hi @PanagiotisKanavos I found the error, in your code try this var c = new ClassWrapper { ClassDTO = new ClassDTO() // ofcourse, I need create ClassDTO with custom properties }; When I do that, the observer "breaks". The problem is that I do not know why it happens – avechuche Jan 17 '19 at 19:38
  • that's because the property assignment occurs *after* construction has finished. The object initialization syntax calls the default constructor first and only then assings properties to the already constructed object. – Panagiotis Kanavos Jan 18 '19 at 07:36

1 Answers1

2

I tried the code and added a new item every 100ms and was ... perplexed to say the least until I accidentally hovered over Throttle and saw :

Ignores elements from an observable sequence which are followed by another element within a specified relative time duration.

I suspect, like me, you expected Throttle() to return the last item in a window. Even though its description in ReactiveX.io is

only emit an item from an Observable if a particular timespan has passed without it emitting another item

And the documentation remarks say :

For streams that never have gaps larger than or equal to dueTime between elements, the resulting stream won't produce any elements.

In fact, I've used it this way in the past, but somehow I trip over the name each time until I remember that the actual operation is debouncing, not throttling.

When I slowed the timer to fire eg every 300 ms I started getting results.

The operator that returns the last event in a window is Sample, not Throttle. If that's what you want, you should use

.Sample( TimeSpan.FromMilliseconds(300))

instead of Throttle.

Use Throttle if you want to update the UI only after notifications stop coming for 250 ms

Update

To test this behaviour I created a console application. I added a couple of fixes to the question's code to allow it to compile :

public class ClassWrapper : BaseClassWrapper
{
    public string SomeProperty { get; set; }
    public ClassWrapper()
    {
        Observable.FromEventPattern<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>
                (x => ClassDTO.MyCollection.CollectionChanged += x, x => ClassDTO.MyCollection.CollectionChanged -= x)
            .Where(x => x.EventArgs.Action == NotifyCollectionChangedAction.Add ||
                        x.EventArgs.Action == NotifyCollectionChangedAction.Replace ||
                        x.EventArgs.Action == NotifyCollectionChangedAction.Remove)
            .Throttle( TimeSpan.FromMilliseconds(250))
            .Subscribe(x =>
            {
                RaisePropertyChanged( ()=> SomeProperty);
            });
    }
}

The application's Main method adds an item every 100ms. 250ms after the last item is added, a single notification event is raised and a message gets printed:

static async Task Main(string[] args)
{
    Console.WriteLine("Started");
    var c = new ClassWrapper();
    c.PropertyChanged += (sender, e) =>
    {
        Console.WriteLine($"Collection has {c.ClassDTO.MyCollection.Count} items");
    };

    for (int i = 0; i < 100; i++)
    {
        c.ClassDTO.MyCollection.Add(new OtherClass());
        await Task.Delay(100);

    }

    Console.ReadKey();
}    
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236