From 5d3c06d5d960e748dce54f47dadefc768552e9d5 Mon Sep 17 00:00:00 2001 From: Vikram Reddy Date: Fri, 3 Jul 2026 12:04:33 +0530 Subject: [PATCH] Fix ToastService documentation and enhance toast handling with async support --- .../ToastServiceDocumentation.razor | 40 +++++++++++++++++++ ...otifyAsync_With_Multiple_Subscribers.razor | 39 ++++++++++++++++++ .../Demos/Toasts/ToastsDocumentation.razor | 7 +++- .../ToastService_Doc_01_Documentation.razor | 13 ++++-- .../Components/Toasts/Toasts.razor.cs | 20 ++++++---- blazorbootstrap/Services/ToastService.cs | 36 +++++++++++++++-- docs/docs/05-components/toasts.mdx | 8 +++- 7 files changed, 145 insertions(+), 18 deletions(-) create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastServiceDocumentation.razor create mode 100644 BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastService_Demo_01_NotifyAsync_With_Multiple_Subscribers.razor diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastServiceDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastServiceDocumentation.razor new file mode 100644 index 000000000..4544fed0d --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastServiceDocumentation.razor @@ -0,0 +1,40 @@ +@page "/toast-service" +@using BlazorBootstrap.Demo.RCL.Components.Pages.Demos.Toasts + +@attribute [Route(pageUrl)] +@layout DemosMainLayout + + + + + +
Use Blazor ToastService to publish application-level toast notifications from anywhere in your app.
+ +
+
1. Add the Toasts component in MainLayout.razor page as shown below.
+ + + + Set the Toasts component parameters as per your requirement. + + +
2. Inject ToastService, then call Notify(...) for the common single Toasts host scenario.
+ + +
3. Use await NotifyAsync(...) when multiple toast subscribers may be registered and the caller needs to await all handlers.
+ +
+ +@code { + private const string pageUrl = DemoRouteConstants.Demos_URL_ToastService; + private const string pageTitle = "Blazor Toast Service"; + private const string pageDescription = "Use Blazor Bootstrap toast service to publish application-level toast notifications from anywhere in your app."; + private const string metaTitle = "Blazor Toast Service"; + private const string metaDescription = "Use Blazor Bootstrap toast service to publish application-level toast notifications from anywhere in your app."; + private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Toasts; +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastService_Demo_01_NotifyAsync_With_Multiple_Subscribers.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastService_Demo_01_NotifyAsync_With_Multiple_Subscribers.razor new file mode 100644 index 000000000..1837f3b3d --- /dev/null +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Services/ToastService/ToastService_Demo_01_NotifyAsync_With_Multiple_Subscribers.razor @@ -0,0 +1,39 @@ +
+
This example registers two Toasts hosts on the same page. Clicking the button publishes one message to both hosts and awaits both subscribers.
+ +
+ +
+ +
@statusMessage
+ + @* These local Toasts hosts intentionally add extra ToastService subscribers on this page + so NotifyAsync can demonstrate awaiting multiple handlers for a single published message. *@ + + +
+ +@code { + [Inject] + protected ToastService ToastService { get; set; } = default!; + + private bool isNotifying; + + private string statusMessage = "Click the button to publish the same toast to two hosts."; + + private async Task NotifyBothHostsAsync() + { + isNotifying = true; + statusMessage = "Publishing to both toast hosts..."; + + await ToastService.NotifyAsync(new ToastMessage( + ToastType.Info, + "Toast Service", + $"NotifyAsync delivered this message to both toast hosts at {DateTime.Now:T}.")); + + statusMessage = $"NotifyAsync completed after both toast hosts handled the message at {DateTime.Now:T}."; + isNotifying = false; + } +} \ No newline at end of file diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Toasts/ToastsDocumentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Toasts/ToastsDocumentation.razor index cc59d2b30..1f7928a6d 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Toasts/ToastsDocumentation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Demos/Toasts/ToastsDocumentation.razor @@ -48,8 +48,8 @@
-
Blazor Toasts component shows a maximum of 5 toasts by default. If you add a new toast to the existing list, the first toast gets deleted like FIFO (First In First Out). Change the maximum capacity according to your need by using the StackLength parameter.
-
In the below example, StackLength is set to 3. It shows a maximum of 3 toast messages at any time.
+
Blazor Toasts component shows a maximum of 5 toasts by default. If you add a new toast to the existing list, the oldest rendered toast is dismissed first in FIFO (First In First Out) order. Change the maximum capacity according to your need by using the StackLength parameter.
+
In the below example, StackLength is set to 3. It shows a maximum of 3 toast messages at any time, and older visible toasts finish their hide transition before they are removed from the stack.
@@ -61,6 +61,9 @@
2. Inject ToastService, then call the Notify(...) method as shown below.
+ + Use Notify(...) for the common single Toasts host scenario. Use await NotifyAsync(...) when multiple toast subscribers are registered and the caller needs to await all handlers. + @code { diff --git a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Services/ToastService/ToastService_Doc_01_Documentation.razor b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Services/ToastService/ToastService_Doc_01_Documentation.razor index 746f72d18..6d486bc51 100644 --- a/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Services/ToastService/ToastService_Doc_01_Documentation.razor +++ b/BlazorBootstrap.Demo.RCL/Components/Pages/Docs/Services/ToastService/ToastService_Doc_01_Documentation.razor @@ -8,7 +8,7 @@ MetaDescription="@metaDescription" ImageUrl="@imageUrl" /> - +
@metaTitle @@ -18,6 +18,11 @@
+
+
Use Notify(...) for the common single Toasts host scenario.
+
Use await NotifyAsync(...) when multiple toast subscribers may be registered and the caller needs to await all handlers.
+
+
@@ -30,8 +35,8 @@ private const string componentName = nameof(ToastService); private const string pageUrl = DemoRouteConstants.Docs_URL_ToastService; private const string pageTitle = componentName; - private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; - private const string metaTitle = $"Blazor {componentName} Component"; - private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} component, guiding you through its configuration options."; + private const string pageDescription = $"This documentation provides a comprehensive reference for the {componentName} service, guiding you through its usage."; + private const string metaTitle = $"Blazor {componentName} Service"; + private const string metaDescription = $"This documentation provides a comprehensive reference for the {componentName} service, guiding you through its usage."; private const string imageUrl = DemoScreenshotSrcConstants.Demos_URL_Toasts; } \ No newline at end of file diff --git a/blazorbootstrap/Components/Toasts/Toasts.razor.cs b/blazorbootstrap/Components/Toasts/Toasts.razor.cs index 024358339..ca1c72d3f 100644 --- a/blazorbootstrap/Components/Toasts/Toasts.razor.cs +++ b/blazorbootstrap/Components/Toasts/Toasts.razor.cs @@ -12,7 +12,7 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) Messages = null; if (ToastService is not null) - ToastService.OnNotify -= OnNotify; + ToastService.OnNotify -= OnNotifyAsync; } await base.DisposeAsyncCore(disposing); @@ -21,12 +21,12 @@ protected override async ValueTask DisposeAsyncCore(bool disposing) protected override void OnInitialized() { if (ToastService is not null) - ToastService.OnNotify += OnNotify; + ToastService.OnNotify += OnNotifyAsync; base.OnInitialized(); } - private void OnNotify(ToastMessage toastMessage) + private async Task OnNotifyAsync(ToastMessage toastMessage) { if (toastMessage is null) return; @@ -35,7 +35,7 @@ private void OnNotify(ToastMessage toastMessage) Messages.Add(toastMessage); - StateHasChanged(); + await InvokeAsync(StateHasChanged); } private void OnToastHiddenAsync(ToastEventArgs args) @@ -70,8 +70,11 @@ private async Task OnToastShownAsync(ToastEventArgs args) { if (message is not null) { - Messages.Remove(message); - if (!string.IsNullOrWhiteSpace(message.ElementId)) + if (string.IsNullOrWhiteSpace(message.ElementId)) + Messages.Remove(message); + else + // Keep rendered toasts in the DOM until Bootstrap completes the hide transition. + // Removing them first can dispose the element before toast.js raises hidden. await SafeInvokeVoidAsync("window.blazorBootstrap.toasts.hide", message.ElementId); } } @@ -153,10 +156,13 @@ private async Task OnToastShownAsync(ToastEventArgs args) /// /// Default value is 5. /// + /// + /// When the limit is exceeded, the oldest rendered toasts are dismissed first. + /// /// [AddedVersion("1.0.0")] [DefaultValue(5)] - [Description("Gets or sets the toast container maximum capacity.")] + [Description("Gets or sets the toast container maximum capacity. When the limit is exceeded, the oldest rendered toasts are dismissed first.")] [Parameter] public int StackLength { get; set; } = 5; diff --git a/blazorbootstrap/Services/ToastService.cs b/blazorbootstrap/Services/ToastService.cs index 78b34e4d7..f457fc3af 100644 --- a/blazorbootstrap/Services/ToastService.cs +++ b/blazorbootstrap/Services/ToastService.cs @@ -1,10 +1,16 @@ namespace BlazorBootstrap; +/// +/// Provides a simple publish/subscribe mechanism for displaying toast messages from anywhere in the application. +/// +/// +/// Register a host in the render tree to display messages published by this service. +/// public class ToastService { #region Events - internal event Action OnNotify = default!; + internal event Func OnNotify = default!; #endregion @@ -13,12 +19,36 @@ public class ToastService /// /// Notifies subscribers of a new toast message event. /// - /// If no subscribers are registered, this method has no effect. This method is typically used to - /// display transient notifications to users. + /// If no subscribers are registered, this method has no effect. Use this overload for the common + /// fire-and-forget scenario where a single host is registered. If you need to await + /// multiple subscribers, use instead. /// The toast message to be delivered to all registered listeners. Cannot be null. [AddedVersion("1.0.0")] [Description("Notifies subscribers of a new toast message event.")] public void Notify(ToastMessage toastMessage) => OnNotify?.Invoke(toastMessage); + /// + /// Notifies all subscribers of a new toast message event and awaits their completion. + /// + /// If no subscribers are registered, this method has no effect. Use this overload when multiple + /// subscribers may be attached and the caller needs to observe completion or failures from each handler. + /// The toast message to be delivered to all registered listeners. Cannot be null. + /// A task that represents the asynchronous notification operation. + [AddedVersion("4.0.0")] + [Description("Notifies all subscribers of a new toast message event and awaits their completion.")] + public async Task NotifyAsync(ToastMessage toastMessage) + { + // Multicast delegate Invoke returns only the last Task, so enumerate subscribers when every handler must complete. + Delegate[] subscribers = OnNotify?.GetInvocationList() ?? Array.Empty(); + if (subscribers.Length > 0) + { + await Task.WhenAll(subscribers.Select(d => + { + Func subscriber = (Func)d; + return subscriber.Invoke(toastMessage); + })); + } + } + #endregion } diff --git a/docs/docs/05-components/toasts.mdx b/docs/docs/05-components/toasts.mdx index af0cd81c0..606c23fcd 100644 --- a/docs/docs/05-components/toasts.mdx +++ b/docs/docs/05-components/toasts.mdx @@ -258,9 +258,9 @@ Change the Blazor Toasts placement according to your need. The default placement ### Stack Length -Blazor Toasts component shows a maximum of 5 toasts by default. If you add a new toast to the existing list, the first toast gets deleted like FIFO (First In First Out). Change the maximum capacity according to your need by using the **StackLength** parameter. +Blazor Toasts component shows a maximum of 5 toasts by default. If you add a new toast to the existing list, the oldest rendered toast is dismissed first in FIFO (First In First Out) order. Change the maximum capacity according to your need by using the **StackLength** parameter. -In the below example, StackLength is set to 3. It shows a maximum of 3 toast messages at any time. +In the below example, StackLength is set to 3. It shows a maximum of 3 toast messages at any time, and older visible toasts finish their hide transition before they are removed from the stack. ```cshtml {1} showLineNumbers @@ -314,6 +314,10 @@ Set the `Toasts` component parameters as per your requirement. 2. Inject `ToastService`, then call the `Notify(...)` method as shown below. +:::tip +Use `Notify(...)` for the common single `Toasts` host scenario. If your app registers multiple toast subscribers and you need to await all handlers, call `await ToastService.NotifyAsync(...)` instead. +::: + ```cshtml {} showLineNumbers @code {