Guidelines
Overview
These guidelines provide best practices and recommendations for building modern ReactiveUI applications. Following these patterns will help you create maintainable, testable, and performant reactive applications.
Modern Development Patterns
Use RxAppBuilder for Application Initialization
RxAppBuilder is the modern way to initialize ReactiveUI applications. It provides a fluent API for configuring dependency injection, schedulers, and platform-specific features.
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf() // or .WithMaui(), .WithBlazor(), etc.
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithRegistration(locator =>
{
// Register your services
locator.RegisterLazySingleton<IDataService>(() => new DataService());
locator.RegisterLazySingleton<INavigationService>(() => new NavigationService());
})
.BuildApp();
Benefits:
- Centralized configuration
- Type-safe service registration
- Platform-specific optimizations
- Simplified testing setup
- Clear dependency graph
Use ReactiveUI.SourceGenerators
ReactiveUI.SourceGenerators eliminates boilerplate code through compile-time code generation. Always prefer source generators over manual property implementation or Fody.
Reactive Properties
public partial class MyViewModel : ReactiveObject
{
// Old way (manual)
private string _name;
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
// New way (source generators) ✅
[Reactive]
private string _name = string.Empty;
}
Observable As Property Helper
public partial class MyViewModel : ReactiveObject
{
// Old way (manual)
private readonly ObservableAsPropertyHelper<string> _fullName;
public string FullName => _fullName.Value;
public MyViewModel()
{
_fullName = this.WhenAnyValue(x => x.FirstName, x => x.LastName,
(f, l) => $"{f} {l}")
.ToProperty(this, x => x.FullName);
}
// New way (source generators) ✅
[Reactive]
private string _firstName = string.Empty;
[Reactive]
private string _lastName = string.Empty;
[ObservableAsProperty]
private string _fullName = string.Empty;
public MyViewModel()
{
_fullNameHelper = this.WhenAnyValue(x => x.FirstName, x => x.LastName,
(f, l) => $"{f} {l}")
.ToProperty(this, nameof(FullName));
}
}
Reactive Commands
public partial class MyViewModel : ReactiveObject
{
// Old way (manual)
public ReactiveCommand<Unit, Unit> SaveCommand { get; }
public MyViewModel()
{
SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync);
}
private async Task SaveAsync() { /* ... */ }
// New way (source generators) ✅
[ReactiveCommand]
private async Task Save()
{
// Your save logic
}
}
Benefits:
- Less boilerplate code
- Compile-time validation
- Better IDE support
- Easier to read and maintain
- No IL weaving complexity
Use ObservableEvents for Event Handling
Instead of manually subscribing to events, use ReactiveMarbles.ObservableEvents.SourceGenerator to convert events to observables.
// Old way ❌
button.Click += Button_Click;
protected override void OnClosed(EventArgs e)
{
button.Click -= Button_Click;
}
// New way ✅
this.WhenActivated(disposables =>
{
button.Events().Click
.Throttle(TimeSpan.FromMilliseconds(500))
.Subscribe(_ => HandleClick())
.DisposeWith(disposables);
});
Always Use WhenActivated
WhenActivated ensures proper lifecycle management and prevents memory leaks.
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
this.WhenActivated(disposables =>
{
// All subscriptions here are automatically disposed
this.Bind(ViewModel, vm => vm.Name, v => v.NameTextBox.Text)
.DisposeWith(disposables);
this.BindCommand(ViewModel, vm => vm.SaveCommand, v => v.SaveButton)
.DisposeWith(disposables);
});
}
}
Rules:
- ✅ Always wrap subscriptions in WhenActivated
- ✅ Always call DisposeWith(disposables)
- ❌ Never subscribe without disposing
- ❌ Don't create subscriptions in constructors (outside WhenActivated)
Architecture Guidelines
ViewModel Design
Keep ViewModels Platform-Agnostic
ViewModels should not reference platform-specific types or UI frameworks.
// Bad ❌
public class MyViewModel : ReactiveObject
{
private readonly Window _window; // Platform-specific
public void ShowDialog() => _window.Show();
}
// Good ✅
public class MyViewModel : ReactiveObject
{
private readonly IDialogService _dialogService; // Interface
public async Task ShowDialog() => await _dialogService.ShowAsync();
}
Single Responsibility
Each ViewModel should have one clear responsibility.
// Bad - too many responsibilities ❌
public class MainViewModel
{
public void LoadData() { }
public void SaveData() { }
public void ExportToExcel() { }
public void SendEmail() { }
public void GenerateReport() { }
}
// Good - focused responsibility ✅
public class MainViewModel
{
private readonly IDataService _dataService;
private readonly INavigationService _navigation;
public ReactiveCommand<Unit, Unit> LoadDataCommand { get; }
public ReactiveCommand<Unit, Unit> NavigateToSettingsCommand { get; }
}
Use Reactive Properties Appropriately
public partial class SearchViewModel : ReactiveObject
{
[Reactive]
private string _searchText = string.Empty;
[ObservableAsProperty]
private bool _isSearching;
[ObservableAsProperty]
private List<SearchResult> _results;
public SearchViewModel()
{
// Derived state from commands
SearchCommand = ReactiveCommand.CreateFromTask(
async () => await PerformSearchAsync(SearchText),
this.WhenAnyValue(x => x.SearchText, text => !string.IsNullOrWhiteSpace(text)));
SearchCommand.IsExecuting
.ToProperty(this, x => x.IsSearching);
SearchCommand
.ToProperty(this, x => x.Results);
}
public ReactiveCommand<Unit, List<SearchResult>> SearchCommand { get; }
}
Dependency Injection
Register Services with RxAppBuilder
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithWpf()
.WithViewsFromAssembly(Assembly.GetExecutingAssembly())
.WithRegistration(locator =>
{
// Singletons for stateful services
locator.RegisterLazySingleton<IAuthService>(() => new AuthService());
locator.RegisterLazySingleton<ISettingsService>(() => new SettingsService());
// Transient for ViewModels
locator.Register<MainViewModel>(() => new MainViewModel());
locator.Register<DetailsViewModel>(() => new DetailsViewModel());
})
.BuildApp();
Constructor Injection Pattern
public class MainViewModel : ReactiveObject
{
private readonly IDataService _dataService;
private readonly INavigationService _navigation;
// Constructor injection ✅
public MainViewModel(
IDataService dataService,
INavigationService navigation)
{
_dataService = dataService;
_navigation = navigation;
}
// Alternative: Service locator (use sparingly)
public MainViewModel()
{
_dataService = AppLocator.Current.GetService<IDataService>();
_navigation = AppLocator.Current.GetService<INavigationService>();
}
}
Performance Guidelines
Use Throttle and Debounce
Prevent excessive operations on high-frequency events.
// Search as user types
this.WhenAnyValue(x => x.SearchText)
.Throttle(TimeSpan.FromMilliseconds(500))
.DistinctUntilChanged()
.Where(text => !string.IsNullOrWhiteSpace(text))
.SelectMany(async text => await SearchAsync(text))
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Subscribe(results => Results = results);
Use DynamicData for Collections
For reactive collections, always use DynamicData instead of ObservableCollection.
public partial class ItemListViewModel : ReactiveObject
{
private readonly SourceCache<Item, int> _itemsCache;
private readonly ReadOnlyObservableCollection<ItemViewModel> _items;
public ReadOnlyObservableCollection<ItemViewModel> Items => _items;
public ItemListViewModel()
{
_itemsCache = new SourceCache<Item, int>(x => x.Id);
_itemsCache.Connect()
.Transform(item => new ItemViewModel(item))
.Filter(vm => vm.IsVisible)
.Sort(SortExpressionComparer<ItemViewModel>.Ascending(x => x.Name))
.ObserveOn(RxSchedulers.MainThreadScheduler)
.Bind(out _items)
.Subscribe();
}
}
Optimize Observable Chains
// Bad - multiple subscriptions ❌
this.WhenAnyValue(x => x.Property1).Subscribe(/* ... */);
this.WhenAnyValue(x => x.Property1).Subscribe(/* ... */);
this.WhenAnyValue(x => x.Property1).Subscribe(/* ... */);
// Good - shared observable ✅
var sharedObservable = this.WhenAnyValue(x => x.Property1)
.Publish()
.RefCount();
sharedObservable.Subscribe(/* ... */);
sharedObservable.Subscribe(/* ... */);
sharedObservable.Subscribe(/* ... */);
Testing Guidelines
Use ReactiveUI.Testing
[Fact]
public void ViewModel_LoadsData_WhenCommandExecuted()
{
// Arrange
new TestScheduler().With(scheduler =>
{
var mockService = Substitute.For<IDataService>();
mockService.GetDataAsync().Returns(Observable.Return(testData));
var vm = new MainViewModel(mockService);
// Act
vm.LoadDataCommand.Execute().Subscribe();
scheduler.AdvanceBy(TimeSpan.FromSeconds(1).Ticks);
// Assert
vm.Data.Should().NotBeNull();
vm.IsLoading.Should().BeFalse();
});
}
Test ViewModels in Isolation
[Fact]
public async Task SearchCommand_FiltersResults_BasedOnSearchText()
{
// Arrange
var vm = new SearchViewModel();
vm.SearchText = "test";
// Act
await vm.SearchCommand.Execute();
// Assert
vm.Results.Should().NotBeEmpty();
vm.Results.Should().OnlyContain(r => r.Name.Contains("test"));
}
Error Handling
Use ThrownExceptions
public MyViewModel()
{
SaveCommand = ReactiveCommand.CreateFromTask(SaveAsync);
// Handle errors gracefully
SaveCommand.ThrownExceptions
.Subscribe(ex =>
{
// Log the error
this.Log().Error(ex, "Failed to save");
// Show user-friendly message
ErrorMessage = "Unable to save. Please try again.";
});
}
Validate User Input
public partial class LoginViewModel : ReactiveValidationObject
{
[Reactive]
private string _username = string.Empty;
[Reactive]
private string _password = string.Empty;
public LoginViewModel()
{
// Validation rules
this.ValidationRule(
vm => vm.Username,
username => !string.IsNullOrWhiteSpace(username),
"Username is required");
this.ValidationRule(
vm => vm.Password,
password => password?.Length >= 6,
"Password must be at least 6 characters");
// Command only executes when valid
LoginCommand = ReactiveCommand.CreateFromTask(
LoginAsync,
this.IsValid());
}
[ReactiveCommand]
private async Task Login() { /* ... */ }
}
Platform-Specific Guidelines
WPF
// Use ReactiveWindow<T>
public partial class MainWindow : ReactiveWindow<MainViewModel>
{
public MainWindow()
{
InitializeComponent();
ViewModel = new MainViewModel();
this.WhenActivated(disposables =>
{
// Bindings here
});
}
}
MAUI
// Use ReactiveContentPage<T>
public partial class MainPage : ReactiveContentPage<MainViewModel>
{
public MainPage()
{
InitializeComponent();
ViewModel = new MainViewModel();
this.WhenActivated(disposables =>
{
// Bindings here
});
}
}
Blazor
// Use ReactiveComponentBase<T> and call StateHasChanged
public partial class CounterComponent : ReactiveComponentBase<CounterViewModel>
{
protected override void OnInitialized()
{
ViewModel = new CounterViewModel();
this.WhenActivated(disposables =>
{
this.WhenAnyValue(x => x.ViewModel.Count)
.Subscribe(_ => InvokeAsync(StateHasChanged))
.DisposeWith(disposables);
});
base.OnInitialized();
}
}
Common Anti-Patterns to Avoid
❌ Don't Use Static RxApp
// Bad ❌
RxApp.MainThreadScheduler = _myCustomScheduler;
RxApp.DefaultExceptionHandler = /* ... */;
// Good ✅
// Use RxAppBuilder to configure schedulers
var app = RxAppBuilder.CreateReactiveUIBuilder()
.WithCustomScheduler(/* ... */)
.WithDefaultExceptionHandler(/* ... */)
.BuildApp();
❌ Don't Forget to Dispose
// Bad ❌
this.WhenAnyValue(x => x.Property).Subscribe(/* ... */);
// Good ✅
this.WhenActivated(disposables =>
{
this.WhenAnyValue(x => x.Property)
.Subscribe(/* ... */)
.DisposeWith(disposables);
});
❌ Don't Mix MVVM Patterns
// Bad - code-behind in view ❌
private void Button_Click(object sender, EventArgs e)
{
// Business logic here
}
// Good - command in ViewModel ✅
[ReactiveCommand]
private void ExecuteAction()
{
// Business logic here
}
❌ Don't Block on Async
// Bad ❌
var result = asyncOperation.Result;
asyncOperation.Wait();
// Good ✅
var result = await asyncOperation;
Migration Path
If you're upgrading from older ReactiveUI versions:
- Replace Fody with SourceGenerators - See Migration Guide
- Adopt RxAppBuilder - See Migration Guide
- Update to Modern Patterns - Follow this guide's recommendations
- Migrate from Xamarin - See Xamarin to MAUI Guide
Additional Resources
Platform-Specific Guidelines
For detailed platform-specific guidance, see: