PageRenderTime 53ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/ReactiveUI/ReactiveCollection.cs

https://github.com/bsiegel/ReactiveUI
C# | 830 lines | 562 code | 151 blank | 117 comment | 107 complexity | e2a06b4203664e29812acaad6b24f32f MD5 | raw file
Possible License(s): Apache-2.0, CC-BY-SA-3.0, LGPL-2.0
  1. using System;
  2. using System.Collections;
  3. using System.Reactive;
  4. using System.Reactive.Linq;
  5. using System.Reactive.Concurrency;
  6. using System.Collections.Generic;
  7. using System.Collections.ObjectModel;
  8. using System.ComponentModel;
  9. using System.Collections.Specialized;
  10. using System.Linq;
  11. using System.Reactive.Linq;
  12. using System.Reactive.Subjects;
  13. using System.Runtime.Serialization;
  14. using System.Diagnostics.Contracts;
  15. using System.Threading;
  16. using System.Reactive.Disposables;
  17. using System.Globalization;
  18. namespace ReactiveUI
  19. {
  20. public class ReactiveCollection<T> : IList<T>, IList, IReactiveCollection<T>, INotifyPropertyChanging, INotifyPropertyChanged
  21. {
  22. public event NotifyCollectionChangedEventHandler CollectionChanging;
  23. public event NotifyCollectionChangedEventHandler CollectionChanged;
  24. public event PropertyChangingEventHandler PropertyChanging;
  25. public event PropertyChangedEventHandler PropertyChanged;
  26. [field: IgnoreDataMember]
  27. bool rxObjectsSetup = false;
  28. [IgnoreDataMember] Subject<NotifyCollectionChangedEventArgs> _changing;
  29. [IgnoreDataMember] Subject<NotifyCollectionChangedEventArgs> _changed;
  30. [DataMember] List<T> _inner;
  31. [IgnoreDataMember] int _suppressionRefCount = 0;
  32. [IgnoreDataMember] Lazy<Subject<T>> _beforeItemsAdded;
  33. [IgnoreDataMember] Lazy<Subject<T>> _itemsAdded;
  34. [IgnoreDataMember] Lazy<Subject<T>> _beforeItemsRemoved;
  35. [IgnoreDataMember] Lazy<Subject<T>> _itemsRemoved;
  36. [IgnoreDataMember] Lazy<Subject<IObservedChange<T, object>>> _itemChanging;
  37. [IgnoreDataMember] Lazy<Subject<IObservedChange<T, object>>> _itemChanged;
  38. [IgnoreDataMember] Dictionary<object, RefcountDisposeWrapper> _propertyChangeWatchers = null;
  39. [IgnoreDataMember] int _resetSubCount = 0;
  40. static bool _hasWhinedAboutNoResetSub = false;
  41. // NB: This exists so the serializer doesn't whine
  42. //
  43. // 2nd NB: VB.NET doesn't deal well with default parameters, create
  44. // some overloads so people can continue to make bad life choices instead
  45. // of using C#
  46. public ReactiveCollection() { setupRx(); }
  47. public ReactiveCollection(IEnumerable<T> initialContents) { setupRx(initialContents); }
  48. public ReactiveCollection(IEnumerable<T> initialContents = null, IScheduler scheduler = null, double resetChangeThreshold = 0.3)
  49. {
  50. setupRx(initialContents, scheduler, resetChangeThreshold);
  51. }
  52. [OnDeserialized]
  53. #if WP7
  54. public
  55. #endif
  56. void setupRx(StreamingContext _) { setupRx(); }
  57. void setupRx(IEnumerable<T> initialContents = null, IScheduler scheduler = null, double resetChangeThreshold = 0.3)
  58. {
  59. if (rxObjectsSetup) return;
  60. scheduler = scheduler ?? RxApp.DeferredScheduler;
  61. _inner = _inner ?? new List<T>();
  62. _changing = new Subject<NotifyCollectionChangedEventArgs>();
  63. _changing.Where(_ => CollectionChanging != null && _suppressionRefCount == 0).Subscribe(x => CollectionChanging(this, x));
  64. _changed = new Subject<NotifyCollectionChangedEventArgs>();
  65. _changed.Where(_ => CollectionChanged != null && _suppressionRefCount == 0).Subscribe(x => CollectionChanged(this, x));
  66. ResetChangeThreshold = resetChangeThreshold;
  67. _beforeItemsAdded = new Lazy<Subject<T>>(() => new Subject<T>());
  68. _itemsAdded = new Lazy<Subject<T>>(() => new Subject<T>());
  69. _beforeItemsRemoved = new Lazy<Subject<T>>(() => new Subject<T>());
  70. _itemsRemoved = new Lazy<Subject<T>>(() => new Subject<T>());
  71. _itemChanging = new Lazy<Subject<IObservedChange<T, object>>>(() => new Subject<IObservedChange<T, object>>());
  72. _itemChanged = new Lazy<Subject<IObservedChange<T, object>>>(() => new Subject<IObservedChange<T, object>>());
  73. // NB: We have to do this instead of initializing _inner so that
  74. // Collection<T>'s accounting is correct
  75. foreach (var item in initialContents ?? Enumerable.Empty<T>()) { Add(item); }
  76. // NB: ObservableCollection has a Secret Handshake with WPF where
  77. // they fire an INPC notification with the token "Item[]". Emulate
  78. // it here
  79. CollectionCountChanging.Subscribe(_ => {
  80. if (PropertyChanging != null) PropertyChanging(this, new PropertyChangingEventArgs("Count"));
  81. });
  82. CollectionCountChanged.Subscribe(_ => {
  83. if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Count"));
  84. });
  85. Changing.Subscribe(_ => {
  86. if (PropertyChanging != null) PropertyChanging(this, new PropertyChangingEventArgs("Item[]"));
  87. });
  88. Changed.Subscribe(_ => {
  89. if (PropertyChanged != null) PropertyChanged(this, new PropertyChangedEventArgs("Item[]"));
  90. });
  91. rxObjectsSetup = true;
  92. }
  93. /*
  94. * Collection<T> core methods
  95. */
  96. protected void InsertItem(int index, T item)
  97. {
  98. if (_suppressionRefCount > 0) {
  99. _inner.Insert(index, item);
  100. if (ChangeTrackingEnabled) addItemToPropertyTracking(item);
  101. return;
  102. }
  103. var ea = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item, index);
  104. _changing.OnNext(ea);
  105. if (_beforeItemsAdded.IsValueCreated) _beforeItemsAdded.Value.OnNext(item);
  106. _inner.Insert(index, item);
  107. _changed.OnNext(ea);
  108. if (_itemsAdded.IsValueCreated) _itemsAdded.Value.OnNext(item);
  109. if (ChangeTrackingEnabled) addItemToPropertyTracking(item);
  110. }
  111. protected void RemoveItem(int index)
  112. {
  113. var item = _inner[index];
  114. if (_suppressionRefCount > 0) {
  115. _inner.RemoveAt(index);
  116. if (ChangeTrackingEnabled) removeItemFromPropertyTracking(item);
  117. return;
  118. }
  119. var ea = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, item, index);
  120. _changing.OnNext(ea);
  121. if (_beforeItemsRemoved.IsValueCreated) _beforeItemsRemoved.Value.OnNext(item);
  122. _inner.RemoveAt(index);
  123. _changed.OnNext(ea);
  124. if (_itemsRemoved.IsValueCreated) _itemsRemoved.Value.OnNext(item);
  125. if (ChangeTrackingEnabled) removeItemFromPropertyTracking(item);
  126. }
  127. protected void SetItem(int index, T item)
  128. {
  129. if (_suppressionRefCount > 0) {
  130. _inner[index] = item;
  131. if (ChangeTrackingEnabled) {
  132. removeItemFromPropertyTracking(_inner[index]);
  133. addItemToPropertyTracking(item);
  134. }
  135. return;
  136. }
  137. var ea = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, _inner[index], index);
  138. _changing.OnNext(ea);
  139. if (ChangeTrackingEnabled) {
  140. removeItemFromPropertyTracking(_inner[index]);
  141. addItemToPropertyTracking(item);
  142. }
  143. _inner[index] = item;
  144. _changed.OnNext(ea);
  145. }
  146. protected void ClearItems()
  147. {
  148. if (_suppressionRefCount > 0) {
  149. _inner.Clear();
  150. if (ChangeTrackingEnabled) clearAllPropertyChangeWatchers();
  151. return;
  152. }
  153. var ea = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
  154. _changing.OnNext(ea);
  155. _inner.Clear();
  156. _changed.OnNext(ea);
  157. if (ChangeTrackingEnabled) clearAllPropertyChangeWatchers();
  158. }
  159. /*
  160. * List<T> methods we can make faster by possibly sending ShouldReset
  161. * notifications instead of thrashing the UI by readding items
  162. * one at a time
  163. */
  164. public double ResetChangeThreshold { get; set; }
  165. public void AddRange(IEnumerable<T> collection)
  166. {
  167. InsertRange(_inner.Count, collection);
  168. }
  169. public void InsertRange(int index, IEnumerable<T> collection)
  170. {
  171. var arr = collection.ToArray();
  172. var disp = isLengthAboveResetThreshold(arr.Length) ?
  173. SuppressChangeNotifications() : Disposable.Empty;
  174. using (disp) {
  175. // NB: If we don't do this, we'll break Collection<T>'s
  176. // accounting of the length
  177. for (int i = arr.Length - 1; i >= 0; i--) {
  178. InsertItem(index, arr[i]);
  179. }
  180. }
  181. }
  182. public void RemoveRange(int index, int count)
  183. {
  184. var disp = isLengthAboveResetThreshold(count) ?
  185. SuppressChangeNotifications() : Disposable.Empty;
  186. using (disp) {
  187. // NB: If we don't do this, we'll break Collection<T>'s
  188. // accounting of the length
  189. for (int i = count; i > 0; i--) {
  190. RemoveItem(index);
  191. }
  192. }
  193. }
  194. public void RemoveAll(IEnumerable<T> items)
  195. {
  196. Contract.Requires(items != null);
  197. var disp = isLengthAboveResetThreshold(items.Count()) ?
  198. SuppressChangeNotifications() : Disposable.Empty;
  199. using (disp) {
  200. // NB: If we don't do this, we'll break Collection<T>'s
  201. // accounting of the length
  202. foreach (var v in items) {
  203. Remove(v);
  204. }
  205. }
  206. }
  207. public void Sort(int index, int count, IComparer<T> comparer)
  208. {
  209. _inner.Sort(index, count, comparer);
  210. Reset();
  211. }
  212. public void Sort(Comparison<T> comparison)
  213. {
  214. _inner.Sort(comparison);
  215. Reset();
  216. }
  217. public void Sort(IComparer<T> comparer = null)
  218. {
  219. _inner.Sort(comparer ?? Comparer<T>.Default);
  220. Reset();
  221. }
  222. public void Reset()
  223. {
  224. var ea = new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset);
  225. _changing.OnNext(ea);
  226. _changed.OnNext(ea);
  227. }
  228. bool isLengthAboveResetThreshold(int toChangeLength)
  229. {
  230. return (double) toChangeLength/_inner.Count > ResetChangeThreshold &&
  231. toChangeLength > 10;
  232. }
  233. /*
  234. * IReactiveCollection<T>
  235. */
  236. public bool ChangeTrackingEnabled {
  237. get { return _propertyChangeWatchers != null; }
  238. set {
  239. if (_propertyChangeWatchers != null && value) return;
  240. if (_propertyChangeWatchers == null && !value) return;
  241. if (value) {
  242. _propertyChangeWatchers = new Dictionary<object, RefcountDisposeWrapper>();
  243. foreach (var item in _inner) { addItemToPropertyTracking(item); }
  244. } else {
  245. clearAllPropertyChangeWatchers();
  246. _propertyChangeWatchers = null;
  247. }
  248. }
  249. }
  250. public IDisposable SuppressChangeNotifications()
  251. {
  252. Interlocked.Increment(ref _suppressionRefCount);
  253. if (!_hasWhinedAboutNoResetSub && _resetSubCount == 0) {
  254. LogHost.Default.Warn("SuppressChangeNotifications was called (perhaps via AddRange), yet you do not");
  255. LogHost.Default.Warn("have a subscription to ShouldReset. This probably isn't what you want, as ItemsAdded");
  256. LogHost.Default.Warn("and friends will appear to 'miss' items");
  257. _hasWhinedAboutNoResetSub = true;
  258. }
  259. return Disposable.Create(() => {
  260. if (Interlocked.Decrement(ref _suppressionRefCount) == 0) {
  261. Reset();
  262. }
  263. });
  264. }
  265. public IObservable<T> BeforeItemsAdded { get { return _beforeItemsAdded.Value; } }
  266. public IObservable<T> ItemsAdded { get { return _itemsAdded.Value; } }
  267. public IObservable<T> BeforeItemsRemoved { get { return _beforeItemsRemoved.Value; } }
  268. public IObservable<T> ItemsRemoved { get { return _itemsRemoved.Value; } }
  269. public IObservable<IObservedChange<T, object>> ItemChanging { get { return _itemChanging.Value; } }
  270. public IObservable<IObservedChange<T, object>> ItemChanged { get { return _itemChanged.Value; } }
  271. IObservable<object> IReactiveCollection.BeforeItemsAdded { get { return BeforeItemsAdded.Select(x => (object) x); } }
  272. IObservable<object> IReactiveCollection.ItemsAdded { get { return ItemsAdded.Select(x => (object) x); } }
  273. IObservable<object> IReactiveCollection.BeforeItemsRemoved { get { return BeforeItemsRemoved.Select(x => (object) x); } }
  274. IObservable<object> IReactiveCollection.ItemsRemoved { get { return ItemsRemoved.Select(x => (object) x); } }
  275. IObservable<IObservedChange<object, object>> IReactiveCollection.ItemChanging {
  276. get {
  277. return _itemChanging.Value.Select(x => (IObservedChange<object, object>) new ObservedChange<object, object>() {
  278. Sender = x.Sender,
  279. PropertyName = x.PropertyName,
  280. Value = x.Value,
  281. });
  282. }
  283. }
  284. IObservable<IObservedChange<object, object>> IReactiveCollection.ItemChanged {
  285. get {
  286. return _itemChanged.Value.Select(x => (IObservedChange<object, object>) new ObservedChange<object, object>() {
  287. Sender = x.Sender,
  288. PropertyName = x.PropertyName,
  289. Value = x.Value,
  290. });
  291. }
  292. }
  293. public IObservable<int> CollectionCountChanging {
  294. get { return _changing.Select(_ => _inner.Count).DistinctUntilChanged(); }
  295. }
  296. public IObservable<int> CollectionCountChanged {
  297. get { return _changed.Select(_ => _inner.Count).DistinctUntilChanged(); }
  298. }
  299. public IObservable<bool> IsEmpty {
  300. get { return _changed.Select(_ => _inner.Count == 0).DistinctUntilChanged(); }
  301. }
  302. public IObservable<NotifyCollectionChangedEventArgs> Changing {
  303. get { return _changing; }
  304. }
  305. public IObservable<NotifyCollectionChangedEventArgs> Changed {
  306. get { return _changed; }
  307. }
  308. public IObservable<Unit> ShouldReset {
  309. get {
  310. return refcountSubscribers(_changed.SelectMany(x =>
  311. x.Action != NotifyCollectionChangedAction.Reset ?
  312. Observable.Empty<Unit>() :
  313. Observable.Return(Unit.Default)), x => _resetSubCount += x);
  314. }
  315. }
  316. /*
  317. * Property Change Tracking
  318. */
  319. void addItemToPropertyTracking(T toTrack)
  320. {
  321. var item = toTrack as IReactiveNotifyPropertyChanged;
  322. if (item == null)
  323. return;
  324. if (_propertyChangeWatchers.ContainsKey(toTrack)) {
  325. _propertyChangeWatchers[toTrack].AddRef();
  326. return;
  327. }
  328. var toDispose = new[] {
  329. item.Changing.Where(_ => _suppressionRefCount == 0).Subscribe(beforeChange =>
  330. _itemChanging.Value.OnNext(new ObservedChange<T, object>() {
  331. Sender = toTrack, PropertyName = beforeChange.PropertyName })),
  332. item.Changed.Where(_ => _suppressionRefCount == 0).Subscribe(change =>
  333. _itemChanged.Value.OnNext(new ObservedChange<T,object>() {
  334. Sender = toTrack, PropertyName = change.PropertyName })),
  335. };
  336. _propertyChangeWatchers.Add(toTrack,
  337. new RefcountDisposeWrapper(Disposable.Create(() => {
  338. toDispose[0].Dispose(); toDispose[1].Dispose();
  339. _propertyChangeWatchers.Remove(toTrack);
  340. })));
  341. }
  342. void removeItemFromPropertyTracking(T toUntrack)
  343. {
  344. _propertyChangeWatchers[toUntrack].Release();
  345. }
  346. void clearAllPropertyChangeWatchers()
  347. {
  348. while (_propertyChangeWatchers.Count > 0) _propertyChangeWatchers.Values.First().Release();
  349. }
  350. static IObservable<TObs> refcountSubscribers<TObs>(IObservable<TObs> input, Action<int> block)
  351. {
  352. return Observable.Create<TObs>(subj => {
  353. block(1);
  354. return new CompositeDisposable(
  355. input.Subscribe(subj),
  356. Disposable.Create(() => block(-1)));
  357. });
  358. }
  359. #region Super Boring IList crap
  360. public IEnumerator<T> GetEnumerator()
  361. {
  362. return _inner.GetEnumerator();
  363. }
  364. IEnumerator IEnumerable.GetEnumerator()
  365. {
  366. return GetEnumerator();
  367. }
  368. public void Add(T item)
  369. {
  370. InsertItem(_inner.Count, item);
  371. }
  372. public void Clear()
  373. {
  374. ClearItems();
  375. }
  376. public bool Contains(T item)
  377. {
  378. return _inner.Contains(item);
  379. }
  380. public void CopyTo(T[] array, int arrayIndex)
  381. {
  382. _inner.CopyTo(array, arrayIndex);
  383. }
  384. public bool Remove(T item)
  385. {
  386. int index = _inner.IndexOf(item);
  387. if (index < 0) return false;
  388. RemoveItem(index);
  389. return true;
  390. }
  391. public int Count { get { return _inner.Count; } }
  392. public bool IsReadOnly { get { return false; } }
  393. public int IndexOf(T item)
  394. {
  395. return _inner.IndexOf(item);
  396. }
  397. public void Insert(int index, T item)
  398. {
  399. InsertItem(index, item);
  400. }
  401. public void RemoveAt(int index)
  402. {
  403. RemoveItem(index);
  404. }
  405. public T this[int index] {
  406. get { return _inner[index]; }
  407. set { SetItem(index, value); }
  408. }
  409. public int Add(object value)
  410. {
  411. Add((T)value);
  412. return Count - 1;
  413. }
  414. public bool Contains(object value)
  415. {
  416. return IsCompatibleObject(value) && Contains((T)value);
  417. }
  418. public int IndexOf(object value)
  419. {
  420. return IsCompatibleObject(value) ? IndexOf((T)value) : -1;
  421. }
  422. public void Insert(int index, object value)
  423. {
  424. Insert(index, (T)value);
  425. }
  426. public bool IsFixedSize { get { return false; } }
  427. public void Remove(object value)
  428. {
  429. if (IsCompatibleObject(value)) Remove((T)value);
  430. }
  431. object IList.this[int index]
  432. {
  433. get { return this[index]; }
  434. set { this[index] = (T)value; }
  435. }
  436. public void CopyTo(Array array, int index)
  437. {
  438. ((IList)_inner).CopyTo(array, index);
  439. }
  440. public bool IsSynchronized { get { return false; } }
  441. public object SyncRoot { get { return this; } }
  442. private static bool IsCompatibleObject(object value)
  443. {
  444. return ((value is T) || ((value == null) && (default(T) == null)));
  445. }
  446. #endregion
  447. }
  448. public static class ReactiveCollectionMixins
  449. {
  450. /// <summary>
  451. /// Creates a collection based on an an Observable by adding items
  452. /// provided until the Observable completes, optionally ensuring a
  453. /// delay. Note that if the Observable never completes and withDelay is
  454. /// set, this method will leak a Timer. This method also guarantees that
  455. /// items are always added via the UI thread.
  456. /// </summary>
  457. /// <param name="fromObservable">The Observable whose items will be put
  458. /// into the new collection.</param>
  459. /// <param name="onError">The handler for errors from the Observable. If
  460. /// not specified, an error will go to DefaultExceptionHandler.</param>
  461. /// <param name="withDelay">If set, items will be populated in the
  462. /// collection no faster than the delay provided.</param>
  463. /// <returns>A new collection which will be populated with the
  464. /// Observable.</returns>
  465. public static ReactiveCollection<T> CreateCollection<T>(
  466. this IObservable<T> fromObservable,
  467. TimeSpan? withDelay = null,
  468. Action<Exception> onError = null)
  469. {
  470. var ret = new ReactiveCollection<T>();
  471. onError = onError ?? (ex => RxApp.DefaultExceptionHandler.OnNext(ex));
  472. if (withDelay == null) {
  473. fromObservable.ObserveOn(RxApp.DeferredScheduler).Subscribe(ret.Add, onError);
  474. return ret;
  475. }
  476. // On a timer, dequeue items from queue if they are available
  477. var queue = new Queue<T>();
  478. var disconnect = Observable.Timer(withDelay.Value, withDelay.Value, RxApp.DeferredScheduler)
  479. .Subscribe(_ => {
  480. if (queue.Count > 0) {
  481. ret.Add(queue.Dequeue());
  482. }
  483. });
  484. // When new items come in from the observable, stuff them in the queue.
  485. // Using the DeferredScheduler guarantees we'll always access the queue
  486. // from the same thread.
  487. fromObservable.ObserveOn(RxApp.DeferredScheduler).Subscribe(queue.Enqueue, onError);
  488. // This is a bit clever - keep a running count of the items actually
  489. // added and compare them to the final count of items provided by the
  490. // Observable. Combine the two values, and when they're equal,
  491. // disconnect the timer
  492. ret.ItemsAdded.Scan(0, ((acc, _) => acc+1)).Zip(fromObservable.Aggregate(0, (acc,_) => acc+1),
  493. (l,r) => (l == r)).Where(x => x).Subscribe(_ => disconnect.Dispose());
  494. return ret;
  495. }
  496. /// <summary>
  497. /// Creates a collection based on an an Observable by adding items
  498. /// provided until the Observable completes, optionally ensuring a
  499. /// delay. Note that if the Observable never completes and withDelay is
  500. /// set, this method will leak a Timer. This method also guarantees that
  501. /// items are always added via the UI thread.
  502. /// </summary>
  503. /// <param name="fromObservable">The Observable whose items will be put
  504. /// into the new collection.</param>
  505. /// <param name="selector">A Select function that will be run on each
  506. /// item.</param>
  507. /// <param name="withDelay">If set, items will be populated in the
  508. /// collection no faster than the delay provided.</param>
  509. /// <returns>A new collection which will be populated with the
  510. /// Observable.</returns>
  511. public static ReactiveCollection<TRet> CreateCollection<T, TRet>(
  512. this IObservable<T> fromObservable,
  513. Func<T, TRet> selector,
  514. TimeSpan? withDelay = null)
  515. {
  516. Contract.Requires(selector != null);
  517. return fromObservable.Select(selector).CreateCollection(withDelay);
  518. }
  519. }
  520. public static class ObservableCollectionMixin
  521. {
  522. /// <summary>
  523. /// Creates a collection whose contents will "follow" another
  524. /// collection; this method is useful for creating ViewModel collections
  525. /// that are automatically updated when the respective Model collection
  526. /// is updated.
  527. ///
  528. /// Note that even though this method attaches itself to any
  529. /// IEnumerable, it will only detect changes from objects implementing
  530. /// INotifyCollectionChanged (like ReactiveCollection). If your source
  531. /// collection doesn't implement this, signalReset is the way to signal
  532. /// the derived collection to reorder/refilter itself.
  533. /// </summary>
  534. /// <param name="selector">A Select function that will be run on each
  535. /// item.</param>
  536. /// <param name="filter">A filter to determine whether to exclude items
  537. /// in the derived collection.</param>
  538. /// <param name="orderer">A comparator method to determine the ordering of
  539. /// the resulting collection.</param>
  540. /// <param name="signalReset">When this Observable is signalled,
  541. /// the derived collection will be manually
  542. /// reordered/refiltered.</param>
  543. /// <returns>A new collection whose items are equivalent to
  544. /// Collection.Select().Where().OrderBy() and will mirror changes
  545. /// in the initial collection.</returns>
  546. public static ReactiveCollection<TNew> CreateDerivedCollection<T, TNew, TDontCare>(
  547. this IEnumerable<T> This,
  548. Func<T, TNew> selector,
  549. Func<T, bool> filter = null,
  550. Func<TNew, TNew, int> orderer = null,
  551. IObservable<TDontCare> signalReset = null)
  552. {
  553. Contract.Requires(selector != null);
  554. var collChanged = new Subject<NotifyCollectionChangedEventArgs>();
  555. if (selector == null) {
  556. selector = (x => (TNew)Convert.ChangeType(x, typeof(TNew), CultureInfo.CurrentCulture));
  557. }
  558. var origEnum = This;
  559. origEnum = (filter != null ? origEnum.Where(filter) : origEnum);
  560. var enumerable = origEnum.Select(selector);
  561. enumerable = (orderer != null ? enumerable.OrderBy(x => x, new FuncComparator<TNew>(orderer)) : enumerable);
  562. var ret = new ReactiveCollection<TNew>(enumerable);
  563. var incc = This as INotifyCollectionChanged;
  564. if (incc != null) {
  565. ((INotifyCollectionChanged)This).CollectionChanged += (o, e) => collChanged.OnNext(e);
  566. }
  567. if (filter != null && orderer == null) {
  568. throw new Exception("If you specify a filter, you must also specify an ordering function");
  569. }
  570. signalReset.Subscribe(_ => collChanged.OnNext(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)));
  571. collChanged.Subscribe(args => {
  572. if (args.Action == NotifyCollectionChangedAction.Reset) {
  573. using(ret.SuppressChangeNotifications()) {
  574. ret.Clear();
  575. enumerable.ForEach(ret.Add);
  576. }
  577. ret.Reset();
  578. return;
  579. }
  580. int oldIndex = (args.Action == NotifyCollectionChangedAction.Replace ?
  581. args.NewStartingIndex : args.OldStartingIndex);
  582. if (args.OldItems != null) {
  583. // NB: Tracking removes gets hard, because unless the items
  584. // are objects, we have trouble telling them apart. This code
  585. // is also tart, but it works.
  586. foreach(T x in args.OldItems) {
  587. if (filter != null && !filter(x)) {
  588. continue;
  589. }
  590. if (orderer == null) {
  591. ret.RemoveAt(oldIndex);
  592. continue;
  593. }
  594. for(int i = 0; i < ret.Count; i++) {
  595. if (orderer(ret[i], selector(x)) == 0) {
  596. ret.RemoveAt(i);
  597. }
  598. }
  599. }
  600. }
  601. if (args.NewItems != null) {
  602. foreach(T x in args.NewItems) {
  603. if (filter != null && !filter(x)) {
  604. continue;
  605. }
  606. if (orderer == null) {
  607. ret.Insert(args.NewStartingIndex, selector(x));
  608. continue;
  609. }
  610. var toAdd = selector(x);
  611. ret.Insert(positionForNewItem(ret, toAdd, orderer), toAdd);
  612. }
  613. }
  614. });
  615. return ret;
  616. }
  617. /// <summary>
  618. /// Creates a collection whose contents will "follow" another
  619. /// collection; this method is useful for creating ViewModel collections
  620. /// that are automatically updated when the respective Model collection
  621. /// is updated.
  622. ///
  623. /// Be aware that this overload will result in a collection that *only*
  624. /// updates if the source implements INotifyCollectionChanged. If your
  625. /// list changes but isn't a ReactiveCollection/ObservableCollection,
  626. /// you probably want to use the other overload.
  627. /// </summary>
  628. /// <param name="selector">A Select function that will be run on each
  629. /// item.</param>
  630. /// <param name="filter">A filter to determine whether to exclude items
  631. /// in the derived collection.</param>
  632. /// <param name="orderer">A comparator method to determine the ordering of
  633. /// the resulting collection.</param>
  634. /// <returns>A new collection whose items are equivalent to
  635. /// Collection.Select().Where().OrderBy() and will mirror changes
  636. /// in the initial collection.</returns>
  637. public static ReactiveCollection<TNew> CreateDerivedCollection<T, TNew>(
  638. this IEnumerable<T> This,
  639. Func<T, TNew> selector,
  640. Func<T, bool> filter = null,
  641. Func<TNew, TNew, int> orderer = null)
  642. {
  643. return This.CreateDerivedCollection(selector, filter, orderer, Observable.Empty<Unit>());
  644. }
  645. static int positionForNewItem<T>(IList<T> list, T item, Func<T, T, int> orderer)
  646. {
  647. if (list.Count == 0) {
  648. return 0;
  649. }
  650. if (list.Count == 1) {
  651. return orderer(list[0], item) >= 0 ? 1 : 0;
  652. }
  653. // NB: This is the most tart way to do this possible
  654. int? prevCmp = null;
  655. int cmp;
  656. for(int i=0; i < list.Count; i++) {
  657. cmp = orderer(list[i], item);
  658. if (prevCmp.HasValue && cmp != prevCmp) {
  659. return i;
  660. }
  661. prevCmp = cmp;
  662. }
  663. return list.Count;
  664. }
  665. class FuncComparator<T> : IComparer<T>
  666. {
  667. Func<T, T, int> _inner;
  668. public FuncComparator(Func<T, T, int> comparer)
  669. {
  670. _inner = comparer;
  671. }
  672. public int Compare(T x, T y)
  673. {
  674. return _inner(x, y);
  675. }
  676. }
  677. }
  678. }
  679. // vim: tw=120 ts=4 sw=4 et :