ReactiveProperty
repository·main·Indexed 21 days ago
https://github.com/runceel/reactivepropertyA library that brings MVVM and asynchronous support to .NET applications using Reactive Extensions (Rx). It enables declarative ViewModel definitions by bridging standard object properties and IObservable<T> streams, replacing manual property change notifications and command logic. Key features include two-way synchronization with INotifyPropertyChanged, LINQ integration for property streams, and ReactiveCommands driven by IObservable<bool>. Supports .NET 8.0+, .NET Framework 4.7.2, and .NET Standard 2.0.
What's inside ReactiveProperty
- ReactiveProperty is an extension of Reactive Extensions (Rx) designed specifically for the MVVM (Model-View-ViewModel) pattern and asynchronous programming. It provides reactive properties that can be easily integrated into ViewModels to facilitate data binding and reactive state management.
What is ReactiveProperty
mainReactiveProperty is a library designed to support MVVM and asynchronous processing for Reactive Extensions (Rx). It targets .NET Standard 2.0 and allows for declarative programming by bridging the gap between standard object properties and
IObservable<T>streams.Key capabilities include:
- Two-way synchronization: Synchronize
ReactiveProperty<T>with standardINotifyPropertyChangedmodel properties. - LINQ integration: Use Rx operators (like
Where,Select,Delay) to transform property streams. - Reactive Commands: Create
ICommandimplementations (ReactiveCommand) that are driven byIObservable<bool>streams (e.g., enabling a command only when a property meets a certain condition). - Declarative Pipelines: Chain operations from an input
ReactivePropertyto an outputReactivePropertyorReactiveCommandusing method chaining.
// Example: Creating a reactive pipeline from an input property to an output property class ViewModel { public ReactiveProperty<string> Input { get; } public ReactiveProperty<string> Output { get; } public ViewModel() { Input = new ReactiveProperty(""); Output = Input .Delay(TimeSpan.FromSeconds(1)) .Select(x => x.ToUpper()) .ToReactiveProperty(); } }- Two-way synchronization: Synchronize
Overview of ReactiveProperty
mainReactiveProperty is a library providing MVVM and asynchronous support features using Reactive Extensions (Rx). It is designed to simplify ViewModel implementation by using declarative code instead of manual property change notifications and command state management.
Key Characteristics:
- No Base Classes: Unlike many MVVM frameworks, ReactiveProperty does not require your ViewModels to inherit from a specific base class, making it compatible with other libraries like Prism or Microsoft.Toolkit.Mvvm.
- Core Abstractions: The core classes like
ReactiveProperty[Slim]wrap a value and implement bothIObservable<T>(for Rx LINQ chains) andINotifyPropertyChanged(for data binding in WPF, WinUI, and MAUI). - Functional Approach: It encourages a functional programming style where properties are transformed through Rx operators.
Note for New Projects: If you are starting a new application, the author recommends using R3 instead of ReactiveProperty, as R3 is a redesigned version aligned with the current .NET ecosystem.
Use ReactiveProperty.XamarinAndroid for MVVM
mainReactiveProperty.XamarinAndroid is an MVVM support library designed for use with ReactiveProperty in Xamarin.Android environments. It provides data binding capabilities between View properties and ReactiveProperties, collection adapters for Android lists, and extension methods to convert Android View events into Observables.What is ReactiveProperty.R3 and when to use it
mainReactiveProperty.R3is a bridge package designed to fill the functional gaps between the original ReactiveProperty and the R3 ecosystem (includingObservableCollections.R3).When to use it
- During Migration: Use it to provide types that R3 lacks but your existing code requires.
- Post-Migration: You may continue to depend on
ReactiveProperty.R3indefinitely to access specific features that have not yet been implemented in native R3.
Design Principles
- Gap-only: It does not duplicate R3 functionality. If R3 provides a feature (even under a different name), the migration skill will map it to native R3 instead of using this library.
- R3-native shapes: Public surfaces return R3
Observable<T>(notSystem.IObservable<T>), useTimeProviderfor time-based features, and use R3ReactiveProperty<bool>for shared command state. - Minimal dependencies: It depends only on
R3. It does not requireSystem.ReactiveorMessagePipe.
What is ReactiveProperty and its core features
mainThe
ReactiveProperty<T>class is the core component of this library. It is designed to bridge the gap between Reactive Extensions (Rx) and XAML-based data binding.Key features include:
- Implements
INotifyPropertyChanged: TheValueproperty triggersPropertyChangedevents, making it compatible with XAML data binding. - Implements
IObservable<T>: It functions as an observable stream, calling theOnNextmethod whenever the value is updated.
Important distinction:
OnNext(viaSubscribe) is called immediately upon subscription with the current value.PropertyChangedis only triggered when the value is actually changed via theValueproperty and is intended for UI data binding. For logic, you should generally prefer Reactive Extensions methods overPropertyChangedhandlers.
using Reactive.Bindings; using System; // Create a ReactiveProperty var name = new ReactiveProperty<string>(); // Use PropertyChanged for UI/Binding logic name.PropertyChanged += (_, e) => Console.WriteLine($"PropertyChanged: {e.PropertyName}"); // Use Subscribe for Rx-based logic name.Subscribe(x => Console.WriteLine($"OnNext: {x}")); // Updating the value triggers both name.Value = "neuecc";- Implements
What is ReactiveCommand and how to use it
mainThe
ReactiveCommandclass is used for commanding in UI applications. It implements both theICommandinterface (for UI binding) and theIObservable<T>interface (allowing it to be treated as a stream of execution events).Key Behaviors:
- Execution Logic: When
Execute()is called, the command triggers itsOnNextcallback. You register this logic using.Subscribe(). - CanExecute Control: You can control whether a command is enabled or disabled by providing an
IObservable<bool>source. When the source publishes a value, theCanExecuteChangedevent is raised. - Always Executable: If you need a command that is never disabled, use the default constructor.
// Create a command that is always executable var alwaysExecutableCommand = new ReactiveCommand(); // Create a command with a parameter that is always executable var alwaysExecutableAndHasCommandParameterCommand = new ReactiveCommand<string>(); // Register execution logic alwaysExecutableCommand.Subscribe(_ => { /* logic */ }); // Trigger execution alwaysExecutableCommand.Execute();- Execution Logic: When
Manage threading for ReactiveCommand and AsyncReactiveCommand
mainControlling which thread handles
CanExecuteevents is essential for UI stability.For
ReactiveCommand: TheCanExecuteevent is raised on a scheduler (defaulting to the UI thread). To change this, use theToReactiveCommandoverload that accepts anIScheduler:canExecuteSource.ToReactiveCommand(theSchedulerInstanceYouWant);For
AsyncReactiveCommand: This class does not change threads automatically. To specify a thread, use theObserveOnmethod on the source before converting to a command:canExecuteSource.ObserveOn(theSchedulerInstanceYouWant).ToAsyncReactiveCommand();For
ReactiveCommandSlim: This is a lightweight version whereCanExecuteChangedis not automatically dispatched on the UI thread. If you need UI thread dispatching, you must explicitly useObserveOnon theIObservable<bool>source.
Use `ReadOnlyReactiveProperty` for read-only streams
mainWhen a property should only be observed and never manually updated (e.g., a derived value from another stream), use the
ReadOnlyReactiveProperty<T>class. This prevents external callers from setting theValueproperty. You can create one using theToReadOnlyReactiveProperty()extension method on anIObservable<T>.public class ViewModel { public ReactiveProperty<string> Input { get; } public ReadOnlyReactiveProperty<string> Output { get; } public ViewModel() { Input = new ReactiveProperty<string>(""); Output = Input .Delay(TimeSpan.FromSeconds(1)) .Select(x => x.ToUpper()) .ToReadOnlyReactiveProperty(); // Converts IObservable to ReadOnlyReactiveProperty } }Handle UI thread dispatching with ReactivePropertySlim
mainReactivePropertySlim<T>does not automatically dispatch updates to the UI thread. If you are working with UI frameworks that require updates on a specific thread, you must either use the standardReactiveProperty<T>(which handles dispatching) or explicitly dispatch the observable stream using.ObserveOnUIDispatcher()before converting it to a slim property.var rp = Observable.Interval(TimeSpan.FromSeconds(1)) .ObserveOnUIDispatcher() // Explicitly dispatch to the UI thread .ToReadOnlyReactivePropertySlim();Ignore initial validation errors
mainBy default,
ReactivePropertyreports errors for the initial value. If you want to avoid showing errors immediately upon startup, you have two options:Skip(1): Use theSkipoperator on the error observable. Note: On platforms like WPF that useINotifyDataErrorInfo, the UI will still show a red error border because the error was still technically reported.ReactivePropertyMode.IgnoreInitialValidationError: Pass this flag to the constructor. This prevents the error from being reported entirely, so the UI (like WPF) will not show an error state for the initial value.
// Option 1: Skip via Observable (UI might still show error border in WPF) NameErrorMessage = Name.ObserveErrorChanged .Skip(1) .Select(x => x?.OfType<string>()?.FirstOrDefault()) .ToReadOnlyReactiveProperty(); // Option 2: Ignore via Constructor (UI will NOT show error border in WPF) Name = new ReactiveProperty<string>(mode: ReactivePropertyMode.Default | ReactivePropertyMode.IgnoreInitialValidationError) .SetValidateAttribute(() => Name);Await ReactiveProperty and ReactiveCommand
mainYou can use the
awaitoperator withReactiveProperty,ReactivePropertySlim,ReadOnlyReactiveProperty,ReadOnlyReactivePropertySlim, andReactiveCommand. When youawaitthese objects, the program execution pauses until the next value is emitted.Recommended Patterns
While you can
awaitaReactivePropertydirectly, it is recommended to use one of the following methods to ensure proper cancellation support:- For multiple awaits (Looping/Continuous monitoring): Use
GetAsyncHandler(CancellationToken)to obtain anObservableAsyncHandler<T>. This handler can be awaited multiple times without re-allocation. - For a single await: Use
await command.WaitUntilValueChangedAsync(CancellationToken)if you only need to wait for a single value change.
Always pass a
CancellationTokento these methods to ensure that pending awaits are cancelled when the component (e.g., a Window or ViewModel) is disposed or closed.// Pattern 1: Multiple awaits using GetAsyncHandler using (var handler = MyCommand.GetAsyncHandler(closeToken)) { while (true) { await handler; // Waits until the command is executed/value changes // Perform logic here } } // Pattern 2: Single await using WaitUntilValueChangedAsync await myReactiveProperty.WaitUntilValueChangedAsync(closeToken);- For multiple awaits (Looping/Continuous monitoring): Use