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

TopicDynamicData / Rx.NETdxn_reactive
Stream typeIObservable<IChangeSet<...>> with OnError/OnCompletedChangeStream<C> and ValueStream<T>: values only, no terminal events
Errorsexceptions, OnErrorResult from setup and edits; transform_safe* callbacks; CommandState::Failed
Subscription lifetimeIDisposable, CompositeDisposableRAII Subscription; drop to dispose; DisposeBag
SchedulersObserveOn, SubscribeOn, schedulers in time operatorsno 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)
PagingPageRequest is one-basedPageRequest is zero-based
ComparersIComparer<T>, SortExpressionComparerclosures; Comparer<T> = Arc<dyn Fn(&T, &T) -> Ordering>; sort_*_by_key
Cache orderSourceCache is unorderedSourceCache keeps an insertion (or move_key) order and reports indexes
Sorting a cacheSort yields an ordered cache change setsort_keyed_* (keeps keys) or sort_* (list-shaped)
BindingBind(out ReadOnlyObservableCollection<T>)as_observable_list(), as_observable_cache(), bind_to_vec(), plus your UI binding layer
Overloadsmany overloads per operatordistinct method names (transform_with_key, transform_safe_with_previous_forced, …)
Nested editsallowed in some casesNestedUpdate error; writes from subscribers are queued instead
Item disposalDisposeMany on IDisposable itemsdispose_many on DisposeItem items, or dispose_many_with(closure)

#DynamicData API map

#Sources

DynamicDatadxn_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, Refreshthe 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()
CountChangedcount_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, PopulateFrompopulate_into, populate_from*

#Operators

DynamicDatadxn_reactive
Filter(predicate)filter (filter_immutable for immutable caches)
Filter(IObservable<Func<T, bool>>)filter_dynamic
Filter(IObservable<TState>, (state, item) => ...)filter_with_state
FilterOnObservablefilter_on_observable
Transform, TransformAsync, TransformSafe, TransformManytransform*, transform_async*, transform_safe*, transform_many*
Transform(..., forceTransform)transform_with_forced, transform_with_forced_all
TransformOnObservabletransform_on_observable
TransformWithInlineUpdatetransform_inline
Sort, SortAndBindsort_* / sort_keyed_*, then as_observable_list
Page, Virtualise, Toppage*, virtualize*, top, and *_keyed for caches
SortAndPage, SortAndVirtualizesort_and_page_*, sort_and_virtualize_*
Group, GroupWithImmutableState, GroupOnObservablegroup*, group_immutable*, group_on_observable
InnerJoin, LeftJoin, RightJoin, FullJoin, ...JoinManyinner_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
TransformToTreetransform_to_tree, transform_to_tree_with_predicate
AutoRefresh, AutoRefreshOnObservableauto_refresh(|item, key| stream), auto_refresh_buffered
MergeMany, MergeManyChangeSetsmerge_many*, merge_many_list_change_sets, merge_many_cache_change_sets*
WhenValueChanged, WhenPropertyChangedwhen_value_changed*, when_item_value_changed*, when_keyed_value_changed*
SubscribeMany, DisposeMany, AsyncDisposeManysubscribe_many, dispose_many*, async_dispose_many*
OnItemAdded, OnItemRemoved, OnItemUpdated, OnItemRefreshedon_item_added, on_item_removed[_with_unsubscribe], on_item_updated, on_item_refreshed
InvokeEvaluateinvoke_evaluate, invoke_evaluate_with
UpdateIndexupdate_index, update_index_with
IgnoreUpdateWhen, IncludeUpdateWhenignore_update_when, include_update_when, ignore_same_value_update
WhereReasonsAre, WhereReasonsAreNot, SuppressRefreshwhere_reasons_are, where_reasons_are_not, suppress_refresh
ChangeKey, AddKey, RemoveKeychange_key, add_key, remove_key
DistinctValuesdistinct_values
TrueForAll, TrueForAnytrue_for_all*, true_for_any*
QueryWhenChanged, ToCollection, ToSortedCollectionquery_when_changed*, to_collection, to_sorted_collection*
Count, Sum, Avg, Max, Min, StdDevcount, sum_by, avg_by, max_by, min_by, std_dev_by
LimitSizeTo, ExpireAfterlimit_size_to, expire_after* (stream or source)
Switch, RefCountswitch, ref_count
StartWithEmpty, NotEmpty, SkipInitial, DeferUntilLoadedstart_with_empty, not_empty, skip_initial, defer_until_loaded
BufferInitial, Batch (Rx Buffer), BatchIf, BufferIfbuffer_initial, batch, batch_count, batch_if*, buffer_if*
MonitorStatusmonitor_status
CollectUpdateStatscollect_update_stats (feature diagnostics)
OfTypeof_type on AnyItem caches

#ReactiveUI map

ReactiveUIdxn_reactive
[Reactive] property, RaiseAndSetIfChangedSignal<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 / CreateFromTaskCommand::new / AsyncCommand::new
canExecute observablewith_can_execute(ReadSignal<bool>)
IsExecutingis_executing()
ThrownExceptionsthe returned CommandError and CommandState::Failed(message)
MessageBusEventBus<T>
IActivatableViewModel, ViewModelActivatorActivatableViewModel, ViewModelActivator
WhenActivated(disposables => ...)when_activated(|bag| ...)
DisposeWith(disposables)subscription.dispose_with(&bag)
CompositeDisposableDisposeBag or CompositeSubscription
ObserveOn(RxApp.MainThreadScheduler)your UI binding layer

#Behaviors that may surprise you

  • Computed republishes equal results. WhenAnyValue plus DistinctUntilChanged habits carry over: use Computed::derive_distinct or distinct_until_changed().
  • Commands run concurrently by default. Overlapping execute calls each run; choose ExecutionPolicy::IgnoreWhileRunning to refuse them. execute on a disabled command returns CommandError::Disabled instead of doing nothing.
  • count() never emits for an always-empty source, and an empty source’s connect() sends no snapshot. Use start_with(0) or start_with_empty().
  • Page responses follow every field. A change outside the window still emits a response (with an empty change set) so total_count stays current.
  • Nested edits fail instead of recursing. Accessing a source from inside its own edit returns NestedUpdate; concurrent writers on other threads simply wait (on wasm32 they get Contended).
  • 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.