· 6 years ago · Apr 06, 2019, 10:20 PM
1using System;
2using System.Collections;
3using System.Collections.Generic;
4using System.Collections.ObjectModel;
5using System.Collections.Specialized;
6using System.ComponentModel;
7using System.Diagnostics;
8using System.Reflection;
9using System.Runtime.Serialization;
10using System.Threading;
11using System.Windows.Threading;
12
13namespace 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}