Advertisement
burningmime

AsyncObservableCollection updated 2015-04-17

Apr 17th, 2015
10,567
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
C# 34.29 KB | None | 0 0
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Collections.ObjectModel;
  5. using System.Collections.Specialized;
  6. using System.ComponentModel;
  7. using System.Diagnostics;
  8. using System.Reflection;
  9. using System.Runtime.Serialization;
  10. using System.Threading;
  11. using System.Windows.Threading;
  12.  
  13. namespace burningmime.util.wpf
  14. {
  15.     /// <summary>
  16.     /// A version of <see cref="ObservableCollection{T}"/> that is locked so that it can be accessed by multiple threads. When you enumerate it (foreach),
  17.     /// you will get a snapshot of the current contents. Also the <see cref="CollectionChanged"/> event will be called on the thread that added it if that
  18.     /// thread is a Dispatcher (WPF/Silverlight/WinRT) thread. This means that you can update this from any thread and recieve notifications of those updates
  19.     /// on the UI thread.
  20.     ///
  21.     /// You can't modify the collection during a callback (on the thread that recieved the callback -- other threads can do whatever they want). This is the
  22.     /// same as <see cref="ObservableCollection{T}"/>.
  23.     /// </summary>
  24.     [Serializable, DebuggerDisplay("Count = {Count}")]
  25.     public sealed class AsyncObservableCollection<T> : IList<T>, IReadOnlyList<T>, IList, INotifyCollectionChanged, INotifyPropertyChanged, ISerializable
  26.     {
  27.         // we implement IReadOnlyList<T> because ObservableCollection<T> does, and we want to mostly keep API compatability...
  28.         // this collection is NOT read only, but neither is ObservableCollection<T>
  29.        
  30.         private readonly ObservableCollection<T> _collection;       // actual collection
  31.         private readonly ThreadLocal<ThreadView> _threadView;       // every thread has its own view of this collection
  32.         private readonly ReaderWriterLockSlim _lock;                // whenever accessing the collection directly, you must aquire the lock
  33.         private volatile int _version;                              // whenever collection is changed, increment this (should only be changed from within write lock, so no atomic needed)
  34.        
  35.         public AsyncObservableCollection()
  36.         {
  37.             _collection = new ObservableCollection<T>();
  38.             _lock = new ReaderWriterLockSlim();
  39.             _threadView = new ThreadLocal<ThreadView>(() => new ThreadView(this));
  40.             // It was a design decision to NOT implement IDisposable here for disposing the ThreadLocal instance. ThreaLocal has a finalizer
  41.             // so it will be taken care of eventually. Since the cache itself is a weak reference, the only difference between explicitly
  42.             // disposing of it and waiting for finalization will be ~80 bytes per thread of memory in the TLS table that will stay around for
  43.             // an extra couple GC cycles. This is a tiny, tiny cost, and reduces the API complexity of this class.
  44.         }
  45.  
  46.         public AsyncObservableCollection(IEnumerable<T> collection)
  47.         {
  48.             _collection = new ObservableCollection<T>(collection);
  49.             _lock = new ReaderWriterLockSlim();
  50.             _threadView = new ThreadLocal<ThreadView>(() => new ThreadView(this));
  51.         }
  52.  
  53.         #region ThreadView -- every thread that acceses this collection gets a unique view of it
  54.         /// <summary>
  55.         /// The "view" that a thread has of this collection. One of these exists for every thread that has accesed this
  56.         /// collection, and a new one is automatically created when a new thread accesses it. Therefore, we can assume
  57.         /// thate everything in here is being called from the correct thread and don't need to worry about threading issues.
  58.         /// </summary>
  59.         private sealed class ThreadView
  60.         {
  61.             // These fields will always be accessed from the correct thread, so no sync issues
  62.             public readonly List<EventArgs> waitingEvents = new List<EventArgs>();    // events waiting to be dispatched
  63.             public bool dissalowReenterancy;                                          // don't allow write methods to be called on the thread that's executing events
  64.  
  65.             // Private stuff all used for snapshot/enumerator
  66.             private readonly int _threadId;                                           // id of the current thread
  67.             private readonly AsyncObservableCollection<T> _owner;                     // the collection
  68.             private readonly WeakReference<List<T>> _snapshot;                        // cache of the most recent snapshot
  69.             private int _listVersion;                                                 // version at which the snapshot was taken
  70.             private int _snapshotId;                                                  // incremented every time a new snapshot is created
  71.             private int _enumeratingCurrentSnapshot;                                  // # enumerating snapshot with current ID; reset when a snapshot is created
  72.  
  73.             public ThreadView(AsyncObservableCollection<T> owner)
  74.             {
  75.                 _owner = owner;
  76.                 _threadId = Thread.CurrentThread.ManagedThreadId;
  77.                 _snapshot = new WeakReference<List<T>>(null);
  78.             }
  79.  
  80.             /// <summary>
  81.             /// Gets a list that's a "snapshot" of the current state of the collection, ie it's a copy of whatever elements
  82.             /// are currently in the collection.
  83.             /// </summary>
  84.             public List<T> getSnapshot()
  85.             {
  86.                 Debug.Assert(Thread.CurrentThread.ManagedThreadId == _threadId);
  87.                 List<T> list;
  88.                 // if we have a cached snapshot that's up to date, just use that one
  89.                 if(!_snapshot.TryGetTarget(out list) || _listVersion != _owner._version)
  90.                 {
  91.                     // need to create a new snapshot
  92.                     // if nothing is using the old snapshot, we can clear and reuse the existing list instead
  93.                     // of allocating a brand new list. yay for eco-friendly solutions!
  94.                     int enumCount = _enumeratingCurrentSnapshot;
  95.                     _snapshotId++;
  96.                     _enumeratingCurrentSnapshot = 0;
  97.  
  98.                     _owner._lock.EnterReadLock();
  99.                     try
  100.                     {
  101.                         _listVersion = _owner._version;
  102.                         if(list == null || enumCount > 0)
  103.                         {
  104.                             // if enumCount > 0 here that means something is currently using the instance of list. we create a new list
  105.                             // here and "strand" the old list so the enumerator can finish enumerating it in peace.
  106.                             list = new List<T>(_owner._collection);
  107.                             _snapshot.SetTarget(list);
  108.                         }
  109.                         else
  110.                         {
  111.                             // clear & reuse the old list
  112.                             list.Clear();
  113.                             list.AddRange(_owner._collection);
  114.                         }
  115.                     }
  116.                     finally
  117.                     {
  118.                         _owner._lock.ExitReadLock();
  119.                     }
  120.                 }
  121.                 return list;
  122.             }
  123.  
  124.             /// <summary>
  125.             /// Called when an enumerator is allocated (NOT when enumeration begins, because by that point we could've moved onto
  126.             /// a new snapshot).
  127.             /// </summary>
  128.             /// <returns>The ID to pass into <see cref="exitEnumerator"/>.</returns>
  129.             public int enterEnumerator()
  130.             {
  131.                 Debug.Assert(Thread.CurrentThread.ManagedThreadId == _threadId);
  132.                 _enumeratingCurrentSnapshot++;
  133.                 return _snapshotId;
  134.             }
  135.  
  136.             /// <summary>
  137.             /// Cleans up after an enumerator.
  138.             /// </summary>
  139.             /// <param name="oldId">The value that <see cref="enterEnumerator"/> returns.</param>
  140.             public void exitEnumerator(int oldId)
  141.             {
  142.                 // if the enumerator is being disposed from a different thread than the one that creatd it, there's no way
  143.                 // to garuntee the atomicity of this operation. if this (EXTREMELY rare) case happens, we'll ditch the list next
  144.                 // time we need to make a new snapshot. this can never happen with a regular foreach()
  145.                 if(Thread.CurrentThread.ManagedThreadId == _threadId)
  146.                 {
  147.                     if(_snapshotId == oldId)
  148.                         _enumeratingCurrentSnapshot--;
  149.                 }
  150.             }
  151.         }
  152.         #endregion
  153.  
  154.         #region Read methods
  155.         public bool Contains(T item)
  156.         {
  157.             _lock.EnterReadLock();
  158.             try
  159.             {
  160.                 return _collection.Contains(item);
  161.             }
  162.             finally
  163.             {
  164.                 _lock.ExitReadLock();
  165.             }
  166.         }
  167.  
  168.         public int Count
  169.         {
  170.             get
  171.             {
  172.                 _lock.EnterReadLock();
  173.                 try
  174.                 {
  175.                     return _collection.Count;
  176.                 }
  177.                 finally
  178.                 {
  179.                     _lock.ExitReadLock();
  180.                 }
  181.             }
  182.         }
  183.  
  184.         public int IndexOf(T item)
  185.         {
  186.             _lock.EnterReadLock();
  187.             try
  188.             {
  189.                 return _collection.IndexOf(item);
  190.             }
  191.             finally
  192.             {
  193.                 _lock.ExitReadLock();
  194.             }
  195.         }
  196.         #endregion
  197.  
  198.         #region Write methods -- VERY repetitive, don't say I didn't warn you
  199.         // ARRRRRRGH!!! C# really needs macros! While it would be possible to do this using closures, it would be a huge performance cost
  200.         // With #define this would look so much nicer and be much easier/less error-prone when it needs to be changed.
  201.  
  202.         public void Add(T item)
  203.         {
  204.             ThreadView view = _threadView.Value;
  205.             if(view.dissalowReenterancy)
  206.                 throwReenterancyException();
  207.             _lock.EnterWriteLock();
  208.             try
  209.             {
  210.                 _version++;
  211.                 _collection.Add(item);
  212.             }
  213.             catch(Exception)
  214.             {
  215.                 view.waitingEvents.Clear();
  216.                 throw;
  217.             }
  218.             finally
  219.             {
  220.                 _lock.ExitWriteLock();
  221.             }
  222.             dispatchWaitingEvents(view);
  223.         }
  224.  
  225.         public void AddRange(IEnumerable<T> items)
  226.         {
  227.             ThreadView view = _threadView.Value;
  228.             if(view.dissalowReenterancy)
  229.                 throwReenterancyException();
  230.             _lock.EnterWriteLock();
  231.             try
  232.             {
  233.                 _version++;
  234.                 foreach(T item in items)
  235.                     _collection.Add(item);
  236.             }
  237.             catch(Exception)
  238.             {
  239.                 view.waitingEvents.Clear();
  240.                 throw;
  241.             }
  242.             finally
  243.             {
  244.                 _lock.ExitWriteLock();
  245.             }
  246.             dispatchWaitingEvents(view);
  247.         }
  248.        
  249.         int IList.Add(object value)
  250.         {
  251.             ThreadView view = _threadView.Value;
  252.             if(view.dissalowReenterancy)
  253.                 throwReenterancyException();
  254.             int result;
  255.             _lock.EnterWriteLock();
  256.             try
  257.             {
  258.                 _version++;
  259.                 result = ((IList) _collection).Add(value);
  260.             }
  261.             catch(Exception)
  262.             {
  263.                 view.waitingEvents.Clear();
  264.                 throw;
  265.             }
  266.             finally
  267.             {
  268.                 _lock.ExitWriteLock();
  269.             }
  270.             dispatchWaitingEvents(view);
  271.             return result;
  272.         }
  273.  
  274.         public void Insert(int index, T item)
  275.         {
  276.             ThreadView view = _threadView.Value;
  277.             if(view.dissalowReenterancy)
  278.                 throwReenterancyException();
  279.             _lock.EnterWriteLock();
  280.             try
  281.             {
  282.                 _version++;
  283.                 _collection.Insert(index, item);
  284.             }
  285.             catch(Exception)
  286.             {
  287.                 view.waitingEvents.Clear();
  288.                 throw;
  289.             }
  290.             finally
  291.             {
  292.                 _lock.ExitWriteLock();
  293.             }
  294.             dispatchWaitingEvents(view);
  295.         }
  296.        
  297.         public bool Remove(T item)
  298.         {
  299.             ThreadView view = _threadView.Value;
  300.             if(view.dissalowReenterancy)
  301.                 throwReenterancyException();
  302.             bool result;
  303.             _lock.EnterWriteLock();
  304.             try
  305.             {
  306.                 _version++;
  307.                 result = _collection.Remove(item);
  308.             }
  309.             catch(Exception)
  310.             {
  311.                 view.waitingEvents.Clear();
  312.                 throw;
  313.             }
  314.             finally
  315.             {
  316.                 _lock.ExitWriteLock();
  317.             }
  318.             dispatchWaitingEvents(view);
  319.             return result;
  320.         }
  321.  
  322.         public void RemoveAt(int index)
  323.         {
  324.             ThreadView view = _threadView.Value;
  325.             if(view.dissalowReenterancy)
  326.                 throwReenterancyException();
  327.             _lock.EnterWriteLock();
  328.             try
  329.             {
  330.                 _version++;
  331.                 _collection.RemoveAt(index);
  332.             }
  333.             catch(Exception)
  334.             {
  335.                 view.waitingEvents.Clear();
  336.                 throw;
  337.             }
  338.             finally
  339.             {
  340.                 _lock.ExitWriteLock();
  341.             }
  342.             dispatchWaitingEvents(view);
  343.         }
  344.  
  345.         public void Clear()
  346.         {
  347.             ThreadView view = _threadView.Value;
  348.             if(view.dissalowReenterancy)
  349.                 throwReenterancyException();
  350.             _lock.EnterWriteLock();
  351.             try
  352.             {
  353.                 _version++;
  354.                 _collection.Clear();
  355.             }
  356.             catch(Exception)
  357.             {
  358.                 view.waitingEvents.Clear();
  359.                 throw;
  360.             }
  361.             finally
  362.             {
  363.                 _lock.ExitWriteLock();
  364.             }
  365.             dispatchWaitingEvents(view);
  366.         }
  367.  
  368.         public void Move(int oldIndex, int newIndex)
  369.         {
  370.             ThreadView view = _threadView.Value;
  371.             if(view.dissalowReenterancy)
  372.                 throwReenterancyException();
  373.             _lock.EnterWriteLock();
  374.             try
  375.             {
  376.                 _version++;
  377.                 _collection.Move(oldIndex, newIndex);
  378.             }
  379.             catch(Exception)
  380.             {
  381.                 view.waitingEvents.Clear();
  382.                 throw;
  383.             }
  384.             finally
  385.             {
  386.                 _lock.ExitWriteLock();
  387.             }
  388.             dispatchWaitingEvents(view);
  389.         }
  390.         #endregion
  391.  
  392.         #region A little bit o' both
  393.         public T this[int index]
  394.         {
  395.             get
  396.             {
  397.                 _lock.EnterReadLock();
  398.                 try
  399.                 {
  400.                     return _collection[index];
  401.                 }
  402.                 finally
  403.                 {
  404.                     _lock.ExitReadLock();
  405.                 }
  406.             }
  407.  
  408.             set
  409.             {
  410.                 ThreadView view = _threadView.Value;
  411.                 if(view.dissalowReenterancy)
  412.                     throwReenterancyException();
  413.                 _lock.EnterWriteLock();
  414.                 try
  415.                 {
  416.                     _version++;
  417.                     _collection[index] = value;
  418.                 }
  419.                 catch(Exception)
  420.                 {
  421.                     view.waitingEvents.Clear();
  422.                     throw;
  423.                 }
  424.                 finally
  425.                 {
  426.                     _lock.ExitWriteLock();
  427.                 }
  428.                 dispatchWaitingEvents(view);
  429.             }
  430.         }
  431.         #endregion
  432.  
  433.         #region GetEnumerator and related methods that work on snapshots
  434.         public IEnumerator<T> GetEnumerator()
  435.         {
  436.             ThreadView view = _threadView.Value;
  437.             return new EnumeratorImpl(view.getSnapshot(), view);
  438.         }
  439.  
  440.         public void CopyTo(T[] array, int arrayIndex)
  441.         {
  442.             // don't need to worry about re-entry/other iterators here since we're at the bottom of the stack
  443.             _threadView.Value.getSnapshot().CopyTo(array, arrayIndex);
  444.         }
  445.  
  446.         public T[] ToArray()
  447.         {
  448.             // don't need to worry about re-entry/other iterators here since we're at the bottom of the stack
  449.             return _threadView.Value.getSnapshot().ToArray();
  450.         }
  451.  
  452.         private sealed class EnumeratorImpl : IEnumerator<T>
  453.         {
  454.             private readonly ThreadView _view;
  455.             private readonly int _myId;
  456.             private List<T>.Enumerator _enumerator;
  457.             private bool _isDisposed;
  458.  
  459.             public EnumeratorImpl(List<T> list, ThreadView view)
  460.             {
  461.                 _enumerator = list.GetEnumerator();
  462.                 _view = view;
  463.                 _myId = view.enterEnumerator();
  464.             }
  465.  
  466.             object IEnumerator.Current { get { return Current; } }
  467.             public T Current
  468.             {
  469.                 get
  470.                 {
  471.                     if(_isDisposed)
  472.                         throwDisposedException();
  473.                     return _enumerator.Current;
  474.                 }
  475.             }
  476.            
  477.             public bool MoveNext()
  478.             {
  479.                 if(_isDisposed)
  480.                     throwDisposedException();
  481.                 return _enumerator.MoveNext();
  482.             }
  483.  
  484.             public void Dispose()
  485.             {
  486.                 if(!_isDisposed)
  487.                 {
  488.                     _enumerator.Dispose();
  489.                     _isDisposed = true;
  490.                     _view.exitEnumerator(_myId);
  491.                 }
  492.             }
  493.  
  494.             void IEnumerator.Reset()
  495.             {
  496.                 throw new NotSupportedException("This enumerator doesn't support Reset()");
  497.             }
  498.  
  499.             private static void throwDisposedException()
  500.             {
  501.                 throw new ObjectDisposedException("The enumerator was disposed");
  502.             }
  503.         }
  504.         #endregion
  505.  
  506.         #region Events
  507.         // Because we want to hold the write lock for as short a time as possible, we enqueue events and dispatch them in a group
  508.         // as soon as the write method is complete
  509.  
  510.         // Collection changed
  511.         private readonly AsyncDispatcherEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs> _collectionChanged =  new AsyncDispatcherEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>();
  512.         private void onCollectionChangedInternal(object sender, NotifyCollectionChangedEventArgs args) { _threadView.Value.waitingEvents.Add(args); }
  513.         public event NotifyCollectionChangedEventHandler CollectionChanged
  514.         {
  515.             add
  516.             {
  517.                 if(value == null) return;
  518.                 _lock.EnterWriteLock(); // can't add/remove event during write operation
  519.                 try
  520.                 {
  521.                     // even though this is technically a write operation, there's no reason to check reenterancy since it won't ever call handler
  522.                     // in fact, removing handlers in the callback could be a useful scenario
  523.                     if(_collectionChanged.isEmpty) // if we were empty before, the handler wasn't attached
  524.                         _collection.CollectionChanged += onCollectionChangedInternal;
  525.                     _collectionChanged.add(value);
  526.                 }
  527.                 finally
  528.                 {
  529.                     _lock.ExitWriteLock();
  530.                 }
  531.             }
  532.             remove
  533.             {
  534.                 if(value == null) return;
  535.                 _lock.EnterWriteLock(); // can't add/remove event during write operation
  536.                 try
  537.                 {
  538.                     // even though this is technically a write operation, there's no reason to check reenterancy since it won't ever call handler
  539.                     // in fact, removing handlers in the callback could be a useful scenario
  540.                     _collectionChanged.remove(value);
  541.                     if(_collectionChanged.isEmpty) // if we're now empty, detatch handler
  542.                         _collection.CollectionChanged -= onCollectionChangedInternal;
  543.                 }
  544.                 finally
  545.                 {
  546.                     _lock.ExitWriteLock();
  547.                 }
  548.             }
  549.         }
  550.        
  551.         // Property changed
  552.         private readonly AsyncDispatcherEvent<PropertyChangedEventHandler, PropertyChangedEventArgs> _propertyChanged = new AsyncDispatcherEvent<PropertyChangedEventHandler, PropertyChangedEventArgs>();
  553.         private void onPropertyChangedInternal(object sender, PropertyChangedEventArgs args) { _threadView.Value.waitingEvents.Add(args); }
  554.         event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
  555.         {
  556.             add
  557.             {
  558.                 if(value == null) return;
  559.                 _lock.EnterWriteLock(); // can't add/remove event during write operation
  560.                 try
  561.                 {
  562.                     // even though this is technically a write operation, there's no reason to check reenterancy since it won't ever call handler
  563.                     // in fact, removing handlers in the callback could be a useful scenario
  564.                     if(_propertyChanged.isEmpty) // if we were empty before, the handler wasn't attached
  565.                         ((INotifyPropertyChanged) _collection).PropertyChanged += onPropertyChangedInternal;
  566.                     _propertyChanged.add(value);
  567.                 }
  568.                 finally
  569.                 {
  570.                     _lock.ExitWriteLock();
  571.                 }
  572.             }
  573.             remove
  574.             {
  575.                 if(value == null) return;
  576.                 _lock.EnterWriteLock(); // can't add/remove event during write operation
  577.                 try
  578.                 {
  579.                     // even though this is technically a write operation, there's no reason to check reenterancy since it won't ever call handler
  580.                     // in fact, removing handlers in the callback could be a useful scenario
  581.                     _propertyChanged.remove(value);
  582.                     if(_propertyChanged.isEmpty) // if we're now empty, detatch handler
  583.                         ((INotifyPropertyChanged) _collection).PropertyChanged -= onPropertyChangedInternal;
  584.                 }
  585.                 finally
  586.                 {
  587.                     _lock.ExitWriteLock();
  588.                 }
  589.             }
  590.         }
  591.  
  592.         private void dispatchWaitingEvents(ThreadView view)
  593.         {
  594.             List<EventArgs> waitingEvents = view.waitingEvents;
  595.             try
  596.             {
  597.                 if(waitingEvents.Count == 0) return; // fast path for no events
  598.                 if(view.dissalowReenterancy)
  599.                 {
  600.                     // Write methods should have checked this before we got here. Since we didn't that means there's a bugg in this class
  601.                     // itself. However, we can't dispatch the events anyways, so we'll have to throw an exception.
  602.                     if(Debugger.IsAttached)
  603.                         Debugger.Break();
  604.                     throwReenterancyException();
  605.                 }
  606.                 view.dissalowReenterancy = true;
  607.                 foreach(EventArgs args in waitingEvents)
  608.                 {
  609.                     NotifyCollectionChangedEventArgs ccArgs = args as NotifyCollectionChangedEventArgs;
  610.                     if(ccArgs != null)
  611.                     {
  612.                         _collectionChanged.raise(this, ccArgs);
  613.                     }
  614.                     else
  615.                     {
  616.                         PropertyChangedEventArgs pcArgs = args as PropertyChangedEventArgs;
  617.                         if(pcArgs != null)
  618.                         {
  619.                             _propertyChanged.raise(this, pcArgs);
  620.                         }
  621.                     }
  622.                 }
  623.             }
  624.             finally
  625.             {
  626.                 view.dissalowReenterancy = false;
  627.                 waitingEvents.Clear();
  628.             }
  629.         }
  630.  
  631.         private static void throwReenterancyException()
  632.         {
  633.             throw new InvalidOperationException("ObservableCollectionReentrancyNotAllowed -- don't modify the collection during callbacks from it!");
  634.         }
  635.         #endregion
  636.  
  637.         #region Methods to make interfaces happy -- most of these just foreward to the appropriate methods above
  638.         IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); }
  639.         void IList.Remove(object value) { Remove((T) value); }
  640.         object IList.this[int index] { get { return this[index]; } set { this[index] = (T) value; } }
  641.         void IList.Insert(int index, object value) { Insert(index, (T) value); }
  642.         bool ICollection<T>.IsReadOnly { get { return false; } }
  643.         bool IList.IsReadOnly { get { return false; } }
  644.         bool IList.IsFixedSize { get { return false; } }
  645.         bool IList.Contains(object value) { return Contains((T) value); }
  646.         object ICollection.SyncRoot { get { throw new NotSupportedException("AsyncObservableCollection doesn't need external synchronization"); } }
  647.         bool ICollection.IsSynchronized { get { return false; } }
  648.         void ICollection.CopyTo(Array array, int index) { CopyTo((T[]) array, index); }
  649.         int IList.IndexOf(object value)  { return IndexOf((T) value); }
  650.         #endregion
  651.  
  652.         #region Serialization
  653.         /// <summary>
  654.         /// Constructor is only here for serialization, you should use the default constructor instead.
  655.         /// </summary>
  656.         public AsyncObservableCollection(SerializationInfo info, StreamingContext context)
  657.             : this((T[]) info.GetValue("values", typeof(T[])))
  658.         {
  659.         }
  660.  
  661.         void ISerializable.GetObjectData(SerializationInfo info, StreamingContext context)
  662.         {
  663.             info.AddValue("values", ToArray(), typeof(T[]));
  664.         }
  665.         #endregion
  666.     }
  667.  
  668.         /// <summary>
  669.     /// Wrapper around an event so that any events added from a Dispatcher thread are invoked on that thread. This means
  670.     /// that if the UI adds an event and that event is called on a different thread, the callback will be dispatched
  671.     /// to the UI thread and called asynchronously. If an event is added from a non-dispatcher thread, or the event
  672.     /// is raised from within the same thread as it was added from, it will be called normally.
  673.     ///
  674.     /// Note that this means that the callback will be asynchronous and may happen at some time in the future rather than as
  675.     /// soon as the event is raised.
  676.     ///
  677.     /// Example usage:
  678.     /// -----------
  679.     ///
  680.     ///     private readonly AsyncDispatcherEvent{PropertyChangedEventHandler, PropertyChangedEventArgs} _propertyChanged =
  681.     ///        new DispatcherEventHelper{PropertyChangedEventHandler, PropertyChangedEventArgs}();
  682.     ///
  683.     ///     public event PropertyChangedEventHandler PropertyChanged
  684.     ///     {
  685.     ///         add { _propertyChanged.add(value); }
  686.     ///         remove { _propertyChanged.remove(value); }
  687.     ///     }
  688.     ///    
  689.     ///     private void OnPropertyChanged(PropertyChangedEventArgs args)
  690.     ///     {
  691.     ///         _propertyChanged.invoke(this, args);
  692.     ///     }
  693.     ///
  694.     /// This class is thread-safe.
  695.     /// </summary>
  696.     /// <typeparam name="TEvent">The delagate type to wrap (ie PropertyChangedEventHandler). Must have a void delegate(object, TArgs) signature.</typeparam>
  697.     /// <typeparam name="TArgs">Second argument of the TEvent. Must be of type EventArgs.</typeparam>
  698.     public sealed class AsyncDispatcherEvent<TEvent, TArgs> where TEvent : class where TArgs : EventArgs
  699.     {
  700.         /// <summary>
  701.         /// Type of a delegate that invokes a delegate. Okay, that sounds weird, but basically, calling this
  702.         /// with a delegate and its arguments will call the Invoke() method on the delagate itself with those
  703.         /// arguments.
  704.         /// </summary>
  705.         private delegate void InvokeMethod(TEvent @event, object sender, TArgs args);
  706.  
  707.         /// <summary>
  708.         /// Method to invoke the given delegate with the given arguments quickly. It uses reflection once (per type)
  709.         /// to create this, then it's blazing fast to call because the JIT knows everything is type-safe.
  710.         /// </summary>
  711.         private static readonly InvokeMethod _invoke;
  712.  
  713.         /// <summary>
  714.         /// Using List{DelegateWrapper} and locking it on every access is what scrubs would do.
  715.         /// </summary>
  716.         private event EventHandler<TArgs> _event;
  717.  
  718.         /// <summary>
  719.         /// Barely worth worrying about this corner case, but we need to lock on removes in case two identical non-dispatcher
  720.         /// events are being removed at once.
  721.         /// </summary>
  722.         private readonly object _removeLock = new object();
  723.  
  724.         /// <summary>
  725.         /// This is absolutely required to have a static constructor, otherwise it would be beforefieldinit which means
  726.         /// that any type exceptions would be delayed until it's actually called. We can also do some extra checks here to
  727.         /// make sure the types are correct.
  728.         /// </summary>
  729.         static AsyncDispatcherEvent()
  730.         {
  731.             Type tEvent = typeof(TEvent);
  732.             Type tArgs = typeof(TArgs);
  733.             if(!tEvent.IsSubclassOf(typeof(MulticastDelegate)))
  734.                 throw new InvalidOperationException("TEvent " + tEvent.Name + " is not a subclass of MulticastDelegate");
  735.             MethodInfo method = tEvent.GetMethod("Invoke", BindingFlags.Instance | BindingFlags.Public | BindingFlags.DeclaredOnly);
  736.             if(method == null)
  737.                 throw new InvalidOperationException("Could not find method Invoke() on TEvent " + tEvent.Name);
  738.             if(method.ReturnType != typeof(void))
  739.                 throw new InvalidOperationException("TEvent " + tEvent.Name + " must have return type of void");
  740.             ParameterInfo[] paramz = method.GetParameters();
  741.             if(paramz.Length != 2)
  742.                 throw new InvalidOperationException("TEvent " + tEvent.Name + " must have 2 parameters");
  743.             if(paramz[0].ParameterType != typeof(object))
  744.                 throw new InvalidOperationException("TEvent " + tEvent.Name + " must have first parameter of type object, instead was " + paramz[0].ParameterType.Name);
  745.             if(paramz[1].ParameterType != tArgs)
  746.                 throw new InvalidOperationException("TEvent " + tEvent.Name + " must have second paramater of type TArgs " + tArgs.Name + ", instead was " + paramz[1].ParameterType.Name);
  747.             _invoke = (InvokeMethod) method.CreateDelegate(typeof(InvokeMethod));
  748.             if(_invoke == null)
  749.                 throw new InvalidOperationException("CreateDelegate() returned null");
  750.         }
  751.  
  752.         /// <summary>
  753.         /// Adds the delegate to the event.
  754.         /// </summary>
  755.         public void add(TEvent value)
  756.         {
  757.             if(value == null)
  758.                 return;
  759.             _event += (new DelegateWrapper(getDispatcherOrNull(), value)).invoke;
  760.         }
  761.  
  762.         /// <summary>
  763.         /// Removes the last instance of delegate from the event (if it exists). Only removes events that were added from the current
  764.         /// dispatcher thread (if they were added from one), so make sure to remove from the same thread that added.
  765.         /// </summary>
  766.         public void remove(TEvent value)
  767.         {
  768.             if(value == null)
  769.                 return;
  770.             Dispatcher dispatcher = getDispatcherOrNull();
  771.             lock(_removeLock) // because events are intrinsically threadsafe, and dispatchers are thread-local, the only time this lock matters is when removing non-dispatcher events
  772.             {
  773.                 EventHandler<TArgs> evt = _event;
  774.                 if(evt != null)
  775.                 {
  776.                     Delegate[] invList = evt.GetInvocationList();
  777.                     for(int i = invList.Length - 1; i >= 0; i--) // Need to go backwards since that's what event -= something does.
  778.                     {
  779.                         DelegateWrapper wrapper = (DelegateWrapper) invList[i].Target;
  780.                         // need to use Equals instead of == for delegates
  781.                         if(wrapper.handler.Equals(value) && wrapper.dispatcher == dispatcher)
  782.                         {
  783.                             _event -= wrapper.invoke;
  784.                             return;
  785.                         }
  786.                     }
  787.                 }
  788.             }
  789.         }
  790.  
  791.         /// <summary>
  792.         /// Checks if any delegate has been added to this event.
  793.         /// </summary>
  794.         public bool isEmpty
  795.         {
  796.             get
  797.             {
  798.                 return _event == null;
  799.             }
  800.         }
  801.  
  802.         /// <summary>
  803.         /// Calls the event.
  804.         /// </summary>
  805.         public void raise(object sender, TArgs args)
  806.         {
  807.             EventHandler<TArgs> evt = _event;
  808.             if(evt != null)
  809.                 evt(sender, args);
  810.         }
  811.  
  812.         private static Dispatcher getDispatcherOrNull()
  813.         {
  814.             return Dispatcher.FromThread(Thread.CurrentThread);
  815.         }
  816.  
  817.         private sealed class DelegateWrapper
  818.         {
  819.             public readonly TEvent handler;
  820.             public readonly Dispatcher dispatcher;
  821.  
  822.             public DelegateWrapper(Dispatcher dispatcher, TEvent handler)
  823.             {
  824.                 this.dispatcher = dispatcher;
  825.                 this.handler = handler;
  826.             }
  827.  
  828.             public void invoke(object sender, TArgs args)
  829.             {
  830.                 if(dispatcher == null || dispatcher == getDispatcherOrNull())
  831.                     _invoke(handler, sender, args);
  832.                 else
  833.                     // ReSharper disable once AssignNullToNotNullAttribute
  834.                     dispatcher.BeginInvoke(handler as Delegate, DispatcherPriority.DataBind, sender, args);
  835.             }
  836.         }
  837.     }
  838. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement