Service vs Markup Overlays
Lumeo lets you open Dialog, Sheet, Drawer, and Alert Dialog two ways: declare them in markup
with <Sheet @bind-Open>,
or call OverlayService.ShowSheetAsync(...)
and await a result. Both
paths render into the same OverlayProvider
and look identical to the user. The difference is who owns the lifecycle and where the result lives.
TL;DR
| Use | When |
|---|---|
Markup <Sheet> |
The overlay belongs to a specific page or component and shares its state (e.g. an edit form bound to the selected row in a table). |
OverlayService |
You want to await a result from procedural code (event handler, command, service), or open the same overlay from many call sites. |
The markup pattern
You declare the overlay alongside the trigger and bind Open
to a field. The overlay's content has direct access to the component's state, services, and
cascading values — no parameter plumbing.
@inject ProjectService Projects
<Button OnClick="() => _editOpen = true">Edit project</Button>
<Sheet @bind-Open="_editOpen">
<SheetContent Side="Lumeo.Side.Right" Size="SheetContent.SheetSize.Default">
<SheetHeader><SheetTitle>Edit @_project.Name</SheetTitle></SheetHeader>
<div class="flex-1 overflow-y-auto p-6">
<EditForm Model="@_project" OnValidSubmit="SaveAsync">
<Input @bind-Value="_project.Name" />
@* binds directly to the parent's _project field *@
</EditForm>
</div>
</SheetContent>
</Sheet>
@code {
[Parameter] public Project _project { get; set; } = default!;
private bool _editOpen;
private async Task SaveAsync()
{
await Projects.SaveAsync(_project);
_editOpen = false;
}
}
Best for: edit forms tied to a selected row, inspect panels, contextual previews. Worst for: anything you'd want to call from a static helper or a service.
The service pattern
Inject OverlayService,
pass a content component type plus parameters, and await
the result. The content component closes itself via the cascading
OverlayReference.
@inject IOverlayService Overlays
<Button OnClick="CreateAsync">New project</Button>
@code {
private async Task CreateAsync()
{
var result = await Overlays.ShowSheetAsync<NewProjectForm>(
title: "New project",
side: Lumeo.Side.Right,
size: SheetSize.Default);
if (!result.Cancelled && result.GetData<Project>() is { } p)
{
// navigate, refresh list, etc.
}
}
}
@* NewProjectForm.razor *@
<Stack Gap="4">
<Input @bind-Value="_name" Placeholder="Project name" />
<Flex Justify="end" Gap="2">
<Button Variant="Button.ButtonVariant.Outline"
OnClick="() => Overlay.Cancel()">Cancel</Button>
<Button OnClick="Submit">Create</Button>
</Flex>
</Stack>
@code {
[CascadingParameter] public OverlayReference Overlay { get; set; } = default!;
private string _name = "";
private void Submit() => Overlay.Close(new Project { Name = _name });
}
Best for: confirmations (ShowAlertDialogAsync),
quick-create forms, picker dialogs, anything reused across pages. Worst for: forms that need to
stay in sync with surrounding page state on every render — the content component is isolated.
Tradeoffs side by side
| Concern | Markup | Service |
|---|---|---|
| Lifecycle owner | Parent component (renders on every parent render) | OverlayProvider (independent) |
| Returning a value | EventCallback or shared field | await returns OverlayResult |
| Access to parent state | Direct (closures over fields) | Via parameters or services only |
| Reuse across pages | Copy the markup | One call site |
| Disposal on navigation | Closes when parent disposes | Survives navigation unless you close it |
| Cascading values | Inherited from parent | Inherited from OverlayProvider only |
| Stacking | Manual | Native — each await stacks above the previous |
Picking the right tool by scenario
- Confirm a destructive action — service. Pure await/result fit; never tied to one trigger.
- Edit the currently selected DataGrid row — markup. The form binds directly to the row record and re-renders with the table.
- Quick-create a tag from inside a Combobox — service. Triggered from arbitrary call sites, returns the new tag id.
- Filter sheet for a catalog page — markup. Filter state lives on the page; the sheet just exposes it.
- App-wide "what's new" dialog after sign-in — service. Opened from a service, not a page.
Mixing the two
It is fine — and common — to use both. A page might own an edit sheet in markup and still
await Overlays.ShowAlertDialogAsync(...)
from inside that sheet's save handler to confirm a destructive sub-action. Both paths render
into the same provider and stack correctly.
See also
- Overlay Service — full API reference
- Sheet — markup component
- Dialog
- Long forms in Sheets