Meziantou.Framework icon indicating copy to clipboard operation
Meziantou.Framework copied to clipboard

ConcurrentObservableCollection<T> should provide a way to defer events

Open meziantou opened this issue 5 years ago • 10 comments

When modifying the collection, it may be faster to do all the modifications and then raise the Reset event instead of raising lots a Add/Remove events

using (collection.DeferEvents())
{
    collection.AddRange(toAdd);
    collection.RemoveRange(toRemove);
}

meziantou avatar Dec 12 '19 20:12 meziantou

Par contre si on fait un reset, je pense que ça fera un reset global de la vue wpf non? Du coup dans le cas où il y a que des add et des remove, on pourrait pas combiner ces events? un event de type Add et un event de type Remove?

LaurentMinot avatar Dec 13 '19 08:12 LaurentMinot

What do you think of this design?

public class ConcurrentObservableCollection<T>
{
+    /// <summary>
+    /// Combine CollectionChanged events when possible (for instance, add/remove)
+    /// </summary>
+    public bool OptimizeEvents { get; set; }

+    /// <summary>
+    /// Begin a batch of operations on the collection. No CollectionChanged event is raised during the batch.
+    /// A Reset event is sent at the end of the batch.
+    /// </summary>
+    public IDisposable BeginBatch();
}

meziantou avatar Dec 14 '19 18:12 meziantou

I like and understand the BeginBatch part, but I don't understand the OptimizeEvents one. Could you detail a bit?

  1. Is it used only in Batch mode?
  2. Does it mean when it's true it will try to combine events and not always send a reset event when possible? Also I think it could be really interesting to have AddRange and RemoveRange methods.

LaurentMinot avatar Dec 15 '19 10:12 LaurentMinot

BeginBatch always send a Reset event. OptimizeEvents is another option, not linked to the batch. As events are dequeue on the Dispatcher thread, there can be multiple events (Add/Remove/Update). It can be possible to suppress some events such as Add/Update followed by Remove.

I don't know if it could be interesting (I guess things based on your comment :)) but we can change BeginBatch to take a parameter that indicates if you want to use a Reset or optimized Add/Remove events.

We can add AddRange and RemoveRange but the implementation is just calling Add/Remove for each items.

public class ConcurrentObservableCollection<T>
{
+    public void AddRange(params T[] items);
+    public void AddRange(IEnumerable<T> items);
+
+    public void RemoveRange(params T[] items);
+    public void RemoveRange(IEnumerable<T> items);
+
+    /// <summary>
+    /// Combine CollectionChanged events when possible (for instance, add/remove)
+    /// </summary>
+    public bool OptimizeEvents { get; set; }
+
+    /// <summary>
+    /// Begin a batch of operations on the collection. No CollectionChanged event is raised during the batch.
+    /// A Reset event is sent at the end of the batch.
+    /// </summary>
+    public IDisposable BeginBatch(BatchMode mode);
}

+public enum BatchMode
+{
+    Normal, // Not sure of this name
+    Optimized,
+    Reset,
+}

meziantou avatar Dec 15 '19 23:12 meziantou

any news?

ghost1372 avatar Apr 28 '21 08:04 ghost1372

@LePtitDev started PR #42. However, he didn't finish it (there were a few opened conversations). Then, the PR was closed when I changed the default branch from master to main.

If you want you can submit a new PR and resolve the opened discussion.

meziantou avatar Apr 28 '21 18:04 meziantou

Yeah, this would be very good to have. I got her by my #230 ticket. I find that most of the time if we are updating "observeablecollection" from another thread, it is usually because we have so many records to process. Unfortunately, I don't quite understand the code well enough to implement what you are proposing above. But in theory, it would be good if we could batch the events and then only have one Reset or Add. I will see if I can figure out the code as it does things beyond what I can do.

heavywoody avatar Jul 28 '21 15:07 heavywoody

I actually got this to work quite well. I actually don't know how to contribute, so I will put the pieces in here I created. If you want to tell me how I can contribute, I am happy to do that. I anyone puts in a better way, please let me know. But this works really, really well for me. I am adding thousands of records every 400 ms and the UI is flawless.

IndexableCollection.cs I added this: public void AddRange(IEnumerable<T> newitems) { lock (_lock) { _observableCollection?.EnqueueAddRange(newitems); } }

DispatchedObserveableCollection.cs I added this:

internal void EnqueueAddRange(IEnumerable<T> items) { EnqueueEvent(PendingEvent.AddRange(items)); }

PendingEvent.cs I added this: public static PendingEvent<T> AddRange<T>(IEnumerable<T> items) => new PendingEvent<T>(PendingEventType.AddRange, items);

And then added this under ProcessPendingEvents case PendingEventType.AddRange: AddRange(pendingEvent.RangeItems); break;

PendingEventType.cs, I added this to enum after Add

AddRange,

PendingEvent'1.cs, I added highlighted text

internal readonly struct PendingEvent<T> { public PendingEvent(PendingEventType type) { Type = type; Item = default; Index = -1; Items = default; RangeItems = default; }

    public PendingEvent(PendingEventType type, int index)
    {
        Type = type;
        Item = default;
        Index = index;
        Items = default;
        **RangeItems = default;**
    }

    public PendingEvent(PendingEventType type, T item)
    {
        Type = type;
        Item = item;
        Index = -1;
        Items = default;
        **RangeItems = default;**
    }

    public PendingEvent(PendingEventType type, T item, int index)
    {
        Type = type;
        Item = item;
        Index = index;
        Items = default;
        **RangeItems = default;**
    }

    public PendingEvent(PendingEventType type, ImmutableList<T> items)
    {
        Type = type;
        Items = items;
        Item = default;
        Index = default;
        **RangeItems = default;**
    }

    public PendingEvent(PendingEventType type, IEnumerable<T> items)
    {
        Type = type;
        Items = default;
        Item = default;
        Index = default;
        **RangeItems = items;**
    }

    public PendingEventType Type { get; }
    public T Item { get; }
    public int Index { get; }
    public ImmutableList<T> Items { get; }

    **public IEnumerable<T> RangeItems { get; }**
}

And finally, under ObserveableCollectionBase.cs, I added this part. Last line very important because it will only update the UI after everything is added.

protected void AddRange(IEnumerable<T> items) { if (items != null) { var index = Items.Count; foreach(var item in items) { Items.Add(item); index++; }

            OnCountPropertyChanged();
            OnIndexerPropertyChanged();
            OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, new List<T>(items), index));
        }
    }

heavywoody avatar Jul 28 '21 18:07 heavywoody

The one part I hadn't figured out with the code is I am successfully inserting these values to the DispatcherCollection, but I notice the ImmutableList isn't in sync with it and trying to find out where I call to sync them?

heavywoody avatar Jul 28 '21 21:07 heavywoody

Nevermind, I completely forgot to also do _items.AddRange.

heavywoody avatar Jul 29 '21 03:07 heavywoody

This issue is stale because it has been open for 60 days with no activity.

github-actions[bot] avatar Jul 29 '23 13:07 github-actions[bot]

This issue was closed because it has been inactive for 14 days since being marked as stale.

github-actions[bot] avatar Aug 13 '23 02:08 github-actions[bot]