/// <summary> /// Creates a collection whose contents will "follow" another /// collection; this method is useful for creating ViewModel collections /// that are automatically updated when the respective Model collection /// is updated. /// </summary> /// <param name="selector">A Select function that will be run on each /// item.</param> /// <returns>A new collection whose items are equivalent to /// Collection.Select(selector) and will mirror the initial collection.</returns> public static ReactiveCollection <TNew> CreateDerivedCollection <T, TNew>( this ObservableCollection <T> This, Func <T, TNew> selector) { Contract.Requires(selector != null); var ret = new ReactiveCollection <TNew>(This.Select(selector)); var coll_changed = new Subject <NotifyCollectionChangedEventArgs>(); This.CollectionChanged += (o, e) => coll_changed.OnNext(e); /* XXX: Ditto as from above * var coll_changed = Observable.FromEvent<NotifyCollectionChangedEventHandler, NotifyCollectionChangedEventArgs>( * x => This.CollectionChanged += x, x => This.CollectionChanged -= x); */ coll_changed.Subscribe(x => { switch (x.Action) { case NotifyCollectionChangedAction.Add: case NotifyCollectionChangedAction.Remove: case NotifyCollectionChangedAction.Replace: // NB: SL4 fills in OldStartingIndex with -1 on Replace :-/ int old_index = (x.Action == NotifyCollectionChangedAction.Replace ? x.NewStartingIndex : x.OldStartingIndex); if (x.OldItems != null) { foreach (object _ in x.OldItems) { ret.RemoveAt(old_index); } } if (x.NewItems != null) { foreach (T item in x.NewItems.Cast <T>()) { ret.Insert(x.NewStartingIndex, selector(item)); } } break; case NotifyCollectionChangedAction.Reset: ret.Clear(); break; default: break; } }); return(ret); }
/// <summary> /// Creates a collection based on an an Observable by adding items /// provided until the Observable completes, optionally ensuring a /// delay. Note that if the Observable never completes and withDelay is /// set, this method will leak a Timer. This method also guarantees that /// items are always added via the UI thread. /// </summary> /// <param name="fromObservable">The Observable whose items will be put /// into the new collection.</param> /// <param name="onError">The handler for errors from the Observable. If /// not specified, an error will go to DefaultExceptionHandler.</param> /// <param name="withDelay">If set, items will be populated in the /// collection no faster than the delay provided.</param> /// <returns>A new collection which will be populated with the /// Observable.</returns> public static ReactiveCollection <T> CreateCollection <T>( this IObservable <T> fromObservable, TimeSpan?withDelay = null, Action <Exception> onError = null) { var ret = new ReactiveCollection <T>(); onError = onError ?? (ex => RxApp.DefaultExceptionHandler.OnNext(ex)); if (withDelay == null) { fromObservable.ObserveOn(RxApp.DeferredScheduler).Subscribe(ret.Add, onError); return(ret); } // On a timer, dequeue items from queue if they are available var queue = new Queue <T>(); var disconnect = Observable.Timer(withDelay.Value, withDelay.Value, RxApp.DeferredScheduler) .Subscribe(_ => { if (queue.Count > 0) { ret.Add(queue.Dequeue()); } }); // When new items come in from the observable, stuff them in the queue. // Using the DeferredScheduler guarantees we'll always access the queue // from the same thread. fromObservable.ObserveOn(RxApp.DeferredScheduler).Subscribe(queue.Enqueue, onError); // This is a bit clever - keep a running count of the items actually // added and compare them to the final count of items provided by the // Observable. Combine the two values, and when they're equal, // disconnect the timer ret.ItemsAdded.Scan(0, ((acc, _) => acc + 1)).Zip(fromObservable.Aggregate(0, (acc, _) => acc + 1), (l, r) => (l == r)).Where(x => x).Subscribe(_ => disconnect.Dispose()); return(ret); }
/// <summary> /// Creates a collection whose contents will "follow" another /// collection; this method is useful for creating ViewModel collections /// that are automatically updated when the respective Model collection /// is updated. /// /// Note that even though this method attaches itself to any /// IEnumerable, it will only detect changes from objects implementing /// INotifyCollectionChanged (like ReactiveCollection). If your source /// collection doesn't implement this, signalReset is the way to signal /// the derived collection to reorder/refilter itself. /// </summary> /// <param name="selector">A Select function that will be run on each /// item.</param> /// <param name="filter">A filter to determine whether to exclude items /// in the derived collection.</param> /// <param name="orderer">A comparator method to determine the ordering of /// the resulting collection.</param> /// <param name="signalReset">When this Observable is signalled, /// the derived collection will be manually /// reordered/refiltered.</param> /// <returns>A new collection whose items are equivalent to /// Collection.Select().Where().OrderBy() and will mirror changes /// in the initial collection.</returns> public static ReactiveCollection <TNew> CreateDerivedCollection <T, TNew, TDontCare>( this IEnumerable <T> This, Func <T, TNew> selector, Func <T, bool> filter = null, Func <TNew, TNew, int> orderer = null, IObservable <TDontCare> signalReset = null) { Contract.Requires(selector != null); var thisAsColl = (IList <T>)This; var collChanged = new Subject <NotifyCollectionChangedEventArgs>(); if (selector == null) { selector = (x => (TNew)Convert.ChangeType(x, typeof(TNew), CultureInfo.CurrentCulture)); } var origEnum = (IEnumerable <T>)thisAsColl; origEnum = (filter != null ? origEnum.Where(filter) : origEnum); var enumerable = origEnum.Select(selector); enumerable = (orderer != null ? enumerable.OrderBy(x => x, new FuncComparator <TNew>(orderer)) : enumerable); var ret = new ReactiveCollection <TNew>(enumerable); var incc = This as INotifyCollectionChanged; if (incc != null) { ((INotifyCollectionChanged)This).CollectionChanged += (o, e) => collChanged.OnNext(e); } if (filter != null && orderer == null) { throw new Exception("If you specify a filter, you must also specify an ordering function"); } signalReset.Subscribe(_ => collChanged.OnNext(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset))); collChanged.Subscribe(args => { if (args.Action == NotifyCollectionChangedAction.Reset) { using (ret.SuppressChangeNotifications()) { ret.Clear(); enumerable.ForEach(ret.Add); } ret.Reset(); return; } int oldIndex = (args.Action == NotifyCollectionChangedAction.Replace ? args.NewStartingIndex : args.OldStartingIndex); if (args.OldItems != null) { // NB: Tracking removes gets hard, because unless the items // are objects, we have trouble telling them apart. This code // is also tart, but it works. foreach (T x in args.OldItems) { if (filter != null && !filter(x)) { continue; } if (orderer == null) { ret.RemoveAt(oldIndex); continue; } for (int i = 0; i < ret.Count; i++) { if (orderer(ret[i], selector(x)) == 0) { ret.RemoveAt(i); } } } } if (args.NewItems != null) { foreach (T x in args.NewItems) { if (filter != null && !filter(x)) { continue; } if (orderer == null) { ret.Insert(args.NewStartingIndex, selector(x)); continue; } var toAdd = selector(x); ret.Insert(positionForNewItem(ret, toAdd, orderer), toAdd); } } }); return(ret); }