LionFire provides a comprehensive Blazor UI toolkit built on MudBlazor that integrates seamlessly with reactive data patterns, MVVM architecture, and workspace-scoped services. This documentation covers UI patterns, component usage, and best practices for building reactive Blazor applications.
Key Philosophy: Minimize boilerplate while maintaining flexibility. Components should work automatically with reactive data sources, but allow manual control when needed.
1. Blazor MVVM Patterns ⭐ START HERE
Essential reading for understanding when to use automatic vs. manual patterns.
Topics:
- ObservableDataView Pattern (Automatic) - Zero-boilerplate data grids
- Manual ViewModel Pattern - Full control for detail pages
- Decision flowchart - Which pattern to use?
- Complete examples - Both patterns side-by-side
When to Use:
- Building list or detail views
- Deciding between automatic and manual approaches
- Understanding reactive binding
Reference guide for all available UI components.
Contents:
ObservableDataView- Reactive data grid with CRUDInspectorView- Property grid inspectorWorkspaceSelector- Workspace selection UICascadingT- Type-safe cascading values- Utility components
When to Use:
- Finding components for common scenarios
- Understanding component parameters
- Exploring advanced features
Deep dive into how reactive updates flow through the UI.
Topics:
- Change detection mechanisms
- Observable subscriptions
- StateHasChanged optimization
- Performance considerations
When to Use:
- Debugging update issues
- Optimizing performance
- Understanding internals
Use when: Displaying a list of workspace documents with standard CRUD operations.
@page "/bots" <ObservableDataView TKey="string" TValue="BotEntity" TValueVM="BotVM" DataServiceProvider="@WorkspaceServices" ReadOnly=false> <Columns> <PropertyColumn Property="x => x.Value.Name" /> <PropertyColumn Property="x => x.Value.Description" /> </Columns> </ObservableDataView> @code { [CascadingParameter(Name = "WorkspaceServices")] public IServiceProvider? WorkspaceServices { get; set; } }What you get:
- ✅ Automatic data loading from workspace
- ✅ Built-in toolbar (Add, Edit, Delete)
- ✅ Reactive updates when files change
- ✅ Sorting, filtering, pagination
- ✅ ~20 lines of code total
Use when: Displaying/editing a single document with custom layout.
@page "/bots/{BotId}" <MudCard> <MudCardContent> <MudTextField Label="Name" @bind-Value="vm.Value.Name" /> <MudTextField Label="Description" @bind-Value="vm.Value.Description" /> <MudSwitch @bind-Checked="vm.Value.Enabled" Label="Enabled" /> </MudCardContent> <MudCardActions> <MudButton OnClick="Save" Color="Color.Primary">Save</MudButton> </MudCardActions> </MudCard> @code { [Parameter] public string? BotId { get; set; } [CascadingParameter(Name = "WorkspaceServices")] public IServiceProvider? WorkspaceServices { get; set; } ObservableReaderWriterItemVM<string, BotEntity, BotVM>? vm; protected override void OnInitialized() { var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>(); var writer = WorkspaceServices.GetService<IObservableWriter<string, BotEntity>>(); vm = new ObservableReaderWriterItemVM<string, BotEntity, BotVM>(reader, writer); vm.Id = BotId; // Automatically loads } private async Task Save() { await vm.Write(); // Save to file } }What you get:
- ✅ Full layout control
- ✅ Automatic data loading/saving
- ✅ Reactive updates from file changes
- ✅ Custom validation and commands
- ✅ ~40 lines of code total
Problem: UI components need access to workspace-specific data readers/writers.
Solution: Cascade WorkspaceServices (IServiceProvider) from layout to descendants.
<!-- Layout Component --> <CascadingValue Name="WorkspaceServices" Value="@WorkspaceServices"> @Body </CascadingValue> <!-- Child Component --> @code { [CascadingParameter(Name = "WorkspaceServices")] public IServiceProvider? WorkspaceServices { get; set; } // Now can resolve workspace services var reader = WorkspaceServices.GetService<IObservableReader<string, MyEntity>>(); }Why: Each workspace has its own service provider with readers/writers pointing to that workspace's directories. See Service Scoping.
LionFire components automatically update when:
- Entity properties change (via
INotifyPropertyChanged) - Files are added/removed from workspace
- Observable collections emit changes
Requirements:
- Entity must implement
INotifyPropertyChanged - Use
ReactiveObjector[ObservableProperty] - Component subscribes to observables
Example:
// ✅ Reactive Entity public partial class BotEntity : ReactiveObject { [Reactive] private string? _name; // Automatically notifies changes } // ❌ Non-Reactive Entity public class BotEntity { public string? Name { get; set; } // No notifications! }ViewModels wrap entities and provide UI-specific functionality:
// Entity (data model) public partial class BotEntity : ReactiveObject { [Reactive] private string? _name; [Reactive] private decimal _profitLoss; } // ViewModel (adds UI logic) public class BotVM : KeyValueVM<string, BotEntity> { public BotVM(string key, BotEntity value) : base(key, value) { } // Computed property for UI public string DisplayName => $"{Value.Name} ({Key})"; // UI-specific formatting public string ProfitLossFormatted => Value.ProfitLoss.ToString("C2"); // Commands public ReactiveCommand<Unit, Unit> ToggleEnabled { get; } }When to Use VMs:
- Need computed properties for display
- Need commands for UI actions
- Want to keep entities pure (no UI logic)
Application Root ↓ WorkspaceLayoutVM ↓ Cascades WorkspaceServices Blazor Pages (@page "/bots") ↓ Uses ObservableDataView (automatic) OR Manual ViewModel Pattern ↓ Both resolve IObservableReader/Writer ↓ Backed by File System (HJSON files) | Scenario | Pattern | Component/VM | Boilerplate |
|---|---|---|---|
| List View | Automatic | ObservableDataView | Minimal (~20 lines) |
| Detail View | Manual | ObservableReaderWriterItemVM | Medium (~40 lines) |
| Master-Detail | Hybrid | List uses ObservableDataView, detail uses manual VM | Mixed |
| Read-Only Display | Manual | ObservableReaderItemVM | Minimal |
| Custom Layout | Manual | Custom VM with direct reader/writer access | High (full control) |
UI Layer (Blazor Components) ↓ uses ViewModel Layer (KeyValueVM, ObservableReaderWriterItemVM) ↓ wraps Reactive Persistence Layer (IObservableReader/Writer) ↓ backed by File System (HJSON files in workspace) Blazor Component ↓ CascadingParameter WorkspaceServices (IServiceProvider) ↓ GetService<T>() IObservableReader<TKey, TValue> ↓ Points to workspace1/Bots/ directory <!-- ✅ Good - Minimal code --> <ObservableDataView TKey="string" TValue="Bot" TValueVM="BotVM" DataServiceProvider="@WorkspaceServices" /> <!-- ❌ Avoid - Manual list management --> @code { List<BotVM> bots; protected override async Task OnInitializedAsync() { var reader = WorkspaceServices.GetService<IObservableReader<string, Bot>>(); // Manual subscription, disposal, updates... } }<!-- ✅ Good - Full control over layout --> <MudCard> <MudTextField @bind-Value="vm.Value.Name" /> <MudTextField @bind-Value="vm.Value.Description" /> </MudCard> <!-- ❌ Avoid - ObservableDataView for single item --> <ObservableDataView ... /> <!-- Overkill for one item --><!-- ✅ Good - Cascade provider --> <CascadingValue Name="WorkspaceServices" Value="@WorkspaceServices"> @Body </CascadingValue> <!-- ❌ Avoid - Cascading individual services --> <CascadingValue Value="@BotReader"> <CascadingValue Value="@BotWriter"> <CascadingValue Value="@PortfolioReader"> <!-- Too many cascades! --> </CascadingValue>// ✅ Good - Reactive notifications public partial class BotEntity : ReactiveObject { [Reactive] private string? _name; } // ❌ Avoid - No change notifications public class BotEntity { public string? Name { get; set; } }@implements IAsyncDisposable @code { IDisposable? subscription; protected override void OnInitialized() { subscription = reader.Values.Connect().Subscribe(changes => { // Handle changes }); } public async ValueTask DisposeAsync() { subscription?.Dispose(); } }<ObservableDataView TKey="string" TValue="Config" TValueVM="ConfigVM" DataServiceProvider="@WorkspaceServices" ReadOnly="true"> <Columns> <PropertyColumn Property="x => x.Value.Name" /> </Columns> </ObservableDataView><ObservableDataView TKey="string" TValue="Bot" TValueVM="BotVM" DataServiceProvider="@WorkspaceServices" ReadOnly="false" CreatableTypes="@(new[] { typeof(Bot) })"> <Columns> <PropertyColumn Property="x => x.Value.Name" /> <TemplateColumn> <CellTemplate> <MudSwitch @bind-Checked="context.Item.Value.Enabled" /> </CellTemplate> </TemplateColumn> </Columns> </ObservableDataView>List Page:
@page "/bots" <ObservableDataView ...> <Columns> <TemplateColumn> <CellTemplate> <MudButton Href="@($"/bots/{context.Item.Key}")">Edit</MudButton> </CellTemplate> </TemplateColumn> </Columns> </ObservableDataView>Detail Page:
@page "/bots/{BotId}" <MudCard> <MudCardContent> <MudTextField @bind-Value="vm.Value.Name" /> <!-- Custom layout --> </MudCardContent> </MudCard> @code { [Parameter] public string? BotId { get; set; } ObservableReaderWriterItemVM<string, Bot, BotVM>? vm; }<ObservableDataView ...> <Columns> <!-- Status indicator --> <TemplateColumn> <CellTemplate> <MudIcon Icon="@Icons.Material.Filled.Circle" Color="@(context.Item.Value.Enabled ? Color.Success : Color.Default)" /> </CellTemplate> </TemplateColumn> <!-- Action buttons --> <TemplateColumn> <CellTemplate> <MudIconButton Icon="@Icons.Material.Filled.PlayArrow" OnClick="@(() => StartBot(context.Item))" /> <MudIconButton Icon="@Icons.Material.Filled.Stop" OnClick="@(() => StopBot(context.Item))" /> </CellTemplate> </TemplateColumn> </Columns> </ObservableDataView>Cause: Using root DI container instead of workspace services.
Fix:
<!-- ❌ Wrong --> <ObservableDataView DataServiceProvider="@ServiceProvider" /> <!-- ✅ Correct --> <ObservableDataView DataServiceProvider="@WorkspaceServices" />Cause: Entity doesn't implement INotifyPropertyChanged.
Fix:
// ❌ Wrong public class Bot { public string Name { get; set; } } // ✅ Correct public partial class Bot : ReactiveObject { [Reactive] private string? _name; }Check:
- Is
DataServiceProviderset toWorkspaceServices? - Are files present in workspace directory?
- Is entity type registered with
AddWorkspaceChildType<T>()? - Check console for errors
Debug:
var reader = WorkspaceServices.GetService<IObservableReader<string, Bot>>(); Console.WriteLine($"Keys: {string.Join(", ", reader?.Keys.Items)}");- Workspace Architecture - High-level workspace concepts
- Service Scoping - Understanding workspace services
- MVVM Architecture - ViewModel patterns
- LionFire.Blazor.Components.MudBlazor - ObservableDataView deep dive
- LionFire.Data.Async.Mvvm - ViewModels and reactive patterns
- LionFire.Reactive - Observable readers/writers
- How-To: Create Blazor Workspace Page - Step-by-step tutorial
LionFire's Blazor UI toolkit provides two primary patterns:
- Component:
ObservableDataView - Boilerplate: Minimal (~20 lines)
- Use for: Lists, grids, CRUD
- ViewModel:
ObservableReaderWriterItemVM - Boilerplate: Medium (~40 lines)
- Use for: Detail views, custom layouts
Key Benefit: Both patterns integrate seamlessly with workspace-scoped services and reactive data persistence, eliminating manual subscription management and state synchronization.
Next Steps:
- Read Blazor MVVM Patterns for detailed examples
- Browse Component Catalog for available components
- Check Reactive UI Updates for performance optimization