Reactive Blazor components built on MudBlazor that integrate with LionFire's MVVM and reactive persistence patterns. These components provide high-level, pre-built UI elements for working with observable data collections, particularly workspace-scoped documents.
Key Feature: Automatic integration with IObservableReader/Writer for file-backed, reactive data grids.
Purpose: Displays a reactive MudDataGrid bound to an IObservableReader/Writer collection with automatic CRUD operations, toolbar, and reactive updates.
When to Use:
- Displaying lists of workspace documents
- Need standard CRUD operations (Create, Read, Update, Delete)
- Want built-in toolbar
- Data is backed by
IObservableReader/Writer
Primary Use Case: Master list views in workspace-based applications.
<ObservableDataView TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
DataServiceProvider="@WorkspaceServices"
AllowedEditModes="EditMode.All"
ReadOnly=false>
<Columns>
<PropertyColumn Property="x => x.Value.Name" Title="Name" />
<PropertyColumn Property="x => x.Value.Description" Title="Description" />
</Columns>
</ObservableDataView>
@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
}Service Resolution Flow:
DataServiceProvider (WorkspaceServices)
↓
EffectiveDataServiceProvider
↓ GetService<IObservableReader<TKey, TValue>>()
↓ GetService<IObservableWriter<TKey, TValue>>()
ViewModel.Data = reader/writer
↓
ObservableDataVM<TKey, TValue, TValueVM>
↓ Subscribes to reader.Values.Connect()
Items Observable Collection
↓
MudDataGrid<TValueVM>
Automatic VM Creation:
// Component automatically creates VMs for each entity using constructor injection:
foreach (var kvp in reader.Values)
{
var vm = VMFactory?.Invoke(kvp.Key, kvp.Value)
?? ActivatorUtilities.CreateInstance<TValueVM>(serviceProvider, kvp.Key, kvp.Value);
}Required:
TKey // Key type (usually string for file names)
TValue // Entity type (e.g., BotEntity)
TValueVM // ViewModel type (e.g., BotVM)Data Source:
[Parameter]
public IServiceProvider? DataServiceProvider { get; set; }
// Pass workspace services here to resolve IObservableReader/Writer
[Parameter]
public IObservableReader<TKey, TValue>? Data { get; set; }
// Alternative: Provide reader/writer directlyCRUD Control:
[Parameter]
public EditMode AllowedEditModes { get; set; }
// EditMode.None, .Cell, .Form, .All
[Parameter]
public bool ReadOnly { get; set; } = true
[Parameter]
public IEnumerable<Type>? CreatableTypes { get; set; }
// Types that can be created via "Add" button
[Parameter]
public bool CanCreateValueType { get; set; } = trueUI Customization:
[Parameter]
public RenderFragment<ObservableDataVM<TKey, TValue, TValueVM>>? ChildContent { get; set; }
// Custom rendering (instead of default MudDataGrid)
[Parameter]
public RenderFragment? Columns { get; set; }
// Column definitions
[Parameter]
public RenderFragment? EditingColumns { get; set; }
// Columns when in edit mode (overrides Columns)
[Parameter]
public RenderFragment<CellContext<TValueVM>>? ChildRowContent { get; set; }
// Expandable row content
[Parameter]
public RenderFragment<TValueVM>? ContextMenu { get; set; }
// Right-click context menuVM Factory:
[Parameter]
public Func<TKey, Optional<TValue>, TValueVM>? VMFactory { get; set; }
// Custom ViewModel creationEvents:
[Parameter]
public EventHandler<DataGridRowClickEventArgs<TValueVM>>? RowClick { get; set; }The component renders a toolbar with:
- Add Button (if
CreatableTypesspecified)- Dropdown if multiple types
- Single button if one type
- Edit Toggle - Switches between view and edit mode
- Delete Toggle - Shows/hides delete column
- Refresh Button (if
ShowRefresh = true)
If no Columns specified, the component generates columns automatically:
[Parameter]
public Func<PropertyInfo, bool>? IsAutoColumn { get; set; }
// Determines which properties become columns
// Default: public, readable, standard types (string, int, etc.)
[Parameter]
public Func<PropertyInfo, bool>? IsAutoEditColumn { get; set; }
// Determines which properties are editable
// Default: public, writeableThe component automatically updates when:
- Files are added/removed from workspace directory
- Entity properties change (via
INotifyPropertyChanged) - Observable collections emit changes
Under the Hood:
// Component subscribes to DynamicData observables
ViewModel.ItemsChanged.Subscribe(_ => InvokeAsync(StateHasChanged));
// VM subscribes to reader's observable cache
reader.Values.Connect().Subscribe(changeSet => {
// Process adds, updates, removes
// Update Items collection
// Trigger UI refresh
});@page "/bots"
@using LionFire.Trading.Automation
<div class="pa-6">
<ObservableDataView @ref=ItemsEditor
DataServiceProvider="WorkspaceServices"
TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
AllowedEditModes=EditMode.All
ReadOnly=false
CreatableTypes="@(new[] { typeof(BotEntity) })"
RowClick="@OnRowClick">
<Columns>
<!-- Status column with navigation -->
<TemplateColumn T="BotVM">
<HeaderTemplate>Status</HeaderTemplate>
<CellTemplate>
<MudLink Href="@($"/bots/{context.Item.Key}")">
<MudIcon Icon="@Icons.Material.Outlined.Circle"
Color="@GetStatusColor(context.Item)" />
</MudLink>
</CellTemplate>
</TemplateColumn>
<!-- Toggle switches -->
<TemplateColumn T="BotVM">
<HeaderTemplate>Enabled</HeaderTemplate>
<CellTemplate>
<MudSwitch T="bool"
@bind-Value="context.Item.Value.Enabled"
Color="Color.Primary"
Size="Size.Small" />
</CellTemplate>
</TemplateColumn>
<!-- Standard columns -->
<PropertyColumn Property="x => x.Value.Exchange" />
<PropertyColumn Property="x => x.Value.Symbol" />
<PropertyColumn Property="x => x.Value.Name" />
<!-- Computed column from VM -->
<PropertyColumn Property="x => x.AD" Title="Avg Drawdown" />
</Columns>
<!-- Expandable row details -->
<ChildRowContent>
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">@context.Item.Value.Name</MudText>
</MudCardHeader>
<MudCardContent>
<MudText>@context.Item.Value.Description</MudText>
<MudText>Comments: @context.Item.Value.Comments</MudText>
</MudCardContent>
</MudCard>
</ChildRowContent>
<!-- Right-click menu -->
<ContextMenu>
<MudMenuItem Icon="@Icons.Material.Filled.Delete"
OnClick="@(() => DeleteBot(context))">
Delete @context.Value.Name
</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Edit"
OnClick="@(() => EditBot(context))">
Edit @context.Value.Name
</MudMenuItem>
</ContextMenu>
</ObservableDataView>
</div>
@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
ObservableDataView<string, BotEntity, BotVM>? ItemsEditor { get; set; }
private Color GetStatusColor(BotVM bot)
=> bot.Value.Enabled ? Color.Success : Color.Default;
private void OnRowClick(object sender, DataGridRowClickEventArgs<BotVM> e)
{
NavigationManager.NavigateTo($"/bots/{e.Item.Key}");
}
private void DeleteBot(BotVM bot) { /* ... */ }
private void EditBot(BotVM bot) { /* ... */ }
}Purpose: Similar to ObservableDataView but works directly with SourceCache<TValue, TKey> from DynamicData.
When to Use:
- Data is already in a
SourceCache - Not using
IObservableReader/Writerpattern - Need reactive collection without file persistence
Usage:
<AsyncVMSourceCacheView TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
SourceCache="@MySourceCache">
<Columns>
<!-- Column definitions -->
</Columns>
</AsyncVMSourceCacheView>Purpose: Displays collections with keys, without ViewModels.
When to Use:
- Simple display, no VM needed
- Read-only data
- Don't need MVVM features
Purpose: Displays collections with explicit ViewModels.
When to Use:
- Have pre-created VMs
- Don't need observable reader/writer
- Manual VM lifecycle management
ViewModel used internally by ObservableDataView. Handles:
- Subscribing to
IObservableReader.Values.Connect() - Creating VMs for entities
- Managing observable items collection
- CRUD command coordination
- Edit mode state
Properties:
public IObservableReader<TKey, TValue>? Data { get; set; }
public IObservableCache<TValueVM, TKey> Items { get; }
public IObservable<IChangeSet<TValueVM, TKey>> ItemsChanged { get; }
public bool CanCreate { get; }
public bool CanDelete { get; }
public bool ShowDeleteColumn { get; set; }
public EditMode AllowedEditModes { get; set; }Static helpers for building MudDataGrid columns programmatically.
Methods:
public static void BuildPropertyColumn<TValueVM, TValue>(
RenderTreeBuilder builder,
PropertyInfo prop,
string? propertyPrefix = null)Creates a PropertyColumn for the given property.
Determines which properties should auto-generate columns.
Methods:
public static bool DefaultIsAutoColumn(PropertyInfo prop)
// Returns true for: public, readable, primitive/string types
public static bool DefaultIsAutoEditColumn(PropertyInfo prop)
// Returns true for: public, writeable, primitive/string types<PackageReference Include="MudBlazor" />
<PackageReference Include="ReactiveUI.Blazor" />
<PackageReference Include="DynamicData" /><ProjectReference Include="..\LionFire.Data.Async.Mvvm\" />
<ProjectReference Include="..\LionFire.Reactive\" />
<ProjectReference Include="..\LionFire.Mvvm\" />
<ProjectReference Include="..\LionFire.Blazor.Components\" />Blazor Component (@page "/bots")
↓ Receives via CascadingParameter
WorkspaceServices (IServiceProvider)
↓ Passes to
ObservableDataView (DataServiceProvider parameter)
↓ Resolves
IObservableReader<string, BotEntity>
IObservableWriter<string, BotEntity>
↓ Wraps in
ObservableDataVM<string, BotEntity, BotVM>
↓ Creates
ObservableCache<BotVM, string> (Items)
↓ Binds to
MudDataGrid<BotVM>
File System Change (Bots/bot1.hjson modified)
↓
HjsonFsDirectoryReaderRx (detects change)
↓
IObservableReader.Values.Connect() (emits changeset)
↓
ObservableDataVM (processes changeset)
↓
Items.Edit(changes => ...) (updates cache)
↓
MudDataGrid (reactive binding detects change)
↓
UI Updates
<ObservableDataView TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
DataServiceProvider="@WorkspaceServices"
ReadOnly="true"
AllowedEditModes="EditMode.None">
<Columns>
<!-- Columns -->
</Columns>
</ObservableDataView><ObservableDataView TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
DataServiceProvider="@WorkspaceServices"
AllowedEditModes="EditMode.All"
CreatableTypes="@(new[] { typeof(BotEntity) })"
ReadOnly="false">
<Columns>
<!-- Columns -->
</Columns>
</ObservableDataView><ObservableDataView TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
DataServiceProvider="@WorkspaceServices"
VMFactory="@CreateVM">
<Columns>
<!-- Columns -->
</Columns>
</ObservableDataView>
@code {
private BotVM CreateVM(string key, Optional<BotEntity> entity)
{
if (!entity.HasValue) return null;
var vm = new BotVM(key, entity.Value);
// Custom initialization
vm.LoadAdditionalData();
return vm;
}
}<ObservableDataView ...>
<Columns>
<TemplateColumn>
<CellTemplate>
<MudButton Href="@($"/bots/{context.Item.Key}")">
Edit
</MudButton>
</CellTemplate>
</TemplateColumn>
</Columns>
</ObservableDataView>For large datasets, enable MudDataGrid virtualization:
<ObservableDataView ...>
<!-- MudDataGrid is virtualized by default for performance -->
</ObservableDataView>The component properly manages subscriptions:
- Subscribes in
OnParametersSetAsync - Disposes subscriptions in
DisposeAsync - Uses
CompositeDisposablefor cleanup
Uses DynamicData's efficient change detection:
- Only updates changed items
- Batches multiple changes
- Minimal UI re-renders via
StateHasChangedthrottling
Check:
- Is
DataServiceProviderset? (Should be@WorkspaceServices) - Are
IObservableReader/Writerservices registered in workspace? - Check console for errors
- Verify data exists:
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>(); Console.WriteLine($"Keys: {string.Join(", ", reader?.Keys.Items ?? Enumerable.Empty<string>())}");
Cause: Using root DI container instead of workspace services.
Solution: Pass WorkspaceServices to DataServiceProvider:
<ObservableDataView DataServiceProvider="@WorkspaceServices" ... />Check:
- Does entity implement
INotifyPropertyChanged? - Is entity using
ReactiveObjector similar? - Are properties marked
[Reactive]or raisingPropertyChanged?
// ❌ Wrong - no change notifications
public class BotEntity
{
public string Name { get; set; }
}
// ✅ Right - reactive properties
public partial class BotEntity : ReactiveObject
{
[Reactive] private string _name;
}Check:
- Does VM have constructor:
MyVM(TKey key, TValue value)? - Is VM type public and instantiable?
- Try providing custom
VMFactoryto debug:VMFactory="@((key, entity) => { Console.WriteLine($"Creating VM for {key}"); return new MyVM(key, entity.Value); })"
- Blazor MVVM Patterns - When to use ObservableDataView vs manual pattern
- Workspace Service Scoping - Understanding workspace services
- LionFire.Data.Async.Mvvm - ViewModels and reactive patterns
- LionFire.Reactive - Observable readers/writers
- MudBlazor Documentation - Underlying component library
LionFire.Blazor.Components.MudBlazor provides high-level reactive components that bridge MudBlazor's UI with LionFire's MVVM and reactive persistence patterns.
Primary Component: ObservableDataView - Automatic reactive data grid with CRUD for workspace documents.
Use When: Building list views for workspace-scoped, file-backed entities with minimal code.
Key Benefit: 20-30 lines of code for a fully functional, reactive, CRUD-enabled data grid with workspace integration.