Coming from DynamicData and ReactiveUI
A side-by-side map of DynamicData and ReactiveUI concepts, operators and behaviors onto dxn_reactive, and the differences that matter.
If you have built .NET applications with DynamicData and ReactiveUI, most concepts here are familiar. This page maps the vocabulary and highlights what differs. If you have never used either, skip it.
#The same pipeline in both worlds
// C# with DynamicData
var subscription = _people.Connect()
.Filter(this.WhenAnyValue(x => x.SearchText).Select(BuildPredicate))
.Sort(SortExpressionComparer<Person>.Ascending(p => p.Name))
.Page(_pageRequests) // one-based PageRequest
.ObserveOn(RxApp.MainThreadScheduler)
.Bind(out _visiblePeople)
.Subscribe();
use dxn_reactive::collections::prelude::*;
use dxn_reactive::prelude::Signal;
#[derive(Clone, Debug, PartialEq)]
struct Person {
id: u32,
name: String,
}
let people = SourceCache::new(|person: &Person| person.id);
let search_text = Signal::new(String::new());
let page = Signal::new(PageRequest::new(0, 20)); // zero-based
let visible_people = people
.connect()
.filter_with_state(search_text.values(), |text: &String, person: &Person| person.name.contains(text.as_str()))
.sort_keyed_by_key(|person| person.name.clone())
.page_keyed_dynamic(page.values())
.as_observable_list()?; // the view binds this; marshal to the UI thread in the binding
people.add_or_update(Person { id: 1, name: "Ada".into() })?;
assert_eq!(visible_people.len()?, 1);
#Key differences
| Topic | DynamicData / Rx.NET | dxn_reactive |
|---|---|---|
| Stream type | IObservable<IChangeSet<...>> with OnError/OnCompleted | ChangeStream<C> and ValueStream<T>: values only, no terminal events |
| Errors | exceptions, OnError | Result from setup and edits; transform_safe* callbacks; CommandState::Failed |
| Subscription lifetime | IDisposable, CompositeDisposable | RAII Subscription; drop to dispose; DisposeBag |
| Schedulers | ObserveOn, SubscribeOn, schedulers in time operators | no ObserveOn: delivery is synchronous on the writer’s thread and the consumer marshals to the UI thread; time operators take a Scheduler (*_with_scheduler; a shared timer thread by default, ManualScheduler in tests) |
| Paging | PageRequest is one-based | PageRequest is zero-based |
| Comparers | IComparer<T>, SortExpressionComparer | closures; Comparer<T> = Arc<dyn Fn(&T, &T) -> Ordering>; sort_*_by_key |
| Cache order | SourceCache is unordered | SourceCache keeps an insertion (or move_key) order and reports indexes |
| Sorting a cache | Sort yields an ordered cache change set | sort_keyed_* (keeps keys) or sort_* (list-shaped) |
| Binding | Bind(out ReadOnlyObservableCollection<T>) | as_observable_list(), as_observable_cache(), bind_to_vec(), plus your UI binding layer |
| Overloads | many overloads per operator | distinct method names (transform_with_key, transform_safe_with_previous_forced, …) |
| Nested edits | allowed in some cases | NestedUpdate error; writes from subscribers are queued instead |
| Item disposal | DisposeMany on IDisposable items | dispose_many on DisposeItem items, or dispose_many_with(closure) |
#DynamicData API map
#Sources
| DynamicData | dxn_reactive |
|---|---|
new SourceList<T>() | SourceList::<T>::default(), SourceList::new(vec) |
new SourceCache<T, TKey>(keySelector) | SourceCache::new(key_selector) |
Edit(updater => ...) | edit(|updater| ...) |
AddOrUpdate, Remove, RemoveKey, Clear, Refresh | the same names in snake case |
EditDiff(items, comparer) | edit_diff(items), edit_diff_by(items, eq) |
Lookup(key) | lookup(&key) returning Option<T> |
Connect(), Preview() | connect(), preview() |
CountChanged | count_changed() |
Watch(key), WatchValue(key) | watch(key), watch_value(key); also watch_optional |
SuspendNotifications() | suspend_notifications() guard |
AsObservableList(), AsObservableCache() | as_observable_list(), as_observable_cache(key) |
ObservableChangeSet.Create(...) | create_list_change_stream, create_cache_change_stream |
ToObservableChangeSet() | to_list_change_set*, to_cache_change_set* |
PopulateInto, PopulateFrom | populate_into, populate_from* |
#Operators
| DynamicData | dxn_reactive |
|---|---|
Filter(predicate) | filter (filter_immutable for immutable caches) |
Filter(IObservable<Func<T, bool>>) | filter_dynamic |
Filter(IObservable<TState>, (state, item) => ...) | filter_with_state |
FilterOnObservable | filter_on_observable |
Transform, TransformAsync, TransformSafe, TransformMany | transform*, transform_async*, transform_safe*, transform_many* |
Transform(..., forceTransform) | transform_with_forced, transform_with_forced_all |
TransformOnObservable | transform_on_observable |
TransformWithInlineUpdate | transform_inline |
Sort, SortAndBind | sort_* / sort_keyed_*, then as_observable_list |
Page, Virtualise, Top | page*, virtualize*, top, and *_keyed for caches |
SortAndPage, SortAndVirtualize | sort_and_page_*, sort_and_virtualize_* |
Group, GroupWithImmutableState, GroupOnObservable | group*, group_immutable*, group_on_observable |
InnerJoin, LeftJoin, RightJoin, FullJoin, ...JoinMany | inner_join, left_join, right_join, full_join, ..._join_many |
And, Or, Xor, Except (static and dynamic) | and/or/xor/except, *_dynamic on a SourceList of streams |
TransformToTree | transform_to_tree, transform_to_tree_with_predicate |
AutoRefresh, AutoRefreshOnObservable | auto_refresh(|item, key| stream), auto_refresh_buffered |
MergeMany, MergeManyChangeSets | merge_many*, merge_many_list_change_sets, merge_many_cache_change_sets* |
WhenValueChanged, WhenPropertyChanged | when_value_changed*, when_item_value_changed*, when_keyed_value_changed* |
SubscribeMany, DisposeMany, AsyncDisposeMany | subscribe_many, dispose_many*, async_dispose_many* |
OnItemAdded, OnItemRemoved, OnItemUpdated, OnItemRefreshed | on_item_added, on_item_removed[_with_unsubscribe], on_item_updated, on_item_refreshed |
InvokeEvaluate | invoke_evaluate, invoke_evaluate_with |
UpdateIndex | update_index, update_index_with |
IgnoreUpdateWhen, IncludeUpdateWhen | ignore_update_when, include_update_when, ignore_same_value_update |
WhereReasonsAre, WhereReasonsAreNot, SuppressRefresh | where_reasons_are, where_reasons_are_not, suppress_refresh |
ChangeKey, AddKey, RemoveKey | change_key, add_key, remove_key |
DistinctValues | distinct_values |
TrueForAll, TrueForAny | true_for_all*, true_for_any* |
QueryWhenChanged, ToCollection, ToSortedCollection | query_when_changed*, to_collection, to_sorted_collection* |
Count, Sum, Avg, Max, Min, StdDev | count, sum_by, avg_by, max_by, min_by, std_dev_by |
LimitSizeTo, ExpireAfter | limit_size_to, expire_after* (stream or source) |
Switch, RefCount | switch, ref_count |
StartWithEmpty, NotEmpty, SkipInitial, DeferUntilLoaded | start_with_empty, not_empty, skip_initial, defer_until_loaded |
BufferInitial, Batch (Rx Buffer), BatchIf, BufferIf | buffer_initial, batch, batch_count, batch_if*, buffer_if* |
MonitorStatus | monitor_status |
CollectUpdateStats | collect_update_stats (feature diagnostics) |
OfType | of_type on AnyItem caches |
#ReactiveUI map
| ReactiveUI | dxn_reactive |
|---|---|
[Reactive] property, RaiseAndSetIfChanged | Signal<T> with set |
WhenAnyValue(x => x.A) | signal.values() |
WhenAnyValue(x => x.A, x => x.B, ...) | Computed::combine2 / combine3, nested for more inputs, or values().combine_latest(...) |
ObservableAsPropertyHelper (ToProperty) | Computed<T>, or Computed::from_stream(&stream, initial) |
ReactiveCommand.Create / CreateFromTask | Command::new / AsyncCommand::new |
canExecute observable | with_can_execute(ReadSignal<bool>) |
IsExecuting | is_executing() |
ThrownExceptions | the returned CommandError and CommandState::Failed(message) |
MessageBus | EventBus<T> |
IActivatableViewModel, ViewModelActivator | ActivatableViewModel, ViewModelActivator |
WhenActivated(disposables => ...) | when_activated(|bag| ...) |
DisposeWith(disposables) | subscription.dispose_with(&bag) |
CompositeDisposable | DisposeBag or CompositeSubscription |
ObserveOn(RxApp.MainThreadScheduler) | your UI binding layer |
#Behaviors that may surprise you
Computedrepublishes equal results.WhenAnyValueplusDistinctUntilChangedhabits carry over: useComputed::derive_distinctordistinct_until_changed().- Commands run concurrently by default. Overlapping
executecalls each run; chooseExecutionPolicy::IgnoreWhileRunningto refuse them.executeon a disabled command returnsCommandError::Disabledinstead of doing nothing. count()never emits for an always-empty source, and an empty source’sconnect()sends no snapshot. Usestart_with(0)orstart_with_empty().- Page responses follow every field. A change outside the window still emits a response (with an empty change set) so
total_countstays current. - Nested edits fail instead of recursing. Accessing a source from inside its own
editreturnsNestedUpdate; concurrent writers on other threads simply wait (onwasm32they getContended). - There is no
ObserveOn. Nothing is moved to another thread unless an operator documents a timer or worker thread; marshalling is the binding’s job.
Next: Contributing.