Skip to content
# ModalService ## Description 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. ## 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. | ## Sub Classes ### BitModalReference Properties The handle a Show call hands back: what the modal is, what it answered with, and the ways to close it. | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | Id | `string` | | The unique id of the shown modal. | | Content | `object?` | null | The instance of the component rendered as the content of the modal. It is captured while the modal is rendered, which is after the Show call returns, so it is still null immediately afterwards. | | IsClosed | `bool` | false | Whether this modal has already been closed. A reference is never reused, so once set it stays set. | | IsDismissed | `bool` | false | Whether the modal was closed by the user - the close button, the overlay, the Escape key - rather than by the application. It is what tells a modal that was walked away from apart from one answered with nothing, which the Result alone cannot. | | Persistent | `bool` | false | Whether the modal survives a container remount and is injected into the next container that mounts. | | Parameters | `BitModalParameters?` | null | The parameters the modal is shown with, before they are merged with the container's own. | | Result | `Task<object?>` | | Completes when the modal is closed, with the value it was closed with - null for a modal that was dismissed rather than answered. | | Rendered | `Task<bool>` | | Completes with true once a container has rendered the modal, and with false for a modal that was closed before it ever rendered - one shown while no container was mounted, or closed in the same breath it was shown. | | Close | `Task` | | Closes the modal without a result. The CanClose guard is not asked. | | CloseWith | `Task (object? result)` | | Closes the modal with the given result, which is what its Result task completes with. The CanClose guard is not asked. | | TryClose | `Task<bool> (object? result)` | | Asks the modal to close and reports whether it did: a modal whose CanClose guard turns the close down stays open and this answers false. | | Dismiss | `Task<bool>` | | Closes the modal as a dismissal - the way the close button, the overlay and the Escape key close it - which asks the CanClose guard and marks the reference as dismissed. The content's own cancel action. | | Update | `Task (BitModalParameters? parameters)` | | Replaces the parameters the modal is shown with and re-renders it. The whole set is replaced rather than merged. | | GetResult<T> | `Task<T?>` | | The result the modal was closed with, cast to T - the type's default for a modal that was dismissed or answered with something else. | | GetContentAsync<T> | `Task<T?>` | | The component rendered as the content, cast to T, waiting for the modal to be rendered first. The type's default for a modal that never rendered or whose content is markup. | ### BitModalParameters Properties The set of options a modal is shown with. Every parameter of the BitModal component has a nullable counterpart here (null meaning "not set", so the modal's own default or the container's value is used), plus the two options only a service can offer: | Name | Type | Default Value | Description | | :--- | :--- | :------------ | :---------- | | CanClose | `Func<Task<bool>>?` | null | Asked before the user closes the modal - the close button, the overlay, the Escape key - and before an explicit TryClose. Answering false keeps the modal open. Close, CloseWith, CloseAll and a close on navigation are the application closing the modal and do not ask it. Only the guard on the modal's own parameters is asked, not one on the container's. | | CloseOnNavigation | `bool?` | null | Whether the modal closes when the app navigates somewhere else, which it does by default. Only a change of path counts; a query string or a fragment changed on the same page does not. Set it to false for the modals that outlive a route change. | ## Examples \n**Basic**: ```razor Show @* in the layout *@ ``` ```csharp [AutoInject] private BitModalService modalService = default!; private async Task ShowModal() { await modalService.Show(new BitModalParameters() { FullWidth = true }); } // the same modal, with the content type only known at run time private async Task ShowModalByType(Type contentType) { await modalService.Show(contentType, modalParameters: new BitModalParameters() { FullWidth = true }); } ``` \n**Header & Footer**: ```razor Show @* ModalBodyContent.razor *@ The header, the close button and the footer all come from the parameters the modal was shown with. ``` ```csharp [AutoInject] private BitModalService modalService = default!; private async Task ShowChromeModal() { await modalService.Show(new BitModalParameters { MaxWidth = "32rem", HeaderText = "Shown by the service", ShowCloseButton = true, FooterText = "The footer of the modal." }); } ``` \n**Awaiting a result**: ```razor Delete the project
Answer: [@confirmAnswer]
@* ConfirmModalContent.razor *@ @Question Yes No ``` ```csharp // ConfirmModalContent.razor [CascadingParameter] private BitModalReference modalReference { get; set; } = default!; [Parameter] public string? Question { get; set; } // the page private string confirmAnswer = "-"; private async Task ShowConfirmModal() { var modal = await modalService.Show(new Dictionary { { nameof(ConfirmModalContent.Question), "Delete the project?" } }); var confirmed = await modal.GetResult(); confirmAnswer = modal.IsDismissed ? "dismissed" : $"{confirmed}"; StateHasChanged(); } ``` \n**Reaching the content**: ```razor Show and count
The content reported: [@contentReport]
``` ```csharp private string contentReport = "-"; private async Task ShowContentReachingModal() { var modal = await modalService.Show(new Dictionary { { nameof(ConfirmModalContent.Question), "How long is this question?" } }); // The content is only instantiated once the container renders the modal, so it is waited for rather // than read straight off the reference the Show call handed back. var content = await modal.GetContentAsync(); contentReport = $"{content?.Question?.Length ?? 0} characters"; StateHasChanged(); } // The other direction: the reference is handed to the factory before the content is built, so a // parameter of the content can be a callback that closes this very modal. private async Task ShowSelfClosingModal() { await modalService.Show(modalRef => new Dictionary { { nameof(UnsavedModalContent.HasChangesChanged), EventCallback.Factory.Create(this, _ => modalRef.Close()) } }); } ``` \n**Markup as content**: ```razor Show markup ``` ```csharp [AutoInject] private BitModalService modalService = default!; private async Task ShowMarkupModal() { await modalService.Show(builder => { builder.OpenElement(0, "div"); builder.AddAttribute(1, "style", "padding:1.5rem;max-width:26rem"); builder.AddContent(2, "This modal was shown with markup rather than with a component of its own."); builder.CloseElement(); }); } ``` \n**Guarding the close**: ```razor Rename the project TryClose it from here
Last attempt: [@guardReport]
@* UnsavedModalContent.razor *@ @if (hasChanges) { Unsaved change: the close button, Escape and TryClose are turned down until you save or discard. } Save Discard ``` ```csharp private bool hasUnsavedChanges; private string guardReport = "-"; private BitModalReference? guardedModal; private async Task ShowGuardedModal() { hasUnsavedChanges = false; guardReport = "-"; guardedModal = await modalService.Show( new Dictionary { { nameof(UnsavedModalContent.HasChangesChanged), EventCallback.Factory.Create(this, v => hasUnsavedChanges = v) } }, new BitModalParameters { // Modeless only so the TryClose button of the page stays reachable while the modal is open. Modeless = true, ShowCloseButton = true, HeaderText = "Rename the project", CanClose = GuardTheClose }); } // The guard reports what it answered, so a dismissal it turns down is visible as something having happened. private Task GuardTheClose() { var canClose = hasUnsavedChanges is false; guardReport = canClose ? "let through" : "turned down (unsaved change)"; StateHasChanged(); return Task.FromResult(canClose); } private async Task TryCloseGuardedModal() { if (guardedModal is null || guardedModal.IsClosed) { guardReport = "nothing open"; return; } await guardedModal.TryClose(); } // UnsavedModalContent.razor: the two ways out drop the changes first, so the guard lets the close through. private async Task Discard() { hasChanges = false; await HasChangesChanged.InvokeAsync(false); await modalReference.Dismiss(); } ``` \n**Updating an open modal**: ```razor Show, then update it ``` ```csharp private async Task ShowUpdatingModal() { var modal = await modalService.Show(new BitModalParameters { MaxWidth = "28rem", HeaderText = "Saving...", Blocking = true }); // Standing in for the work: the modal blocks while it runs, and grows its way out once it is done. await Task.Delay(2000); await modal.Update(new BitModalParameters { MaxWidth = "28rem", HeaderText = "Saved", ShowCloseButton = true, FooterText = "The parameters were replaced while the modal was on the screen." }); } // mutating the parameters already handed to the modal works too, followed by a Refresh private async Task RenameTheOpenModal(BitModalReference modal) { modal.Parameters!.HeaderText = "A new title"; await modalService.Refresh(modal); } ``` \n**Persistent modals**: ```razor Show a persistent modal Show an ordinary one @(isDemoContainerMounted ? "Unmount the container" : "Mount the container again")
Persistent modal: [@DescribeModal(persistentModal)]
Ordinary modal: [@DescribeModal(ordinaryModal)]
@* An app mounts one container, in its layout: this one is the example's, so that unmounting it leaves the modals of the rest of the page alone. *@ @if (isDemoContainerMounted) { } @* DemoModalContainer.razor: a BitModalContainer that is handed the service it renders for. *@ @inherits BitModalContainerBase @foreach (var modalReference in _modalRefs) { @modalReference.Modal } @code { [Parameter, EditorRequired] public BitModalService Service { get; set; } = default!; protected override BitModalServiceBase ModalService => Service; protected override BitModalParameters? MergeParameters(BitModalParameters? modalParameters, BitModalParameters? containerParameters) { return BitModalParameters.Merge(modalParameters, containerParameters); } protected override bool? GetCloseOnNavigation(BitModalReference modalReference) { return GetMergedParameters(modalReference)?.CloseOnNavigation; } } ``` ```csharp private readonly BitModalService demoModalService = new(); private bool isDemoContainerMounted = true; private BitModalReference? persistentModal; private BitModalReference? ordinaryModal; private async Task ShowPersistentModal() { persistentModal = await demoModalService.Show( new BitModalParameters { MaxWidth = "28rem", HeaderText = "Persistent" }, persistent: true); } private async Task ShowOrdinaryModal() { ordinaryModal = await demoModalService.Show( new BitModalParameters { MaxWidth = "28rem", HeaderText = "Ordinary" }); } // Unmounting the container is what tells the two apart: the ordinary modal is closed by the container that // was rendering it, and the persistent one is only taken off the screen until a container mounts again. private void ToggleDemoContainer() { isDemoContainerMounted = isDemoContainerMounted is false; } private string DescribeModal(BitModalReference? modalRef) { if (modalRef is null) return "never shown"; if (modalRef.IsClosed) return "closed"; return isDemoContainerMounted ? "open" : "open, waiting for a container"; } ``` \n**Closing on navigation**: ```razor Show a modal Show one that stays Change the query string (both stay) Go to another page
Modal: [@DescribeNavigationModal(navigationModal)]
Modal that stays: [@DescribeNavigationModal(lingeringModal)]
@* every modal of this container outlives the route change unless it says otherwise *@ ``` ```csharp [AutoInject] private NavigationManager navigationManager = default!; private BitModalReference? navigationModal; private BitModalReference? lingeringModal; private async Task ShowNavigationModal() { navigationModal = await modalService.Show(new BitModalParameters { MaxWidth = "28rem", Modeless = true, ShowCloseButton = true, HeaderText = "Closes on navigation" }); } // the modals that outlive a route change say so themselves private async Task ShowLingeringModal() { lingeringModal = await modalService.Show(new BitModalParameters { MaxWidth = "28rem", Modeless = true, ShowCloseButton = true, HeaderText = "Stays across a route change", CloseOnNavigation = false }); } private void NavigateWithQuery() { // The same page, so the modals on it are the modals of the page still being looked at. navigationManager.NavigateTo($"/components/modalservice?at={DateTime.Now.Ticks}"); } private void NavigateToAnotherPage() { // A different path, which is what closes the modals of the page being left behind. navigationManager.NavigateTo("/components/modal"); } ``` \n**What is open**: ```razor Show one more
Open: [@modalService.OpenModals.Count]   Container mounted: [@modalService.IsContainerAvailable]
Close all ``` ```csharp [AutoInject] private BitModalService modalService = default!; // shown Modeless and in a corner so the page stays reachable and every modal of the stack stays visible private async Task ShowStackedModal() { var count = modalService.OpenModals.Count; await modalService.Show(new BitModalParameters { MaxWidth = "20rem", Modeless = true, ShowCloseButton = true, HeaderText = $"Modal {count + 1}", Position = StackedModalPosition(count) }); } private static BitPosition StackedModalPosition(int index) => (index % 5) switch { 0 => BitPosition.TopStart, 1 => BitPosition.TopEnd, 2 => BitPosition.BottomStart, 3 => BitPosition.BottomEnd, _ => BitPosition.Center }; private async Task CloseAllModals() { await modalService.CloseAll(); } // the code that only kept the id finds the modal again private async Task CloseById(string id) { var modal = modalService.GetModal(id); if (modal is not null) { await modal.Close(); } } ``` \n**Watching every modal**: ```razor Show
Shown: [@shownCount]   Closed: [@closedCount]
``` ```csharp private int shownCount; private int closedCount; // shown Modeless and in a corner so the Show button stays clickable while the counters are watched private async Task ShowWatchedModal() { var count = modalService.OpenModals.Count; await modalService.Show(new BitModalParameters { MaxWidth = "20rem", Modeless = true, ShowCloseButton = true, HeaderText = $"Modal {count + 1}", Position = StackedModalPosition(count) }); } protected override void OnInitialized() { modalService.OnAddModal += HandleOnAddModal; modalService.OnCloseModal += HandleOnCloseModal; base.OnInitialized(); } private Task HandleOnAddModal(BitModalReference modalRef) { shownCount++; return InvokeAsync(StateHasChanged); } private Task HandleOnCloseModal(BitModalReference modalRef) { closedCount++; return InvokeAsync(StateHasChanged); } public void Dispose() { modalService.OnAddModal -= HandleOnAddModal; modalService.OnCloseModal -= HandleOnCloseModal; } ```