Blazor UI components for workspace management, including layout ViewModels, workspace selection, and grid-based workspace display. This library provides the UI layer for the LionFire workspace system, integrating with MudBlazor and BlazorGridStack for rich user experiences.
Purpose: Enable users to select, navigate, and visualize workspaces through Blazor components.
Location: UI/Workspaces/Layouts/WorkspaceLayoutVM.cs
Purpose: ViewModel managing workspace lifecycle, service scoping, and user-workspace relationships.
Inheritance: UserLayoutVM (from LionFire.Blazor.Components)
- User-to-Workspace Mapping: Sets up workspace directories based on logged-in user
- Workspace Service Provider Creation: Builds workspace-scoped
IServiceProvider - Configurator Invocation: Calls all registered
IWorkspaceServiceConfiguratorinstances - Service Cascading: Provides
WorkspaceServicesto descendant components
public class WorkspaceLayoutVM : UserLayoutVM
{
// Dependencies
public IServiceProvider ServiceProvider { get; }
public AppInfo AppInfo { get; }
public IEnumerable<IWorkspaceServiceConfigurator> WorkspaceServiceConfigurators { get; }
// User Workspaces
public string? WorkspacesDir { get; } // Base directory for user's workspaces
public bool WorkspacesAvailable { get; }
// Active Workspace
public string? WorkspaceId { get; set; } // Currently selected workspace
public string EffectiveWorkspaceName { get; } // Display name
// Workspace Services
public IServiceProvider? WorkspaceServices { get; set; } // Workspace-scoped provider
}User Logs In
↓
UserChanged event
↓
ConfigureUserServices()
↓ Sets UserWorkspacesDir based on user
↓ Registers IObservableReaderWriter<string, Workspace>
UserServices created
↓
User Selects Workspace
↓
WorkspaceId property set
↓
OnWorkspaceChanged()
↓
DoConfigureWorkspaceServices()
↓ Invokes all IWorkspaceServiceConfigurator instances
↓ Builds workspace-scoped IServiceProvider
WorkspaceServices created
↓ Cascaded to child components
Components receive workspace services
Layout Component:
@inherits LayoutComponentBase
@inject WorkspaceLayoutVM LayoutVM
<CascadingValue Name="UserServices" Value="@LayoutVM.UserServices">
<CascadingValue Name="WorkspaceServices" Value="@LayoutVM.WorkspaceServices">
<!-- Workspace Selector -->
<WorkspaceSelector @bind-SelectedId="@LayoutVM.WorkspaceId"
VM="@LayoutVM" />
<!-- Page Content -->
@Body
</CascadingValue>
</CascadingValue>
@code {
protected override async Task OnInitializedAsync()
{
await LayoutVM.InitializeAsync();
}
}Default Path (configured automatically):
C:\ProgramData\{AppInfo.OrgDir}\{AppInfo.EffectiveDataDirName}\Users\{Username}\Workspaces\
Example:
C:\ProgramData\LionFire\Trading\Users\Alice\Workspaces\
├── workspace1.hjson ← Workspace metadata
├── workspace1\ ← Workspace directory
│ ├── Bots\
│ └── Portfolios\
└── workspace2.hjson
└── workspace2\
Custom Workspace Path:
public class MyWorkspaceLayoutVM : WorkspaceLayoutVM
{
protected override async ValueTask ConfigureUserServices(IServiceCollection services)
{
await base.ConfigureUserServices(services);
// Override default path
userWorkspacesService.UserWorkspacesDir = GetCustomWorkspacePath(EffectiveUserName);
}
private string GetCustomWorkspacePath(string username)
{
// Custom logic for workspace location
return Path.Combine("D:\\MyWorkspaces", username);
}
}Additional Workspace Services:
protected override async ValueTask ConfigureWorkspaceServices(
IServiceCollection services,
string? workspaceId)
{
await base.ConfigureWorkspaceServices(services, workspaceId);
// Register custom workspace-scoped services
services.AddScoped<IMyWorkspaceService, MyWorkspaceService>();
}Location: UI/Workspaces/WorkspaceSelector.razor
Purpose: Dropdown selector for choosing the active workspace from available user workspaces.
[CascadingParameter(Name = "UserServices")]
public IServiceProvider? UserServices { get; set; }
[Parameter]
public string? SelectedId { get; set; }
[Parameter]
public EventCallback<string?> SelectedIdChanged { get; set; }
[Parameter]
public WorkspaceLayoutVM? VM { get; set; }<WorkspaceSelector @bind-SelectedId="@currentWorkspaceId" />
@code {
string? currentWorkspaceId;
}- Automatic Workspace Discovery: Reads available workspaces from
UserServices - Two-Way Binding: Updates
SelectedIdwhen user changes selection - Reactive Updates: Automatically updates when workspaces are added/removed
- MudBlazor Integration: Uses
MudSelectcomponent
// Resolves workspace reader from UserServices
Workspaces = UserServices?.GetRequiredService<IObservableReaderWriter<string, Workspace>>();
// Displays all workspace keys
foreach (var key in Workspaces.Keys.Keys)
{
<MudSelectItem Value="@key">@key</MudSelectItem>
}
// Fires SelectedIdChanged event
private string? internalSelectedId
{
set
{
currentWorkspace = value;
await SelectedIdChanged.InvokeAsync(currentWorkspace);
}
}If no workspace selected and workspaces available, automatically selects first workspace:
protected override async Task OnParametersSetAsync()
{
if (SelectedId == null && Workspaces?.Keys.Count > 0)
{
internalSelectedId = Workspaces.Keys.Keys.First();
}
}Location: UI/Workspaces/WorkspaceGrid.razor
Purpose: Grid-based layout for displaying workspace widgets using BlazorGridStack.
Status: Experimental / Work in Progress
- Grid-Based Layout: Uses BlazorGridStack for draggable, resizable widgets
- Dynamic Widgets: Add widgets at runtime
- Persistent Layout: (Future) Save/restore grid layout
@page "/workspaces/{WorkspaceName}"
<WorkspaceGrid WorkspaceName="@WorkspaceName" /><BlazorGridStackBody GridStackOptions="@(new() { Class = "DashboardGrid" })">
<BlazorGridStackWidget WidgetOptions="@(new() {X = 3, Y = 2})">
<div class="tile">
<!-- Widget content -->
</div>
</BlazorGridStackWidget>
</BlazorGridStackBody>Note: This component is experimental and may undergo significant changes.
Location: UI/Workspaces/WorkspaceNavMenu.razor
Purpose: Navigation menu for workspace-related routes.
Typical Usage: Sidebar navigation in workspace layout.
Location: UI/Workspaces/Workspaces.razor
Purpose: Main workspaces page/component.
Location: UI/Documents/DocumentSelector.razor
Purpose: Selector component for choosing documents within a workspace.
Complete layout with user and workspace scoping:
<!-- MainLayout.razor -->
@inherits LayoutComponentBase
@inject WorkspaceLayoutVM LayoutVM
@inject AuthenticationStateProvider AuthProvider
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<MudLayout>
<!-- Top Bar -->
<MudAppBar Elevation="1">
<MudText Typo="Typo.h6">My Application</MudText>
<MudSpacer />
<WorkspaceSelector @bind-SelectedId="@LayoutVM.WorkspaceId"
VM="@LayoutVM" />
</MudAppBar>
<!-- Sidebar -->
<MudDrawer Open="true" Elevation="1">
<WorkspaceNavMenu />
</MudDrawer>
<!-- Main Content -->
<MudMainContent Class="pa-4">
<CascadingValue Name="UserServices" Value="@LayoutVM.UserServices">
<CascadingValue Name="WorkspaceServices" Value="@LayoutVM.WorkspaceServices">
@Body
</CascadingValue>
</CascadingValue>
</MudMainContent>
</MudLayout>
@code {
protected override async Task OnInitializedAsync()
{
// Initialize layout VM
await LayoutVM.InitializeAsync();
}
}Quick workspace switching in header:
<MudMenu Icon="@Icons.Material.Filled.Workspaces" Label="@currentWorkspaceName">
@if (workspaces != null)
{
foreach (var workspace in workspaces)
{
<MudMenuItem OnClick="@(() => SwitchWorkspace(workspace.Key))">
@workspace.Key
</MudMenuItem>
}
}
<MudDivider />
<MudMenuItem OnClick="@CreateNewWorkspace">
<MudIcon Icon="@Icons.Material.Filled.Add" /> New Workspace
</MudMenuItem>
</MudMenu>
@code {
IEnumerable<KeyValuePair<string, Workspace>>? workspaces;
private void SwitchWorkspace(string workspaceId)
{
LayoutVM.WorkspaceId = workspaceId;
}
}Dashboard showing workspace info:
@page "/dashboard"
<MudGrid>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">Active Workspace</MudText>
</MudCardHeader>
<MudCardContent>
<MudText>Workspace: @LayoutVM.EffectiveWorkspaceName</MudText>
<MudText>Path: @LayoutVM.WorkspacesDir</MudText>
</MudCardContent>
</MudCard>
</MudItem>
<MudItem xs="12" md="6">
<MudCard>
<MudCardHeader>
<MudText Typo="Typo.h6">Workspace Documents</MudText>
</MudCardHeader>
<MudCardContent>
<!-- Show document counts, recent items, etc. -->
</MudCardContent>
</MudCard>
</MudItem>
</MudGrid>
@code {
[CascadingParameter(Name = "WorkspaceServices")]
public IServiceProvider? WorkspaceServices { get; set; }
[Inject]
public WorkspaceLayoutVM LayoutVM { get; set; }
}┌──────────────────────────────────────────────────────────────┐
│ MainLayout.razor │
│ - Injects WorkspaceLayoutVM │
└───────────────────────┬──────────────────────────────────────┘
│
↓ Cascades
┌──────────────────────────────────────────────────────────────┐
│ CascadingValue: UserServices │
│ - IObservableReaderWriter<string, Workspace> │
│ - User-scoped services │
└───────────────────────┬──────────────────────────────────────┘
│
↓ Cascades
┌──────────────────────────────────────────────────────────────┐
│ CascadingValue: WorkspaceServices │
│ - IObservableReader<string, BotEntity> │
│ - IObservableWriter<string, BotEntity> │
│ - Workspace-scoped services │
└───────────────────────┬──────────────────────────────────────┘
│
↓ Available to
┌──────────────────────────────────────────────────────────────┐
│ Page Components │
│ - Receive services via CascadingParameter │
│ - Use ObservableDataView or manual VM patterns │
└──────────────────────────────────────────────────────────────┘
Application Start
↓
WorkspaceLayoutVM created (injected)
↓
User Authenticates
↓
OnUserChanged()
↓ ConfigureUserServices()
↓ Set UserWorkspacesDir
↓ Register IObservableReaderWriter<Workspace>
UserServices = serviceCollection.BuildServiceProvider()
↓
WorkspaceSelector displays available workspaces
↓
User Selects Workspace
↓
WorkspaceId property set
↓
OnWorkspaceChanged()
↓ DoConfigureWorkspaceServices()
↓ Invoke IWorkspaceServiceConfigurator[]
↓ Register workspace-scoped services
WorkspaceServices = serviceCollection.BuildServiceProvider()
↓
Components receive WorkspaceServices
↓
Components resolve IObservableReader/Writer
↓
Data loaded and displayed
<PackageReference Include="BlazorGridStack" />
<PackageReference Include="Microsoft.AspNetCore.Components.Web" />
<PackageReference Include="ReactiveUI.SourceGenerators" /><ProjectReference Include="..\LionFire.Blazor.Components.MudBlazor\" />
<ProjectReference Include="..\LionFire.Blazor.Components.UI\" />
<ProjectReference Include="..\LionFire.Reactive\" />
<ProjectReference Include="..\LionFire.Structures\" />
<ProjectReference Include="..\LionFire.Workspaces.UI\" />- Workspace Architecture - Overall workspace concepts
- Service Scoping - Service provider hierarchy
- Integration Diagram - Complete flow diagrams
- Blazor MVVM Patterns - When to use which UI pattern
- Component Catalog - All available components
- LionFire.Workspaces - Core workspace implementation
- LionFire.Workspaces.Abstractions - Workspace interfaces
- LionFire.Blazor.Components.MudBlazor - ObservableDataView and components
Startup Configuration:
// Program.cs
builder.Services
.AddWorkspacesModel()
.AddWorkspaceChildType<BotEntity>()
.AddWorkspaceChildType<Portfolio>()
.AddSingleton<WorkspaceLayoutVM>();Layout:
<!-- MainLayout.razor -->
<CascadingValue Name="UserServices" Value="@LayoutVM.UserServices">
<CascadingValue Name="WorkspaceServices" Value="@LayoutVM.WorkspaceServices">
<WorkspaceSelector @bind-SelectedId="@LayoutVM.WorkspaceId" />
@Body
</CascadingValue>
</CascadingValue>Custom Layout VM:
public class MultiUserWorkspaceLayoutVM : WorkspaceLayoutVM
{
protected override async ValueTask OnUserChanged()
{
await base.OnUserChanged();
// Custom per-user initialization
await LoadUserPreferences(EffectiveUserName);
await InitializeUserWorkspaces(EffectiveUserName);
}
}Create from Template:
public class WorkspaceTemplateService
{
public async Task CreateWorkspaceFromTemplate(
string workspaceName,
string templateName)
{
var templatePath = GetTemplatePath(templateName);
var workspacePath = GetWorkspacePath(workspaceName);
// Copy template structure
await CopyDirectory(templatePath, workspacePath);
// Initialize workspace metadata
var workspace = new Workspace { Name = workspaceName };
await SaveWorkspaceMetadata(workspace);
}
}LionFire.Workspaces.UI.Blazor provides the Blazor UI layer for workspace management:
Key Components:
- WorkspaceLayoutVM - Manages workspace lifecycle and service scoping
- WorkspaceSelector - UI for selecting active workspace
- WorkspaceGrid - Grid-based workspace layout (experimental)
Key Features:
- User-to-workspace directory mapping
- Workspace service provider creation and cascading
- Integration with IWorkspaceServiceConfigurator pattern
- MudBlazor and BlazorGridStack integration
Use This Library: When building Blazor applications with multi-workspace support and workspace-scoped services.
Integration: Works seamlessly with LionFire.Workspaces (core) and LionFire.Blazor.Components.MudBlazor (components).