# Service vs Markup Overlays

Source: https://lumeo.nativ.sh/docs/service-vs-markup-overlays

# 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](docs/services/overlay) — full API reference
-   [Sheet](components/sheet) — markup component
-   [Dialog](components/dialog)
-   [Long forms in Sheets](docs/long-forms-in-sheets)
