This PR stabilizes the Nexus Ingestion Engine by implementing functional service proxies for the Blazor WASM client and refining the backend infrastructure for real-time progress tracking and database compatibility. ### Key Changes - **Infrastructure Stabilization**: - Implemented production-grade `EbookRepository` with PostgreSQL `EF.Functions.ILike` support. - Enforced `IsReadyForReading = false` state for newly added ebooks (resolves #35). - Updated `SignalRSyncBroadcaster` to support targeted user messaging and ingestion-specific progress updates (resolves #37). - **WASM Client Functional Proxies**: - Replaced "Throwing" dummy services with `WasmEbookRepository`, `WasmSyncBroadcaster`, `WasmBookStorageService`, and `WasmEmbeddingGenerator`. - These services proxy requests to the backend via a new set of Minimal API endpoints in `NexusReader.Web`. - **Domain Refinement**: - Added `IsReadyForReading` flag to the `Ebook` entity to manage background AI processing states. ### Related Issues - Fixes #35 - Fixes #36 - Fixes #37 --------- Co-authored-by: Marek Jasiński <jasins.marek@gmail.com> Reviewed-on: #42 Co-authored-by: Antigravity <antigravity@google.com> Co-committed-by: Antigravity <antigravity@google.com>
12 KiB
Blazor Components & Component Lifecycle
Component Structure
Components are the fundamental building blocks of Blazor applications. A Blazor component is a self-contained piece of UI with an optional logic.
Basic Component Syntax
@page "/example"
@using MyApp.Services
@inject IMyService MyService
<h3>@Title</h3>
<div>@ChildContent</div>
<button @onclick="HandleClick">Click me</button>
@code {
[Parameter]
public string Title { get; set; } = "Default";
[Parameter]
public RenderFragment? ChildContent { get; set; }
private void HandleClick()
{
// Handle button click
}
}
Key elements:
@pagedirective: Makes component routable (optional for non-page components)@using: Import namespaces@inject: Dependency injection- HTML markup: Regular HTML with Blazor directives
@codeblock: C# logic including lifecycle methods
Component vs Page
-
Page Component: Has
@pagedirective, routable via URL- Example:
/Counterroute - Located in
Pages/folder (convention)
- Example:
-
Reusable Component: No
@pagedirective, used by other components- Example:
<UserCard @bind-User="user" /> - Located in
Shared/or domain-specific folder
- Example:
Component Lifecycle
Lifecycle Sequence
Component lifecycle methods execute in this order:
1. SetParametersAsync()
↓
2. OnInitialized() or OnInitializedAsync()
↓
3. OnParametersSet() or OnParametersSetAsync()
↓
4. ShouldRender() [decision point - skip if returns false]
↓
5. OnAfterRender() or OnAfterRenderAsync()
When parameters change (parent re-renders):
SetParametersAsync() [parameters updated]
↓
OnParametersSet() [NOT OnInitialized - that runs once only]
↓
ShouldRender()
↓
OnAfterRender()
Lifecycle Methods Detailed
SetParametersAsync()
- When: First method called, before initialization
- Purpose: Set component parameters
- Usage: Rarely overridden, use OnInitialized instead
- Code Example:
public override async Task SetParametersAsync(ParameterView parameters)
{
// Custom parameter processing if needed
await base.SetParametersAsync(parameters);
}
OnInitialized / OnInitializedAsync()
- When: Once per component lifetime, after parameters set
- Purpose: Initialize component state, load data
- Runs: Only ONCE, even if parameters change
- Code Example:
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
data = await Service.LoadDataAsync();
}
Common Uses:
- Load initial data from API
- Set up subscriptions
- Initialize state based on parameters
OnParametersSet / OnParametersSetAsync()
- When: After parameters set, runs EVERY time parameters change
- Purpose: React to parameter changes
- Runs: Every time parent re-renders with different values
- Code Example:
protected override async Task OnParametersSetAsync()
{
await base.OnParametersSetAsync();
if (UserId != previousUserId)
{
data = await Service.LoadUserDataAsync(UserId);
previousUserId = UserId;
}
}
Common Uses:
- Update UI based on new parameter values
- Fetch new data when ID parameter changes
- React to cascading parameter changes
ShouldRender()
- When: Before DOM rendering, decision point
- Purpose: Optimize rendering by skipping unnecessary renders
- Returns: true (render) or false (skip)
- Code Example:
protected override bool ShouldRender()
{
// Only render if specific field changed
return hasChanged;
}
Common Optimizations:
- Skip render if data unchanged
- Prevent re-render from external events
- Implement custom change detection
OnAfterRender / OnAfterRenderAsync()
- When: After component rendered to DOM
- Purpose: Work with DOM, initialize JS libraries, final setup
- Parameter:
firstRender- true only on first render - Code Example:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
// Initialize JS library only once
await JS.InvokeVoidAsync("initializeChart", elementRef);
}
}
Critical Use Case:
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// Load JS module
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./scripts/myScript.js");
// Initialize library
await module.InvokeVoidAsync("setupChart", element);
}
}
Important: Always use firstRender check for one-time initialization. This prevents re-initializing on every parameter change.
Component Parameters
Parameter Declaration
@code {
// Simple parameter
[Parameter]
public string Title { get; set; } = "Default";
// Required parameter (C# 11+)
[Parameter, EditorRequired]
public int UserId { get; set; }
// Child content
[Parameter]
public RenderFragment? ChildContent { get; set; }
// Event callback
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
// Cascading parameter
[CascadingParameter]
public ThemeInfo? CurrentTheme { get; set; }
}
Parameter Best Practices
Use Clear Names:
// ✅ Good - clear intent
[Parameter]
public bool IsVisible { get; set; }
// ❌ Poor - ambiguous
[Parameter]
public bool State { get; set; }
Use Nullable Types for Optional:
// ✅ Good - nullable indicates optional
[Parameter]
public string? OptionalValue { get; set; }
// ✅ Good - default value
[Parameter]
public int MaxItems { get; set; } = 10;
// ❌ Poor - not clear if optional
[Parameter]
public string RequiredValue { get; set; }
Use [EditorRequired] for Required Parameters (C# 11+):
// ✅ Best practice - compiler enforces, IDE warns
[Parameter, EditorRequired]
public string Title { get; set; } = default!;
// Fallback for older C#
[Parameter]
public string Title { get; set; } = default!;
Use EventCallback for Child-to-Parent Communication:
// ✅ Correct - EventCallback for async safety
[Parameter]
public EventCallback<string> OnValueChanged { get; set; }
// ✅ With custom args
[Parameter]
public EventCallback<ValueChangeEventArgs> OnValueChanged { get; set; }
// ❌ Avoid - direct Action, not async-safe
[Parameter]
public Action<string>? OnValueChanged { get; set; }
Parameter Change Detection
To know when a parameter changed:
@code {
private int previousUserId;
[Parameter]
public int UserId { get; set; }
protected override async Task OnParametersSetAsync()
{
if (UserId != previousUserId)
{
previousUserId = UserId;
await LoadUserData();
}
}
}
Or use a comparison strategy:
private object? previousCriteria;
protected override async Task OnParametersSetAsync()
{
var currentCriteria = (SearchId, SearchTerm);
if (!Equals(previousCriteria, currentCriteria))
{
previousCriteria = currentCriteria;
await PerformSearch();
}
}
Cascading Values
Cascading values allow ancestor components to provide data to all descendants without explicit parameter passing.
Providing Cascading Values
<!-- Parent component -->
<CascadingValue Value="@currentUser">
@ChildContent
</CascadingValue>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
private User currentUser = new();
}
Receiving Cascading Values
<!-- Child component anywhere in hierarchy -->
@code {
[CascadingParameter]
public User? CurrentUser { get; set; }
protected override void OnInitialized()
{
if (CurrentUser == null)
{
// Handle missing cascading value
}
}
}
Multiple Cascading Values
<!-- Provider -->
<CascadingValue Value="@theme">
<CascadingValue Value="@currentUser">
<CascadingValue Value="@permissions">
@ChildContent
</CascadingValue>
</CascadingValue>
</CascadingValue>
<!-- Consumer - multiple parameters -->
@code {
[CascadingParameter]
public Theme? Theme { get; set; }
[CascadingParameter]
public User? CurrentUser { get; set; }
[CascadingParameter]
public Permissions? Permissions { get; set; }
}
Named Cascading Values
For disambiguation when multiple values of same type:
<!-- Provider -->
<CascadingValue Value="@themeLight" Name="Light">
<CascadingValue Value="@themeDark" Name="Dark">
@ChildContent
</CascadingValue>
</CascadingValue>
<!-- Consumer -->
@code {
[CascadingParameter(Name = "Light")]
public Theme? LightTheme { get; set; }
[CascadingParameter(Name = "Dark")]
public Theme? DarkTheme { get; set; }
}
RenderFragment for Component Composition
RenderFragment enables flexible component composition.
Basic RenderFragment
<!-- Parent component -->
<div>
<h2>Header</h2>
@ChildContent
<footer>Footer</footer>
</div>
@code {
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
<!-- Usage -->
<Layout>
<p>This is the main content</p>
</Layout>
Typed RenderFragment with Context
<!-- ListComponent.razor -->
@foreach (var item in Items)
{
@ItemTemplate(item)
}
@code {
[Parameter]
public IEnumerable<Item> Items { get; set; } = [];
[Parameter]
public RenderFragment<Item>? ItemTemplate { get; set; }
}
<!-- Usage -->
<ListComponent Items="@items">
<ItemTemplate Context="item">
<div>@item.Name - @item.Price</div>
</ItemTemplate>
</ListComponent>
Multiple Named Content Areas
<!-- Card component with multiple slots -->
<div class="card">
<div class="card-header">@Header</div>
<div class="card-body">@Body</div>
<div class="card-footer">@Footer</div>
</div>
@code {
[Parameter]
public RenderFragment? Header { get; set; }
[Parameter]
public RenderFragment? Body { get; set; }
[Parameter]
public RenderFragment? Footer { get; set; }
}
<!-- Usage -->
<Card>
<Header>
<h3>Card Title</h3>
</Header>
<Body>
<p>Card content</p>
</Body>
<Footer>
<button>Action</button>
</Footer>
</Card>
Component Best Practices
Single Responsibility
- Each component should have one clear purpose
- Avoid god components that do too much
- Example:
UserProfilecomponent should focus on displaying user info, not handle complex business logic
Composition Over Inheritance
- Use cascading values for shared state, not deep hierarchies
- Compose components rather than creating base classes
- Example: Create theme provider component instead of theme-aware base class
Keep Components Simple
- Minimize
@codeblock logic - Move complex logic to services
- Example: Validation logic → ValidationService, not in component
Proper Disposal
- Implement
IDisposableorIAsyncDisposable - Unsubscribe from events
- Dispose timers and resources
@implements IAsyncDisposable
@inject IJSRuntime JS
private IJSObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
module = await JS.InvokeAsync<IJSObjectReference>(
"import", "./myScript.js");
}
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
await module.DisposeAsync();
}
}
Related Resources: See state-management-events.md for event handling and state updates. See performance-advanced.md for optimization techniques.