Guide for implementing LionFire workspace documents with Blazor MVVM patterns, including ObservableDataView component usage, workspace-scoped service injection, and reactive persistence. Use this skill when creating Blazor pages for workspace documents, fixing workspace service scoping issues, or implementing list/detail views with ObservableReader/Writer.
How this skill is triggered — by the user, by Claude, or both
Slash command
/lionfire-core-patterns:workspace-blazor-mvvmThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill when:
Use this skill when:
ObservableDataView component for list viewsIObservableReader/Writer) in BlazorKeywords: workspace, ObservableDataView, IObservableReader, IObservableWriter, workspace services, CascadingParameter, Blazor MVVM, workspace scoping, workspace documents
The Problem: IObservableReader/Writer services are workspace-scoped, not in the root DI container.
The Solution: Use CascadingParameter to get WorkspaceServices and resolve services manually.
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>();
See: references/service-scoping.md for complete explanation.
When: Displaying a list/grid of workspace documents with CRUD operations.
Code (~20-30 lines):
<ObservableDataView TKey="string"
TValue="BotEntity"
TValueVM="BotVM"
DataServiceProvider="@WorkspaceServices">
<Columns>
<PropertyColumn Property="x => x.Value.Name" />
</Columns>
</ObservableDataView>
@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
}
See: examples/list-view-example.razor for complete example.
When: Displaying/editing a single workspace document with custom layout.
Code (~60-80 lines):
@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
private ObservableReaderWriterItemVM<string, BotEntity, BotVM>? VM { get; set; }
protected override async Task OnParametersSetAsync()
{
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;
}
}
See: examples/detail-view-example.razor for complete example.
When encountering:
InvalidOperationException: Unable to resolve service for type
'IObservableReader`2[System.String,MyEntity]'
Steps:
Identify the issue: Component trying to inject workspace-scoped services from root container
Check current pattern:
// ❌ WRONG: Tries to inject from root
@inherits ReactiveInjectableComponentBase<ObservableReaderWriterItemVM<string, BotEntity, BotVM>>
// ✅ RIGHT: Get workspace services via cascading parameter
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
protected override async Task OnParametersSetAsync()
{
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>();
var writer = WorkspaceServices.GetService<IObservableWriter<string, BotEntity>>();
VM = new ObservableReaderWriterItemVM<string, BotEntity, BotVM>(reader, writer);
VM.Id = ItemId;
}
// In Program.cs, ensure:
services
.AddWorkspaceChildType<BotEntity>() // ← Must have this!
.AddWorkspaceDocumentService<string, BotEntity>();
For detailed explanation: Load references/service-scoping.md.
Goal: Display a reactive grid of workspace documents with CRUD operations.
Steps:
[Alias("Bot")]
public partial class BotEntity : ReactiveObject
{
[Reactive] private string? _name;
}
public class BotVM : KeyValueVM<string, BotEntity>
{
public BotVM(string key, BotEntity value) : base(key, value) { }
}
services
.AddWorkspaceChildType<BotEntity>()
.AddWorkspaceDocumentService<string, BotEntity>()
.AddTransient<BotVM>();
Create list page using ObservableDataView:
examples/list-view-example.razorDataServiceProvider="@WorkspaceServices"Customize columns as needed (PropertyColumn, TemplateColumn)
Add navigation to detail page:
<MudButton Href="@($"/bots/{context.Item.Key}")">Edit</MudButton>
For complete pattern details: Load references/blazor-mvvm-patterns.md.
Goal: Display/edit a single workspace document.
Steps:
@page "/bots/{BotId}"
@code {
[Parameter]
public string? BotId { get; set; }
}
@inject ILogger<Bot> Logger
@inject IServiceProvider ServiceProvider
@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
}
Resolve reader/writer and create VM:
examples/detail-view-example.razorObservableReaderWriterItemVMBind UI to VM.Value:
<MudTextField @bind-Value="VM.Value.Name" Label="Name" />
private async Task Save() => await VM.Write();
For complete pattern details: Load references/blazor-mvvm-patterns.md.
Goal: Add a new document type (e.g., Portfolio) to existing workspace application.
Steps:
[Alias("Portfolio")]
public partial class PortfolioEntity : ReactiveObject
{
[Reactive] private string? _name;
// ... other properties
}
public class PortfolioVM : KeyValueVM<string, PortfolioEntity>
{
public PortfolioVM(string key, PortfolioEntity value) : base(key, value) { }
}
services
.AddWorkspaceChildType<PortfolioEntity>()
.AddWorkspaceDocumentService<string, PortfolioEntity>()
.AddTransient<PortfolioVM>();
Create pages (follow Workflow 2 and 3 above)
Result: Portfolios appear in workspace subdirectory Portfolios/
Purpose: Pre-built MudDataGrid component with reactive workspace data integration.
Key Parameters:
TKey, TValue, TValueVM - Type parametersDataServiceProvider - CRITICAL: Pass @WorkspaceServices hereAllowedEditModes - EditMode.None, .Cell, .Form, .AllReadOnly - Enable/disable editingColumns - Custom column definitionsAutomatic Features:
Purpose: ViewModel wrapper for single workspace documents.
Constructor:
public ObservableReaderWriterItemVM(
IObservableReader<TKey, TValue> reader,
IObservableWriter<TKey, TValue> writer)
Key Properties:
Id - The document key (triggers load when set)Value - The wrapped entity (as TValueVM)Writer - Access to persistenceKey Methods:
Write() - Save changes to file// ❌ WRONG
@inject IObservableReader<string, BotEntity> Reader
// ✅ RIGHT
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
var reader = WorkspaceServices.GetService<IObservableReader<string, BotEntity>>();
<!-- ❌ WRONG -->
<ObservableDataView TKey="string" TValue="BotEntity" TValueVM="BotVM" />
<!-- ✅ RIGHT -->
<ObservableDataView TKey="string" TValue="BotEntity" TValueVM="BotVM"
DataServiceProvider="@WorkspaceServices" />
// ❌ WRONG - Missing AddWorkspaceChildType
services.AddWorkspaceDocumentService<string, BotEntity>();
// ✅ RIGHT
services
.AddWorkspaceChildType<BotEntity>() // ← MUST have this
.AddWorkspaceDocumentService<string, BotEntity>();
Detailed documentation (load as needed):
references/service-scoping.md - Deep dive on workspace service scoping and DI
references/blazor-mvvm-patterns.md - Complete Blazor MVVM pattern guide
references/workspaces-architecture.md - Workspace architecture overview
Need to display workspace documents?
├─ Multiple items (list/grid)?
│ └─ Use ObservableDataView ✅
│ → See examples/list-view-example.razor
└─ Single item (detail/edit)?
└─ Use Manual VM Pattern ✅
→ See examples/detail-view-example.razor
Getting "Unable to resolve service" error?
└─ Using @inject instead of CascadingParameter?
└─ Fix: Use CascadingParameter for WorkspaceServices
→ See references/service-scoping.md
This skill teaches two complementary patterns:
Both require: CascadingParameter to access workspace-scoped services.
Most Common Issue: Trying to inject workspace services from root DI container.
Solution: Always use CascadingParameter(Name = "WorkspaceServices").
Guides reception of code review feedback: verify before implementing, avoid performative agreement, push back with technical reasoning when needed.
Design banners for social media, ads, website heroes, and print with multiple art direction options and AI-generated visuals.
npx claudepluginhub lionfire/core --plugin lionfire-core-patterns