Agent skill
reactiveui
USE FOR: Building MVVM applications using Reactive Extensions with ReactiveObject, WhenAnyValue, ReactiveCommand, view model activation, and data binding for WPF, MAUI, Avalonia, and Blazor. DO NOT USE FOR: Simple applications without reactive data flows, server-side APIs without UI, or projects that prefer CommunityToolkit.Mvvm for a simpler MVVM approach.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/other/reactiveui
Metadata
Additional technical details for this skill
- author
- Tyler-R-Kendrick
- version
- 1.0.0
- displayName
- ReactiveUI
SKILL.md
ReactiveUI
Overview
ReactiveUI is a composable, cross-platform MVVM framework built on Reactive Extensions (Rx.NET). It provides ReactiveObject as a base class for view models with change notification, WhenAnyValue for observing property changes as IObservable<T>, ReactiveCommand for async command execution with built-in busy/error state, and ObservableAsPropertyHelper<T> (OAPH) for computed output properties driven by observables.
ReactiveUI supports WPF, WinUI, MAUI, Avalonia, Blazor, and Xamarin. It integrates with DynamicData for reactive collections and provides platform-specific binding extensions through ReactiveUI.WPF, ReactiveUI.Maui, etc.
Installation
dotnet add package ReactiveUI
dotnet add package ReactiveUI.WPF # For WPF
# OR
dotnet add package ReactiveUI.Maui # For .NET MAUI
# OR
dotnet add package ReactiveUI.Blazor # For Blazor
ReactiveObject and Property Change Notification
using ReactiveUI;
namespace MyApp.ViewModels;
public class LoginViewModel : ReactiveObject
{
private string _username = string.Empty;
public string Username
{
get => _username;
set => this.RaiseAndSetIfChanged(ref _username, value);
}
private string _password = string.Empty;
public string Password
{
get => _password;
set => this.RaiseAndSetIfChanged(ref _password, value);
}
private bool _rememberMe;
public bool RememberMe
{
get => _rememberMe;
set => this.RaiseAndSetIfChanged(ref _rememberMe, value);
}
private string _errorMessage = string.Empty;
public string ErrorMessage
{
get => _errorMessage;
set => this.RaiseAndSetIfChanged(ref _errorMessage, value);
}
}
WhenAnyValue: Observing Property Changes
WhenAnyValue converts property changes into IObservable<T> streams.
using System;
using System.Reactive.Linq;
using ReactiveUI;
namespace MyApp.ViewModels;
public class SearchViewModel : ReactiveObject
{
private string _searchText = string.Empty;
public string SearchText
{
get => _searchText;
set => this.RaiseAndSetIfChanged(ref _searchText, value);
}
private readonly ObservableAsPropertyHelper<bool> _hasSearchText;
public bool HasSearchText => _hasSearchText.Value;
private readonly ObservableAsPropertyHelper<int> _characterCount;
public int CharacterCount => _characterCount.Value;
public SearchViewModel()
{
// Single property observation
_hasSearchText = this.WhenAnyValue(x => x.SearchText)
.Select(text => !string.IsNullOrWhiteSpace(text))
.ToProperty(this, x => x.HasSearchText);
_characterCount = this.WhenAnyValue(x => x.SearchText)
.Select(text => text?.Length ?? 0)
.ToProperty(this, x => x.CharacterCount);
}
}
Combining multiple properties:
using System;
using System.Reactive.Linq;
using ReactiveUI;
namespace MyApp.ViewModels;
public class RegistrationViewModel : ReactiveObject
{
private string _email = string.Empty;
public string Email
{
get => _email;
set => this.RaiseAndSetIfChanged(ref _email, value);
}
private string _password = string.Empty;
public string Password
{
get => _password;
set => this.RaiseAndSetIfChanged(ref _password, value);
}
private string _confirmPassword = string.Empty;
public string ConfirmPassword
{
get => _confirmPassword;
set => this.RaiseAndSetIfChanged(ref _confirmPassword, value);
}
private readonly ObservableAsPropertyHelper<bool> _isFormValid;
public bool IsFormValid => _isFormValid.Value;
private readonly ObservableAsPropertyHelper<string> _passwordStrength;
public string PasswordStrength => _passwordStrength.Value;
public RegistrationViewModel()
{
// Combine multiple properties for validation
_isFormValid = this.WhenAnyValue(
x => x.Email,
x => x.Password,
x => x.ConfirmPassword,
(email, pass, confirm) =>
!string.IsNullOrWhiteSpace(email) &&
email.Contains('@') &&
pass.Length >= 8 &&
pass == confirm)
.ToProperty(this, x => x.IsFormValid);
_passwordStrength = this.WhenAnyValue(x => x.Password)
.Select(EvaluatePasswordStrength)
.ToProperty(this, x => x.PasswordStrength, initialValue: "None");
}
private static string EvaluatePasswordStrength(string password)
{
if (string.IsNullOrEmpty(password)) return "None";
if (password.Length < 8) return "Weak";
bool hasUpper = password.Any(char.IsUpper);
bool hasDigit = password.Any(char.IsDigit);
bool hasSpecial = password.Any(c => !char.IsLetterOrDigit(c));
int score = (hasUpper ? 1 : 0) + (hasDigit ? 1 : 0) + (hasSpecial ? 1 : 0);
return score switch
{
>= 3 => "Strong",
2 => "Medium",
_ => "Weak"
};
}
}
ReactiveCommand
ReactiveCommand wraps async operations with built-in IsExecuting, error handling, and canExecute observable guards.
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using ReactiveUI;
namespace MyApp.ViewModels;
public class OrderViewModel : ReactiveObject
{
private string _productName = string.Empty;
public string ProductName
{
get => _productName;
set => this.RaiseAndSetIfChanged(ref _productName, value);
}
private int _quantity = 1;
public int Quantity
{
get => _quantity;
set => this.RaiseAndSetIfChanged(ref _quantity, value);
}
private readonly ObservableAsPropertyHelper<bool> _isSubmitting;
public bool IsSubmitting => _isSubmitting.Value;
public ReactiveCommand<Unit, OrderResult> SubmitCommand { get; }
public ReactiveCommand<Unit, Unit> CancelCommand { get; }
private readonly IOrderService _orderService;
public OrderViewModel(IOrderService orderService)
{
_orderService = orderService;
// CanExecute observable: only when form is valid and not already submitting
var canSubmit = this.WhenAnyValue(
x => x.ProductName,
x => x.Quantity,
(name, qty) =>
!string.IsNullOrWhiteSpace(name) && qty > 0);
// Create async command with canExecute guard
SubmitCommand = ReactiveCommand.CreateFromTask(
SubmitOrderAsync,
canSubmit);
// Track IsExecuting as a property
_isSubmitting = SubmitCommand.IsExecuting
.ToProperty(this, x => x.IsSubmitting);
// Handle errors
SubmitCommand.ThrownExceptions
.Subscribe(ex =>
Console.WriteLine($"Order failed: {ex.Message}"));
CancelCommand = ReactiveCommand.Create(() =>
{
ProductName = string.Empty;
Quantity = 1;
});
}
private async Task<OrderResult> SubmitOrderAsync()
{
return await _orderService.PlaceOrderAsync(ProductName, Quantity);
}
}
public record OrderResult(string OrderId, string Status);
public interface IOrderService
{
Task<OrderResult> PlaceOrderAsync(string productName, int quantity);
}
View Model Activation
ReactiveUI provides IActivatableViewModel for managing resources that should only exist when the view is visible.
using System;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using ReactiveUI;
namespace MyApp.ViewModels;
public class DashboardViewModel : ReactiveObject, IActivatableViewModel
{
public ViewModelActivator Activator { get; } = new();
private readonly ObservableAsPropertyHelper<string> _currentTime;
public string CurrentTime => _currentTime.Value;
private readonly ObservableAsPropertyHelper<int> _activeUsers;
public int ActiveUsers => _activeUsers.Value;
public DashboardViewModel()
{
_currentTime = ObservableAsPropertyHelper<string>
.Default(initialValue: "Loading...");
_activeUsers = ObservableAsPropertyHelper<int>.Default();
this.WhenActivated(disposables =>
{
// Timer that only runs when the view is active
Observable.Interval(TimeSpan.FromSeconds(1))
.Select(_ => DateTime.Now.ToString("HH:mm:ss"))
.ToProperty(this, x => x.CurrentTime, out _currentTime)
.DisposeWith(disposables);
// Poll for active users while view is visible
Observable.Interval(TimeSpan.FromSeconds(30))
.SelectMany(_ => Observable.FromAsync(FetchActiveUsersAsync))
.ToProperty(this, x => x.ActiveUsers, out _activeUsers)
.DisposeWith(disposables);
});
}
private async Task<int> FetchActiveUsersAsync()
{
await Task.Delay(100);
return Random.Shared.Next(50, 200);
}
}
Interaction: View Model to View Communication
using System;
using System.Reactive;
using System.Reactive.Linq;
using System.Threading.Tasks;
using ReactiveUI;
namespace MyApp.ViewModels;
public class FileViewModel : ReactiveObject
{
// Interaction: VM asks the view a question and gets an answer
public Interaction<string, bool> ConfirmDelete { get; } = new();
public Interaction<Unit, string?> SelectFile { get; } = new();
public ReactiveCommand<string, Unit> DeleteCommand { get; }
public ReactiveCommand<Unit, Unit> OpenCommand { get; }
public FileViewModel()
{
DeleteCommand = ReactiveCommand.CreateFromTask<string>(async (fileName) =>
{
// Ask the view for confirmation
bool confirmed = await ConfirmDelete.Handle(fileName);
if (confirmed)
{
// Perform deletion
Console.WriteLine($"Deleted: {fileName}");
}
});
OpenCommand = ReactiveCommand.CreateFromTask(async () =>
{
string? path = await SelectFile.Handle(Unit.Default);
if (path is not null)
{
Console.WriteLine($"Opening: {path}");
}
});
}
}
// In the View (WPF example):
// ViewModel.ConfirmDelete.RegisterHandler(async interaction =>
// {
// var result = MessageBox.Show(
// $"Delete {interaction.Input}?", "Confirm",
// MessageBoxButton.YesNo);
// interaction.SetOutput(result == MessageBoxResult.Yes);
// });
ReactiveUI vs. CommunityToolkit.Mvvm
| Feature | ReactiveUI | CommunityToolkit.Mvvm |
|---|---|---|
| Change notification | RaiseAndSetIfChanged (manual) |
[ObservableProperty] (generated) |
| Computed properties | ObservableAsPropertyHelper<T> |
[NotifyPropertyChangedFor] |
| Commands | ReactiveCommand (Rx-based) |
[RelayCommand] (generated) |
| Async command state | Built-in IsExecuting, errors |
AsyncRelayCommand.IsRunning |
| Property observation | WhenAnyValue (observable streams) |
Partial method notifications |
| Activation lifecycle | IActivatableViewModel |
Not built-in |
| View-VM interaction | Interaction<TInput, TOutput> |
IMessenger.Send/Register |
| Reactive collections | DynamicData integration | ObservableCollection |
| Learning curve | Higher (Rx knowledge required) | Lower (attribute-based) |
| Best for | Complex reactive data flows | Simple MVVM with code generation |
Best Practices
-
Use
this.RaiseAndSetIfChanged(ref _field, value)for every mutable property instead of manually callingOnPropertyChangedbecause it handles equality comparison, backing field assignment, and notification in a single atomic call. -
Use
ObservableAsPropertyHelper<T>(OAPH) for computed/derived properties instead of subscribing and setting a property manually; OAPH integrates with ReactiveUI's scheduler and property-change notification automatically. -
Pass a
canExecuteobservable toReactiveCommand.Createso that the command automatically disables bound buttons when the condition is false, and automatically disables during execution to prevent double-submission. -
Subscribe to
command.ThrownExceptionsfor everyReactiveCommandbecause unobserved exceptions in commands are routed toRxApp.DefaultExceptionHandlerwhich terminates the application by default. -
Use
IActivatableViewModelwiththis.WhenActivated(disposables => { ... })for subscriptions that should only run while the view is visible to prevent background timers, network polls, and event handlers from running when the view is navigated away. -
Apply
ThrottletoWhenAnyValuestreams for search text inputs to avoid executing an API call on every keystroke;Throttle(TimeSpan.FromMilliseconds(300))waits for the user to stop typing before emitting. -
Use
Interaction<TInput, TOutput>instead of injecting view-layer services into view models to keep view models testable; in tests, register a handler that returns a predetermined response without showing UI. -
Call
.DisposeWith(disposables)on every subscription insideWhenActivatedto ensure subscriptions are cleaned up when the view is deactivated, preventing memory leaks and stale data updates. -
Use
WhenAnyValuewith multiple property overloads (up to 12 properties) to combine multiple inputs into a single derived value rather than subscribing to each property individually, which simplifies synchronization. -
Keep view model constructors synchronous and move async initialization into a
ReactiveCommandorWhenActivatedblock because constructors cannot be awaited and async void constructors crash on unhandled exceptions.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?