Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 

README.md

UI Patterns & Components

Overview

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.


Documentation Structure

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 CRUD
  • InspectorView - Property grid inspector
  • WorkspaceSelector - Workspace selection UI
  • CascadingT - 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

Quick Start

Pattern 1: Automatic List View (Recommended for Lists)

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

Pattern 2: Manual Detail View (Recommended for Details)

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

Core Concepts

1. Workspace-Scoped Services

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.


2. Reactive Data Binding

LionFire components automatically update when:

  • Entity properties change (via INotifyPropertyChanged)
  • Files are added/removed from workspace
  • Observable collections emit changes

Requirements:

  1. Entity must implement INotifyPropertyChanged
  2. Use ReactiveObject or [ObservableProperty]
  3. 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!
}

3. ViewModel Layer

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)

4. Component Hierarchy

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)

Patterns Summary

When to Use Each Pattern

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)

Architecture Integration

Layer Stack

UI Layer (Blazor Components)
    ↓ uses
ViewModel Layer (KeyValueVM, ObservableReaderWriterItemVM)
    ↓ wraps
Reactive Persistence Layer (IObservableReader/Writer)
    ↓ backed by
File System (HJSON files in workspace)

Service Resolution Flow

Blazor Component
    ↓ CascadingParameter
WorkspaceServices (IServiceProvider)
    ↓ GetService<T>()
IObservableReader<TKey, TValue>
    ↓ Points to
workspace1/Bots/ directory

Best Practices

1. Prefer ObservableDataView for Lists

<!-- ✅ 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...
    }
}

2. Use Manual Pattern for Detail Views

<!-- ✅ 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 -->

3. Cascade WorkspaceServices, Not Individual Services

<!-- ✅ 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>

4. Use ReactiveObject for Entities

// ✅ Good - Reactive notifications
public partial class BotEntity : ReactiveObject
{
    [Reactive] private string? _name;
}

// ❌ Avoid - No change notifications
public class BotEntity
{
    public string? Name { get; set; }
}

5. Dispose Subscriptions Properly

@implements IAsyncDisposable

@code {
    IDisposable? subscription;

    protected override void OnInitialized()
    {
        subscription = reader.Values.Connect().Subscribe(changes => {
            // Handle changes
        });
    }

    public async ValueTask DisposeAsync()
    {
        subscription?.Dispose();
    }
}

Common Scenarios

Scenario 1: Simple Read-Only List

<ObservableDataView TKey="string"
                    TValue="Config"
                    TValueVM="ConfigVM"
                    DataServiceProvider="@WorkspaceServices"
                    ReadOnly="true">
    <Columns>
        <PropertyColumn Property="x => x.Value.Name" />
    </Columns>
</ObservableDataView>

Scenario 2: Editable List with Add/Delete

<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>

Scenario 3: Master-Detail Navigation

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;
}

Scenario 4: Custom Column Templates

<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>

Troubleshooting

Issue: "Unable to resolve service IObservableReader"

Cause: Using root DI container instead of workspace services.

Fix:

<!-- ❌ Wrong -->
<ObservableDataView DataServiceProvider="@ServiceProvider" />

<!-- ✅ Correct -->
<ObservableDataView DataServiceProvider="@WorkspaceServices" />

Issue: UI not updating when entity changes

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;
}

Issue: Component shows empty grid

Check:

  1. Is DataServiceProvider set to WorkspaceServices?
  2. Are files present in workspace directory?
  3. Is entity type registered with AddWorkspaceChildType<T>()?
  4. Check console for errors

Debug:

var reader = WorkspaceServices.GetService<IObservableReader<string, Bot>>();
Console.WriteLine($"Keys: {string.Join(", ", reader?.Keys.Items)}");

Related Documentation

Architecture

Project Documentation

Guides


Summary

LionFire's Blazor UI toolkit provides two primary patterns:

Automatic Pattern (Lists)

  • Component: ObservableDataView
  • Boilerplate: Minimal (~20 lines)
  • Use for: Lists, grids, CRUD

Manual Pattern (Details)

  • 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:

  1. Read Blazor MVVM Patterns for detailed examples
  2. Browse Component Catalog for available components
  3. Check Reactive UI Updates for performance optimization