Skip to content

Surfaces

ModalService

Bit.BlazorUI

BitModalService shows a BitModal with any content from anywhere in the app - a service, a view model, a handler - rather than from the markup of the page the modal belongs to. Every modal it shows is rendered by the single BitModalContainer mounted in the layout, and each Show hands back a reference: it carries the content component, closes the modal, completes with the result the modal was answered with, and says whether the user was the one who closed it. The modal around the content is customized with a BitModalParameters, which covers the same ground as the BitModal parameters and adds the two things only a service can offer - a guard that turns down a close, and the policy for what happens when the app navigates away. Modals close on navigation by default, survive it when asked to, and can be shown persistent so they outlive the container that renders them.

Notes

A BitModalContainer must be mounted once, in the layout, for the shown modals to render - a modal shown with no container is not rendered, and the service reports that through the logger. Register the service as Scoped on Blazor Server, where a singleton would be shared between circuits; a singleton is only safe for the single-user hosting models (WebAssembly and Hybrid).

The container takes a ModalParameters of its own, which every modal it renders starts from and each modal can override - the place for the house style: a maximum width, a close button, a position.

Usage

Every example is live. Open its code to see exactly what produced the component running underneath.

Basic

Show takes the component to render as the content of the modal, and optionally a BitModalParameters that customizes the modal around it - the same set of options the BitModal parameters cover. The content reaches its own modal through the cascaded BitModalReference, which is how it closes itself.

An overload takes the component type as a Type instead, for the callers that pick their content from a map or a route rather than by naming it in code.

Header & Footer

The chrome of the modal is part of the parameters too, so a modal shown through the service gets its title, its close button and its footer without the content having to draw any of them: HeaderText, ShowCloseButton and FooterText on the BitModalParameters. What the content is left with is the body.

Awaiting a result

CloseWith takes the value the modal is answered with, and the reference's Result completes with it - which turns a modal into a question the calling code can await. GetResult<T> is the typed way of reading it: a modal that was dismissed rather than answered gives back the default of the type instead of a null to guard against.

A dismissal completes with null either way, so IsDismissed is what tells "walked away from" apart from "answered with nothing" - the reference records whether the user was the one who closed the modal.


Answer: [-]

Reaching the content

The content component is only instantiated when the container renders the modal, which is after the Show call returns - so Content is still null right afterwards. Rendered completes once the modal is on the screen, and GetContentAsync<T> waits for it and hands the instance back, which is how the calling code reaches into its content to focus a field or load its data.

The other direction is the parameters factory overload: it is handed the reference before the content is built, so a parameter of the content can be a callback that closes this very modal - without the window a reference captured only after Show returns leaves open.


The content reported: [-]

Markup as content

Show also takes plain markup, for the content that is not worth a component of its own - a line of text, a note, a fragment the calling code already has in hand. The modal around it is customized with the same BitModalParameters as any other.

The reference's Content stays null for a modal shown this way, since markup is not a component instance and there is none to hand back; reach for one of the Show<T> overloads where the content has to be reached after it is shown.

Guarding the close

CanClose is asked before the user closes a modal - the close button, the overlay, the Escape key - and answering false keeps it open. It is what a modal with something to lose reaches for: a half-filled form, an upload still running. The modal below turns down every dismissal while the field has an unsaved change; Save and Discard are the ways out.

The guard is only asked of the user. Close, CloseWith and CloseAll are the application closing the modal on its own terms and go through regardless, and so does a close on navigation. TryClose is the programmatic call that does ask, and answers whether the modal actually closed.

The modal below is shown Modeless so the TryClose button of the page stays reachable while it is open - the only reason for it here; the guard is asked the same on a modal that holds the page. A refusal leaves the modal exactly where it was, so both sides say what happened: the modal shows the unsaved change it is turning the close down over, and the line below reports what the guard last answered.




Last attempt: [-]

Updating an open modal

Update replaces the parameters of a modal that is already on the screen and re-renders it, which is how a modal changes its title, grows a close button or turns blocking part-way through whatever it is doing - a long save that is not to be interrupted, a wizard whose steps are titled differently.

Mutating the members of the parameters object already handed to the modal works too, since it is the same object the modal reads - but nothing notices such a change on its own, so follow it with Refresh, which is also how the container-level parameters are re-read.

Persistent modals

A modal shown as persistent outlives the container that renders it: it is tracked by the service, and injected into the next container that mounts. That is what keeps it on the screen across a layout change, and what lets it be shown before any container has mounted at all - during start-up, from a handler that runs before the first render.

Everything else is the same: it closes the same ways, and closing it stops it from coming back. Every other modal is over the moment its container is gone, and is closed with a null result rather than left with a Result nobody will ever complete.

Which is what the container below is for: show one of each, unmount the container, and the ordinary modal is closed while the persistent one is only off the screen - mounting a container again brings it back. It renders for a modal service of its own so that unmounting it leaves the rest of the page alone; an app mounts one container, in its layout.






Container: [mounted]
Persistent modal: [never shown]
Ordinary modal: [never shown]

Closing on navigation

A modal belongs to the page it was opened from, so the modals on the screen are closed when the app navigates somewhere else - leaving one lying over a page it says nothing about is the alternative. Only a change of path counts: a query string or a fragment changed on the same page is still the same page, which the buttons below show.

CloseOnNavigation is the opt-out, for the modals that outlive the route change - a sign-in prompt, a running upload - and it can be set for every modal at once through the container's own parameters.

Show both, change the query string and they stay; go to another page and only the second one comes along. Both are shown Modeless so the buttons stay reachable while they are open - a modal that holds the page is closed by a route change just the same.








Modal: [never shown]
Modal that stays: [never shown]

What is open

OpenModals is a snapshot of what the service currently has open, in the order it was opened, and GetModal finds one again by the id its reference carries - for the code that only kept the id. CloseAll closes all of them, each with a null result: what a sign-out or a navigation reaches for, since whatever was left open belongs to the state being left behind.

IsContainerAvailable reports whether a container is mounted right now, which is whether a Show would actually render anything. It is live state rather than registration, so it is the check for the code that runs before the layout has rendered.



Open: [0]   Container mounted: [True]

Watching every modal

OnAddModal and OnCloseModal are raised for every modal the service shows and closes, whoever showed it. They are where cross-cutting behavior goes: logging what was opened, pausing a poll while a modal is up, telling analytics what the user was asked.

Both are awaited, so a handler can do real work - and a handler that throws does not leave a half-shown modal behind: the show is rolled back and the failure is reported to the caller.


Shown: [0]   Closed: [0]

API

Every parameter, public member, sub-class and enum this component exposes.

BitModalService parameters

Name Type Default value Description
OnAddModal event Func<BitModalReference, Task>? The event for when a new modal gets added through calling the Show method.
OnCloseModal event Func<BitModalReference, Task>? The event for when a modal gets removed through calling the Close method.
IsContainerAvailable bool Whether a BitModalContainer is currently mounted for this service, i.e. whether a Show call right now would actually render its modal. It reflects live state rather than registration in DI.
OpenModals IReadOnlyList<BitModalReference> A snapshot of the modals this service currently has open, in the order they were opened. It holds what the mounted container renders, plus the persistent modals that are still waiting for a container to mount.
GetModal BitModalReference? (string? id) The open modal with the given id, or null when there is none - it was closed, or the id belongs to another service.
Close Task (BitModalReference modal) Closes an already opened modal using its reference, with a null result. This is the application closing the modal, so the CanClose guard is not asked.
Close Task (BitModalReference modal, object? result) Closes an already opened modal using its reference, with the result its Result task completes with. The CanClose guard is not asked.
TryClose Task<bool> (BitModalReference modal, object? result) Asks a modal to close and reports whether it did: a modal whose CanClose guard turns the close down stays open and this answers false.
CloseAll Task Closes every modal this service currently has open, each with a null result. The CanClose guards are not asked.
Refresh Task (BitModalReference? modal) Re-renders the open modals, invalidating their memoized merged parameters. Call it after mutating modal parameters in place, which doesn't change any object reference and is therefore not detected on its own. Without an argument it refreshes every open modal.
Show Task<BitModalReference> (Dictionary<string, object>? parameters) Shows a new BitModal with a custom component with parameters as its content.
Show Task<BitModalReference> (BitModalParameters? modalParameters) Shows a new BitModal with a custom component as its content with custom parameters for the modal.
Show Task<BitModalReference> (Dictionary<string, object>? parameters, BitModalParameters? modalParameters, bool persistent) Shows a new BitModal with a custom component as its content with custom parameters for the custom component and the modal. A persistent modal survives a container remount and is injected into the next container that mounts.
Show Task<BitModalReference> (Type componentType, Dictionary<string, object>? parameters, BitModalParameters? modalParameters, bool persistent) Shows a new BitModal with a component whose type is only known at run time as its content, for the callers that pick their content from a map or a route. Throws an ArgumentException for a type that is not a Blazor component.
Show Task<BitModalReference> (RenderFragment content, BitModalParameters? modalParameters, bool persistent) Shows a new BitModal with the given markup as its content, for the content that is not worth a component of its own. The reference's Content stays null for such a modal, since markup is not a component instance.
Show Task<BitModalReference> (Func<BitModalReference, Dictionary<string, object>?> parametersFactory, BitModalParameters? modalParameters, bool persistent) Shows a new BitModal, building the content component's parameters from a factory that receives the modal reference. Use this overload when a parameter needs the reference itself, such as an OnClose callback that closes this very modal.

Feedback

Found a mistake, a gap, or something that could be clearer? Every page and every component is one click from its source.