Lumeo

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