Reactive programming framework for Blazor applications built on R3. Uses Roslyn source generation to automatically create observable models with command bindings, dependency injection support, and component triggers. Includes code fixes for quick refactoring.
$ dotnet add package RxBlazorV2A reactive programming framework for Blazor applications built on top of R3 (Reactive Extensions). RxBlazorV2 uses Roslyn source generators to automatically create observable models with reactive property bindings, command patterns, and dependency injection support.
IErrorModel[ObservableComponent]dotnet add package RxBlazorV2
The package includes the source generator and code fixes automatically.
| Package | Description |
|---|---|
| RxBlazorV2.MudBlazor | Reactive MudBlazor button components with progress indicators, cancellation, and confirmation dialogs |
[ObservableModelScope(ModelScope.Scoped)]
[ObservableComponent]
public partial class CounterModel : ObservableModel
{
public partial int Count { get; set; }
[ObservableCommand(nameof(Increment))]
public partial IObservableCommand IncrementCommand { get; }
private void Increment() => Count++;
}
@page "/counter"
@inherits CounterModelComponent
<p>Count: @Model.Count</p>
<button @onclick="() => Model.IncrementCommand.Execute()">Increment</button>
Properties marked as partial with both getter and setter are automatically enhanced with change notifications, value equality checks, and integration with the reactive observable stream.
public partial class MyModel : ObservableModel
{
public partial string Name { get; set; } = "Default";
public partial int Count { get; set; }
}
Commands link UI actions to model methods with automatic CanExecute tracking.
| Interface | Description |
|---|---|
IObservableCommand | Sync, no parameters |
IObservableCommand<T> | Sync with parameter |
IObservableCommandAsync | Async, no parameters |
IObservableCommandAsync<T> | Async with parameter and cancellation |
IObservableCommandR<T> | Sync with return value |
IObservableCommandR<T1, T2> | Sync with parameter and return value |
IObservableCommandRAsync<T> | Async with return value |
IObservableCommandRAsync<T1, T2> | Async with parameter and return value |
[ObservableCommand(nameof(Save), nameof(CanSave))]
public partial IObservableCommandAsync SaveCommand { get; }
[ObservableCommand(nameof(Calculate))]
public partial IObservableCommandR<int> CalculateCommand { get; }
private async Task Save() { /* ... */ }
private bool CanSave() => !string.IsNullOrEmpty(Name);
private int Calculate() => Count * 2;
IErrorModel)Commands automatically capture exceptions and route them to an IErrorModel implementation for centralized error handling:
// Implement IErrorModel in your error handling model
[ObservableModelScope(ModelScope.Singleton)]
public partial class ErrorModel : ObservableModel, IErrorModel
{
[ObservableComponentTrigger]
public ObservableList<string> Errors { get; } = [];
public void HandleError(Exception error)
{
Errors.Add(error.Message);
}
}
// Any model that injects IErrorModel gets automatic error capture
[ObservableModelScope(ModelScope.Scoped)]
public partial class MyModel : ObservableModel
{
public partial MyModel(IErrorModel errorModel);
[ObservableCommand(nameof(DoRiskyOperation))]
public partial IObservableCommandAsync RiskyCommand { get; }
private async Task DoRiskyOperation()
{
// If this throws, the exception is automatically
// captured and sent to ErrorModel.HandleError()
await _service.DoSomethingAsync();
}
}
Key Points:
IErrorModel via partial constructor to enable automatic error captureHandleError(Exception) methodRxBlazorV2.MudBlazor.StatusDisplay for automatic UI feedbackModels can reference and react to changes in other models through the partial constructor pattern:
[ObservableModelScope(ModelScope.Scoped)]
public partial class ShoppingCartModel : ObservableModel
{
// Declare dependencies via partial constructor
public partial ShoppingCartModel(ProductCatalogModel catalog, ILogger logger);
// Generator creates:
// - protected ProductCatalogModel Catalog { get; } with auto-subscription
// - private readonly ILogger _logger field
public partial decimal Total { get; set; }
private void RecalculateTotal()
{
Total = Quantity * Catalog.Price; // Access referenced model
}
}
Key Points:
protected properties with auto-subscriptionprivate readonly fields with underscore prefixUse [ObservableComponent] to generate a Blazor component base class:
[ObservableModelScope(ModelScope.Scoped)]
[ObservableComponent] // Generates MyModelComponent
public partial class MyModel : ObservableModel
{
public partial string Title { get; set; }
}
@page "/mypage"
@inherits MyModelComponent
<h1>@Model.Title</h1>
Options:
componentName: Custom component class name (default: {ModelName}Component)includeReferencedTriggers: Include triggers from referenced models (default: true)RxBlazorV2 provides multiple trigger types for different reactive scenarios:
Generate hook methods in components for property changes:
[ObservableComponent]
public partial class SettingsModel : ObservableModel
{
// Generates OnThemeChanged() hook in component
[ObservableComponentTrigger]
public partial string Theme { get; set; }
// Async hook with custom name
[ObservableComponentTriggerAsync(hookMethodName: "HandleDarkModeToggle")]
public partial bool IsDarkMode { get; set; }
// Render only - no hook generated
[ObservableComponentTrigger(ComponentTriggerType.RenderOnly)]
public partial int Counter { get; set; }
// Hook only - no re-render
[ObservableComponentTrigger(ComponentTriggerType.HookOnly)]
public partial string BackgroundTask { get; set; }
}
ComponentTriggerType Options:
RenderAndHook (default): Calls hook AND re-renders componentRenderOnly: Re-renders but no hook methodHookOnly: Calls hook but no automatic re-renderExecute internal methods automatically when properties change:
public partial class ValidationModel : ObservableModel
{
[ObservableTrigger(nameof(ValidateEmail))]
public partial string Email { get; set; }
[ObservableTriggerAsync(nameof(SaveAsync), nameof(CanSave))]
public partial string Data { get; set; }
private void ValidateEmail() { /* validation */ }
private async Task SaveAsync() { /* save */ }
private bool CanSave() => !string.IsNullOrEmpty(Data);
}
Auto-execute commands when properties change:
public partial class SearchModel : ObservableModel
{
public partial string Query { get; set; }
[ObservableCommand(nameof(Search))]
[ObservableCommandTrigger(nameof(Query))]
public partial IObservableCommandAsync SearchCommand { get; }
private async Task Search() { /* search logic */ }
}
Define reactive contracts in abstract base classes with attribute transfer to concrete implementations:
// Abstract base class defines the contract with reactive attributes
public abstract class StatusBaseModel : ObservableModel
{
[ObservableComponentTrigger]
public abstract ObservableList<StatusMessage> Messages { get; }
[ObservableComponentTrigger]
[ObservableTrigger(nameof(CanAddMessageTrigger))]
public abstract bool CanAddMessage { get; set; }
[ObservableCommandTrigger(nameof(CanAddMessage))]
public abstract IObservableCommand AddMessageCommand { get; }
protected abstract void CanAddMessageTrigger();
}
// Concrete class - attributes are automatically transferred from base
[ObservableComponent]
[ObservableModelScope(ModelScope.Singleton)]
public partial class AppStatusModel : StatusBaseModel
{
public override ObservableList<StatusMessage> Messages { get; } = [];
public override partial bool CanAddMessage { get; set; }
[ObservableCommand(nameof(AddMessage))]
public override partial IObservableCommand AddMessageCommand { get; }
protected override void CanAddMessageTrigger() { /* ... */ }
private void AddMessage() { /* ... */ }
}
Key Points:
override partial - attributes are automatically transferred[ObservableComponentTrigger], [ObservableTrigger], [ObservableCommandTrigger] all transferoverride modifier in generated codePrivate methods that read referenced model properties are automatically detected and subscribed. No special naming convention required - the generator analyzes which properties are actually accessed:
[ObservableModelScope(ModelScope.Scoped)]
public partial class ShoppingCartModel : ObservableModel
{
public partial ShoppingCartModel(ProductCatalogModel catalog);
[ObservableTrigger(nameof(RecalculateTotal))]
public partial int Quantity { get; set; }
public partial decimal Total { get; set; }
// This method is BOTH a property trigger AND an internal observer:
// - Called when Quantity changes (via [ObservableTrigger])
// - Called when Catalog.Price changes (auto-detected read)
private void RecalculateTotal()
{
Total = Quantity * Catalog.Price;
}
}
Valid Signatures:
private void MethodName()private Task MethodName() or private Task MethodName(CancellationToken ct)Key Points:
Allow external services to observe model changes using [ObservableModelObserver]:
public class NotificationService
{
public NotificationService(UserModel userModel)
{
// Service is injected into model constructor
}
[ObservableModelObserver(nameof(UserModel.UnreadCount))]
private void OnUnreadCountChanged(UserModel model)
{
UpdateBadge(model.UnreadCount);
}
[ObservableModelObserver(nameof(UserModel.Status))]
private async Task OnStatusChangedAsync(UserModel model, CancellationToken ct)
{
await SyncStatusAsync(model.Status, ct);
}
}
Group property changes for single notifications:
public partial class FormModel : ObservableModel
{
[ObservableBatch("userInfo")]
public partial string FirstName { get; set; }
[ObservableBatch("userInfo")]
public partial string LastName { get; set; }
public void UpdateUser(string first, string last)
{
using (SuspendNotifications("userInfo"))
{
FirstName = first;
LastName = last;
} // Single notification fired here
}
}
| Attribute | Target | Description |
|---|---|---|
[ObservableModelScope] | Class | DI lifetime (Singleton, Scoped, Transient) |
[ObservableComponent] | Class | Generate component base class |
[ObservableCommand] | Property | Link command to implementation method |
[ObservableComponentTrigger] | Property | Generate component hook (sync) |
[ObservableComponentTriggerAsync] | Property | Generate component hook (async) |
[ObservableTrigger] | Property | Execute method on change (sync) |
[ObservableTriggerAsync] | Property | Execute method on change (async) |
[ObservableCommandTrigger] | Property | Auto-execute command on property change |
[ObservableModelObserver] | Method | Subscribe service method to model property changes |
[ObservableBatch] | Property | Group for batched notifications |
Key Design Principles:
Removed: Generic [ObservableTrigger<T>] and [ObservableTriggerAsync<T>] attributes
The generic trigger attributes have been removed. Use these alternatives instead:
| Removed | Replacement |
|---|---|
[ObservableTrigger<T>(method, param)] | [ObservableModelObserver] on service methods |
[ObservableTriggerAsync<T>(method, param)] | [ObservableModelObserver] with async signature |
Note: The non-generic
[ObservableTrigger]and[ObservableTriggerAsync]attributes for internal method execution are still available.
For internal model observers that react to referenced model changes, use the new auto-detection pattern:
[ObservableModelScope(ModelScope.Scoped)]
public partial class MyModel : ObservableModel
{
public partial MyModel(SettingsModel settings);
// Auto-detected: private void method accessing Settings properties
private void OnThemeChanged()
{
// Automatically subscribed to Settings.Theme changes
ApplyTheme(Settings.Theme);
}
// Async version with CancellationToken
private async Task OnLanguageChangedAsync(CancellationToken ct)
{
await LoadLocalizationAsync(Settings.Language, ct);
}
}
For external services, use [ObservableModelObserver]:
public class ThemeService
{
[ObservableModelObserver(nameof(SettingsModel.Theme))]
private void OnThemeChanged(SettingsModel model)
{
ApplyGlobalTheme(model.Theme);
}
}
field keyword)See the RxBlazorV2Sample project for comprehensive, interactive examples:
| Sample | Description |
|---|---|
| BasicCommands | Sync/async commands and observable properties |
| BasicCommandWithReturn | Commands that return values |
| ParameterizedCommands | Commands with type-safe parameters |
| CommandsWithCanExecute | Conditional command execution |
| CommandsWithCancellation | Long-running async with cancellation |
| ErrorHandling | Automatic error capture via IErrorModel |
| CommandTriggers | Auto-execute commands on property changes |
| ComponentTriggers | Component hooks for property changes |
| PropertyTriggers | Internal method execution on changes |
| ModelObservers | External and internal service subscriptions |
| ModelReferences | Cross-model reactive subscriptions |
| ModelPatterns | Partial constructor pattern examples |
| GenericModels | Generic observable models with DI |
| ObservableBatches | Batched property notifications |
| ValueEquality | Automatic value equality |
| CrossComponentCommunication | Share models across components |
| InternalModelObservers | Auto-detected private methods reacting to referenced model changes |
Run the sample application:
dotnet run --project RxBlazorV2Sample
The ReactivePatternSample is a multi-project Blazor application demonstrating all reactive patterns in a real-world scenario:
| Project | Description |
|---|---|
| ReactivePatternSample | Main Blazor WASM host application |
| ReactivePatternSample.Auth | Authentication model with login/logout commands and triggers |
| ReactivePatternSample.Settings | User preferences with component triggers for theme/language |
| ReactivePatternSample.Status | Status bar model with internal observers and notifications |
| ReactivePatternSample.Storage | Persistence layer with external model observers |
| ReactivePatternSample.Todo | Todo list demonstrating commands, triggers, and cross-model reactivity |
| ReactivePatternSample.Share | Sharing functionality with async commands and dialogs |
Key demonstrations:
[ObservableModelObserver]Run the reactive pattern sample:
dotnet run --project ReactivePatternSample/ReactivePatternSample
The generator provides comprehensive diagnostics (RXBG001-RXBG082) with code fixes. See the Diagnostics Help folder for detailed documentation.
Key diagnostic ranges:
Contributions are welcome! Please ensure:
This project is licensed under the MIT License - see the LICENSE file for details.
Built with: