ReactiveUI Extensions
The ReactiveUI Extensions repository contains a collection of useful extensions, helpers, and utilities that complement the core ReactiveUI framework. These extensions provide additional functionality for common scenarios in reactive application development.
Overview
ReactiveUI Extensions includes various packages that extend ReactiveUI's capabilities across different areas:
- Platform-specific helpers
- Additional reactive patterns
- Testing utilities
- Integration helpers
- Community contributions
Available Extensions
Core Extensions
Extensions that work across all platforms and enhance core ReactiveUI functionality.
Reactive Property Extensions
Additional extension methods for working with reactive properties:
// Property chaining helpers
this.WhenAnyValue(x => x.FirstName, x => x.LastName)
.CombineLatest((f, l) => $"{f} {l}")
.ToProperty(this, x => x.FullName, out _fullName);
// Conditional property updates
this.WhenAnyValue(x => x.IsEnabled)
.WhereTrue()
.Subscribe(_ => EnableFeature());
this.WhenAnyValue(x => x.IsEnabled)
.WhereFalse()
.Subscribe(_ => DisableFeature());
Observable Extensions
Enhanced observable operations:
// Retry with delay
observable
.RetryWithDelay(3, TimeSpan.FromSeconds(1))
.Subscribe(result => ProcessResult(result));
// Timeout with fallback
observable
.TimeoutOrDefault(TimeSpan.FromSeconds(5), defaultValue)
.Subscribe(result => HandleResult(result));
// Buffer until condition
observable
.BufferUntil(condition)
.Subscribe(batch => ProcessBatch(batch));
Platform Extensions
WPF Extensions
Additional helpers for WPF applications:
// Dispatcher helpers
observable
.ObserveOnDispatcher()
.Subscribe(UpdateUI);
// Window management
this.WhenClosed()
.Subscribe(_ => Cleanup());
// Dependency property binding
this.BindDependencyProperty(
DependencyPropertyName,
this.WhenAnyValue(x => x.ViewModel.Property));
MAUI Extensions
MAUI-specific utilities:
// Navigation helpers
await this.NavigateToAsync<DetailViewModel>();
await this.NavigateBackAsync();
// Platform-specific code
if (DeviceInfo.Platform == DevicePlatform.iOS)
{
// iOS-specific code
}
// Lifecycle events
this.WhenAppearing()
.Subscribe(_ => LoadData());
this.WhenDisappearing()
.Subscribe(_ => SaveState());
Avalonia Extensions
Avalonia-specific helpers:
// Style helpers
control.ObserveStyleChanges()
.Subscribe(style => ApplyStyle(style));
// Gesture recognition
control.ObserveGestures()
.OfType<TapGesture>()
.Subscribe(gesture => HandleTap(gesture));
Testing Extensions
Test Helpers
Utilities for testing reactive code:
using ReactiveUI.Extensions.Testing;
[Fact]
public async Task ViewModel_ShouldLoadData()
{
// Arrange
var scheduler = new TestScheduler();
var vm = new MyViewModel(scheduler);
// Act
vm.LoadDataCommand.Execute().Subscribe();
scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks);
// Assert
vm.Data.Should().NotBeNull();
}
Mock Extensions
Helpers for creating mocks:
var mockService = MockRepository.Create<IDataService>();
mockService
.SetupObservable(x => x.GetDataAsync())
.Returns(Observable.Return(testData));
Collection Extensions
Enhanced collection operations:
// Reactive collection transformations
var derivedList = sourceList
.Connect()
.Transform(item => new ItemViewModel(item))
.Filter(vm => vm.IsVisible)
.Sort(SortExpressionComparer<ItemViewModel>.Ascending(x => x.Name))
.Bind(out _items)
.Subscribe();
// Collection change tracking
sourceCollection
.ObserveCollectionChanges()
.Subscribe(change => HandleChange(change));
Validation Extensions
Additional validation helpers:
using ReactiveUI.Extensions.Validation;
public class RegisterViewModel : ReactiveValidationObject
{
[Reactive]
private string _email;
[Reactive]
private string _password;
[Reactive]
private string _confirmPassword;
public RegisterViewModel()
{
// Email validation with custom rule
this.ValidationRule(
vm => vm.Email,
email => email.IsValidEmail(),
"Please enter a valid email address");
// Password strength validation
this.ValidationRule(
vm => vm.Password,
password => password.MeetsPasswordRequirements(),
"Password must be at least 8 characters and contain upper, lower, and digits");
// Confirmation match validation
this.ValidationRule(
vm => vm.ConfirmPassword,
vm => vm.Password,
(confirm, password) => confirm == password,
"Passwords must match");
}
}
// Extension method implementations
public static class ValidationExtensions
{
public static bool IsValidEmail(this string email)
{
return Regex.IsMatch(email, @"^[^@\s]+@[^@\s]+\.[^@\s]+$");
}
public static bool MeetsPasswordRequirements(this string password)
{
return password?.Length >= 8 &&
password.Any(char.IsUpper) &&
password.Any(char.IsLower) &&
password.Any(char.IsDigit);
}
}
Command Extensions
Enhanced ReactiveCommand functionality:
// Command with progress
public ReactiveCommand<Unit, Unit> LongRunningCommand { get; }
public MyViewModel()
{
LongRunningCommand = ReactiveCommand.CreateFromTask(async () =>
{
var progress = new Progress<int>(percent =>
ProgressPercentage = percent);
await LongRunningOperation(progress);
});
// Track command execution state
LongRunningCommand.IsExecuting
.ToProperty(this, x => x.IsLoading, out _isLoading);
// Log command errors
LongRunningCommand.ThrownExceptions
.Subscribe(ex => LogError(ex));
}
Logging Extensions
Enhanced logging capabilities:
using ReactiveUI.Extensions.Logging;
public class MyViewModel : ReactiveObject, IEnableLogger
{
public MyViewModel()
{
// Structured logging
this.Log().Info("ViewModel created with {UserId}", userId);
// Performance logging
using (this.Log().MeasurePerformance("LoadData"))
{
LoadData();
}
// Exception logging with context
try
{
PerformOperation();
}
catch (Exception ex)
{
this.Log().ErrorException("Operation failed", ex, new { UserId, Operation = "Save" });
}
}
}
Serialization Extensions
Helpers for serialization:
using ReactiveUI.Extensions.Serialization;
// JSON serialization with reactive properties
public class MyViewModel : ReactiveObject
{
[Reactive]
[JsonProperty("user_name")]
private string _userName;
public string ToJson()
{
return this.SerializeToJson();
}
public static MyViewModel FromJson(string json)
{
return json.DeserializeFromJson<MyViewModel>();
}
}
Scheduler Extensions
Additional scheduler utilities:
// Conditional scheduler selection
var scheduler = isTestMode
? (IScheduler)new TestScheduler()
: RxSchedulers.MainThreadScheduler;
// Throttled scheduler
observable
.ObserveOn(ThrottledScheduler.Default)
.Subscribe(UpdateUI);
// Batch scheduler for efficiency
observable
.ObserveOn(BatchScheduler.Create(TimeSpan.FromMilliseconds(100)))
.Subscribe(ProcessBatch);
Installation
Install extensions via NuGet:
# Core extensions
dotnet add package ReactiveUI.Extensions
# Platform-specific extensions
dotnet add package ReactiveUI.Extensions.WPF
dotnet add package ReactiveUI.Extensions.MAUI
dotnet add package ReactiveUI.Extensions.Avalonia
# Testing extensions
dotnet add package ReactiveUI.Extensions.Testing
Usage with RxAppBuilder
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithReactiveUIExtensions() // Add extensions
.WithViewsFromAssembly(typeof(App).Assembly)
.BuildApp();
Best Practices
- Choose Extensions Wisely: Only include extensions you actually need
- Test Thoroughly: Extensions may have different maturity levels
- Check Compatibility: Ensure extensions are compatible with your ReactiveUI version
- Read Documentation: Each extension may have specific usage patterns
- Contribute Back: Consider contributing your own extensions
Custom Extensions
Creating Your Own Extensions
public static class MyCustomExtensions
{
// Extension method for IObservable
public static IObservable<T> LogAndContinue<T>(
this IObservable<T> source,
string operationName)
{
return source.Do(
item => Debug.WriteLine($"[{operationName}] Item: {item}"),
ex => Debug.WriteLine($"[{operationName}] Error: {ex.Message}"),
() => Debug.WriteLine($"[{operationName}] Completed"));
}
// Extension method for ReactiveObject
public static IDisposable WhenPropertyChanges<T, TProperty>(
this T source,
Expression<Func<T, TProperty>> property,
Action<TProperty> action)
where T : ReactiveObject
{
return source.WhenAnyValue(property)
.Skip(1) // Skip initial value
.Subscribe(action);
}
}
// Usage
observable
.LogAndContinue("DataLoad")
.Subscribe();
this.WhenPropertyChanges(x => x.SelectedItem,
item => LoadDetails(item));
Community Extensions
The ReactiveUI community has created various extensions:
- ReactiveUI.Events.Roslyn: Event-to-observable via Roslyn
- ReactiveUI.AndroidSupport: Legacy Android support helpers
- Custom Platform Helpers: Various community-contributed helpers
Check the Extensions Repository for the latest community contributions.
Migration Guide
From Legacy Extensions
If you're using older extension patterns:
// Old way
RxApp.DependencyResolver.Register(() => new MyService());
// New way with extensions
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithRegistration(locator =>
{
locator.RegisterLazySingleton<IMyService>(() => new MyService());
})
.BuildApp();
Troubleshooting
Extension Not Found
- Verify Package: Ensure the extension package is installed
- Check Namespace: Import the correct namespace
- Update Packages: Extensions may require specific ReactiveUI versions
Conflicts with Core
- Check Priorities: Extensions may override core functionality
- Review Dependencies: Ensure compatible versions
- Explicit Imports: Use full namespace if ambiguous
Contributing
To contribute your own extensions:
- Fork the Extensions Repository
- Create a new package for your extension
- Add comprehensive tests
- Submit a pull request with documentation