replace Bazored.Toast with custom Toast component

This commit is contained in:
Eduardo
2026-08-04 23:00:07 -03:00
committed by Acentech Dev
parent 23bc36fd65
commit 72874567c6
27 changed files with 525 additions and 101 deletions

View File

@@ -1,12 +1,6 @@
@inherits LayoutComponentBase
<div class="page">
<!--
<div class="sidebar">
<NavMenu />
</div>
<BlazoredToasts />
-->
<main>
<div class="top-row px-4">
<BreadcrumbNavigation />

View File

@@ -9,9 +9,6 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Blazored.Toast
@using Blazored.Toast.Services
@using BlazorInputFile
@using MUnique.OpenMU.Web.Shared

View File

@@ -6,7 +6,6 @@
<PackageVersion Include="BCrypt.Net-Next" Version="4.0.3" />
<PackageVersion Include="BenchmarkDotNet" Version="0.15.8" />
<PackageVersion Include="BlazorInputFile" Version="0.2.0" />
<PackageVersion Include="Blazored.Toast" Version="4.2.1" />
<PackageVersion Include="BuildWebCompiler2022" Version="1.14.15" />
<PackageVersion Include="DG.AdvancedDataGridView" Version="1.2.30115.18" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.16.1" />

View File

@@ -92,69 +92,6 @@ internal class EntityFrameworkContextBase : IContext
}
}
/// <summary>
/// Determines whether the exception is a transient conflict caused by a concurrent entity mutation
/// racing this save, and is therefore worth retrying.
/// </summary>
/// <param name="exception">The exception thrown by the save.</param>
/// <returns><c>true</c> if the save should be retried.</returns>
private static bool IsTransientConcurrencyConflict(Exception exception)
{
// A concurrent entity mutation racing this save corrupts the change tracker mid-enumeration.
// Depending on exactly where change detection was, it surfaces as one of several types - a
// modified collection (InvalidOperationException), a transiently-null internal key
// (ArgumentNullException/NullReferenceException), or an out-of-range index. All are transient:
// the racing mutation is a single quick operation, so a bounded retry lands on a stable moment.
// A genuinely persistent error of the same type is not masked - it rethrows once the retries
// are exhausted. The deterministic serialization (per-player persistence lock) is the primary
// guard; this retry only needs to absorb the rare, bursty sources that lock isn't held for.
return exception is DbUpdateConcurrencyException
or InvalidOperationException
or ArgumentNullException
or NullReferenceException
or IndexOutOfRangeException
or KeyNotFoundException;
}
private async ValueTask<bool> SaveChangesCoreAsync(CancellationToken cancellationToken)
{
using var l = await this._lock.LockAsync();
// when we have a change publisher attached, we want to get the changed entries before accepting them.
// Otherwise, we can accept them.
var acceptChanges = true;
object? sender = null;
SavedChangesEventArgs? args = null;
if (this._changeListener is { })
{
this.Context.SavedChanges += OnSavedChanges;
acceptChanges = false;
}
try
{
await this.Context.SaveChangesAsync(acceptChanges, cancellationToken).ConfigureAwait(false);
if (args is not null)
{
await this.OnSavedChangesAsync(sender, args).ConfigureAwait(false);
}
}
finally
{
this.Context.SavedChanges -= OnSavedChanges;
}
return true;
void OnSavedChanges(object? s, SavedChangesEventArgs e)
{
sender = s;
args = e;
}
}
/// <inheritdoc />
public IDisposable SuspendChangeNotifications()
{
@@ -323,6 +260,69 @@ internal class EntityFrameworkContextBase : IContext
this.Context.Dispose();
}
/// <summary>
/// Determines whether the exception is a transient conflict caused by a concurrent entity mutation
/// racing this save, and is therefore worth retrying.
/// </summary>
/// <param name="exception">The exception thrown by the save.</param>
/// <returns><c>true</c> if the save should be retried.</returns>
private static bool IsTransientConcurrencyConflict(Exception exception)
{
// A concurrent entity mutation racing this save corrupts the change tracker mid-enumeration.
// Depending on exactly where change detection was, it surfaces as one of several types - a
// modified collection (InvalidOperationException), a transiently-null internal key
// (ArgumentNullException/NullReferenceException), or an out-of-range index. All are transient:
// the racing mutation is a single quick operation, so a bounded retry lands on a stable moment.
// A genuinely persistent error of the same type is not masked - it rethrows once the retries
// are exhausted. The deterministic serialization (per-player persistence lock) is the primary
// guard; this retry only needs to absorb the rare, bursty sources that lock isn't held for.
return exception is DbUpdateConcurrencyException
or InvalidOperationException
or ArgumentNullException
or NullReferenceException
or IndexOutOfRangeException
or KeyNotFoundException;
}
private async ValueTask<bool> SaveChangesCoreAsync(CancellationToken cancellationToken)
{
using var l = await this._lock.LockAsync();
// when we have a change publisher attached, we want to get the changed entries before accepting them.
// Otherwise, we can accept them.
var acceptChanges = true;
object? sender = null;
SavedChangesEventArgs? args = null;
if (this._changeListener is { })
{
this.Context.SavedChanges += OnSavedChanges;
acceptChanges = false;
}
try
{
await this.Context.SaveChangesAsync(acceptChanges, cancellationToken).ConfigureAwait(false);
if (args is not null)
{
await this.OnSavedChangesAsync(sender, args).ConfigureAwait(false);
}
}
finally
{
this.Context.SavedChanges -= OnSavedChanges;
}
return true;
void OnSavedChanges(object? s, SavedChangesEventArgs e)
{
sender = s;
args = e;
}
}
private bool DetachInternal(object item)
{
var entry = this.Context.Entry(item);

View File

@@ -34,7 +34,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
TaxStore = table.Column<byte>(type: "smallint", nullable: false),
TaxHunt = table.Column<int>(type: "integer", nullable: false),
IsHuntZoneEnabled = table.Column<bool>(type: "boolean", nullable: false),
TributeMoney = table.Column<long>(type: "bigint", nullable: false)
TributeMoney = table.Column<long>(type: "bigint", nullable: false),
},
constraints: table =>
{
@@ -53,7 +53,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
DefenseLevel = table.Column<byte>(type: "smallint", nullable: false),
RegenLevel = table.Column<byte>(type: "smallint", nullable: false),
LifeLevel = table.Column<byte>(type: "smallint", nullable: false),
CurrentHp = table.Column<int>(type: "integer", nullable: false)
CurrentHp = table.Column<int>(type: "integer", nullable: false),
},
constraints: table =>
{
@@ -87,7 +87,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
GuildScoreCastleSiege = table.Column<int>(type: "integer", nullable: false),
GuildScoreCastleSiegeMembers = table.Column<int>(type: "integer", nullable: false),
GateBuyPrice = table.Column<int>(type: "integer", nullable: false),
StatueBuyPrice = table.Column<int>(type: "integer", nullable: false)
StatueBuyPrice = table.Column<int>(type: "integer", nullable: false),
},
constraints: table =>
{
@@ -125,7 +125,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
DefaultSide = table.Column<byte>(type: "smallint", nullable: false),
SpawnX = table.Column<byte>(type: "smallint", nullable: false),
SpawnY = table.Column<byte>(type: "smallint", nullable: false),
Direction = table.Column<int>(type: "integer", nullable: false)
Direction = table.Column<int>(type: "integer", nullable: false),
},
constraints: table =>
{
@@ -155,7 +155,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
State = table.Column<byte>(type: "smallint", nullable: false),
DayOfWeek = table.Column<int>(type: "integer", nullable: false),
Hour = table.Column<byte>(type: "smallint", nullable: false),
Minute = table.Column<byte>(type: "smallint", nullable: false)
Minute = table.Column<byte>(type: "smallint", nullable: false),
},
constraints: table =>
{
@@ -183,7 +183,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
Level = table.Column<byte>(type: "smallint", nullable: false),
RequiredJewelOfGuardianCount = table.Column<int>(type: "integer", nullable: false),
RequiredZen = table.Column<int>(type: "integer", nullable: false),
Value = table.Column<int>(type: "integer", nullable: false)
Value = table.Column<int>(type: "integer", nullable: false),
},
constraints: table =>
{
@@ -236,7 +236,7 @@ namespace MUnique.OpenMU.Persistence.EntityFramework.Migrations
X1 = table.Column<byte>(type: "smallint", nullable: false),
Y1 = table.Column<byte>(type: "smallint", nullable: false),
X2 = table.Column<byte>(type: "smallint", nullable: false),
Y2 = table.Column<byte>(type: "smallint", nullable: false)
Y2 = table.Column<byte>(type: "smallint", nullable: false),
},
constraints: table =>
{

View File

@@ -8,7 +8,7 @@
@implements IDisposable
@inject CreationPanelService Panel
@inject Blazored.Toast.Services.IToastService ToastService
@inject IToastService ToastService
@if (this.Panel.Current is { } session)
{

View File

@@ -39,7 +39,7 @@
</div>
</div>
<BlazoredToasts />
<ToastContainer />
<article class="content px-4 py-3">
@Body

View File

@@ -25,7 +25,6 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" />
<PackageReference Include="Blazored.Toast" />
<PackageReference Include="BlazorInputFile" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" />
<PackageReference Include="Nito.AsyncEx" />

View File

@@ -6,12 +6,12 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared.Components.Toast;
using MUnique.OpenMU.Web.Shared.Services;
/// <summary>

View File

@@ -6,13 +6,13 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
using System.ComponentModel.DataAnnotations;
using System.Threading;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.Interfaces;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared.Components.Modal;
using MUnique.OpenMU.Web.Shared.Components.Toast;
using MUnique.OpenMU.Web.Shared.Services;
/// <summary>

View File

@@ -6,7 +6,6 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
using System.Reflection;
using System.Threading;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
@@ -19,6 +18,7 @@ using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared;
using MUnique.OpenMU.Web.Shared.Components;
using MUnique.OpenMU.Web.Shared.Components.Modal;
using MUnique.OpenMU.Web.Shared.Components.Toast;
using MUnique.OpenMU.Web.Shared.Services;
/// <summary>

View File

@@ -8,7 +8,6 @@ using System.Collections;
using System.ComponentModel;
using System.Reflection;
using System.Threading;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.QuickGrid;
using Microsoft.Extensions.Logging;
@@ -18,6 +17,7 @@ using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared;
using MUnique.OpenMU.Web.Shared.Components.Modal;
using MUnique.OpenMU.Web.Shared.Components.Toast;
using MUnique.OpenMU.Web.Shared.Services;
/// <summary>

View File

@@ -6,7 +6,6 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
using System.Reflection;
using System.Threading;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.Routing;
@@ -19,6 +18,7 @@ using MUnique.OpenMU.Web.Shared;
using MUnique.OpenMU.Web.Shared.Components;
using MUnique.OpenMU.Web.Shared.Components.MapEditor;
using MUnique.OpenMU.Web.Shared.Components.Modal;
using MUnique.OpenMU.Web.Shared.Components.Toast;
using MUnique.OpenMU.Web.Shared.Services;
/// <summary>

View File

@@ -6,7 +6,6 @@ namespace MUnique.OpenMU.Web.AdminPanel.Pages;
using System.ComponentModel;
using System.Threading;
using Blazored.Toast.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.QuickGrid;
using Microsoft.AspNetCore.Components.Routing;
@@ -16,6 +15,7 @@ using MUnique.OpenMU.DataModel.Configuration;
using MUnique.OpenMU.DataModel.Entities;
using MUnique.OpenMU.Persistence;
using MUnique.OpenMU.Web.AdminPanel.Properties;
using MUnique.OpenMU.Web.Shared.Components.Toast;
using MUnique.OpenMU.Web.Shared.Services;
/// <summary>

View File

@@ -2,7 +2,6 @@
@using System.ComponentModel
@using Microsoft.Extensions.DependencyInjection
@using Blazored.Toast.Services
@using MUnique.OpenMU.Interfaces
@using MUnique.OpenMU.Web.AdminPanel.Properties

View File

@@ -5,7 +5,6 @@
namespace MUnique.OpenMU.Web.AdminPanel;
using System.IO;
using Blazored.Toast;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
@@ -61,7 +60,7 @@ public class Startup
.ConfigureApplicationPartManager(setup =>
setup.FeatureProviders.Add(new GenericControllerFeatureProvider()));
services.AddBlazoredToast();
services.AddToasts();
services.AddScoped<ModalService>();
services.AddScoped<IModalService>(sp => sp.GetRequiredService<ModalService>());

View File

@@ -5,7 +5,6 @@
namespace MUnique.OpenMU.Web.AdminPanel;
using System.IO;
using Blazored.Toast;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting.StaticWebAssets;
using Microsoft.Extensions.DependencyInjection;
@@ -66,7 +65,7 @@ public static class WebApplicationExtensions
.ConfigureApplicationPartManager(setup =>
setup.FeatureProviders.Add(new GenericControllerFeatureProvider()));
services.AddBlazoredToast();
services.AddToasts();
services.AddScoped<ModalService>();
services.AddScoped<IModalService>(sp => sp.GetRequiredService<ModalService>());

View File

@@ -10,9 +10,6 @@
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.JSInterop
@using Blazored.Toast
@using Blazored.Toast.Services
@using BlazorInputFile
@using MUnique.OpenMU.Web.AdminPanel
@@ -20,6 +17,7 @@
@using MUnique.OpenMU.Web.Shared
@using MUnique.OpenMU.Web.Shared.Components
@using MUnique.OpenMU.Web.Shared.Components.Toast
@using MUnique.OpenMU.Web.Shared.Components.Modal
@using MUnique.OpenMU.Web.Shared.Components.Form
@using MUnique.OpenMU.Web.Shared.Components.Form.Modal

View File

@@ -0,0 +1,63 @@
// <copyright file="IToastService.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Web.Shared.Components.Toast;
using System;
using System.Collections.Generic;
/// <summary>
/// Service for showing toast notifications.
/// </summary>
public interface IToastService
{
/// <summary>
/// Occurs when the list of toasts has changed (added, closed, cleared).
/// </summary>
event Action? StateChanged;
/// <summary>
/// Gets the currently shown toasts.
/// </summary>
IReadOnlyList<ToastInstance> Toasts { get; }
/// <summary>
/// Shows a success toast.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="heading">The optional heading.</param>
void ShowSuccess(string message, string? heading = null);
/// <summary>
/// Shows an info toast.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="heading">The optional heading.</param>
void ShowInfo(string message, string? heading = null);
/// <summary>
/// Shows a warning toast.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="heading">The optional heading.</param>
void ShowWarning(string message, string? heading = null);
/// <summary>
/// Shows an error toast.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="heading">The optional heading.</param>
void ShowError(string message, string? heading = null);
/// <summary>
/// Closes the specified toast (triggers its closing animation).
/// </summary>
/// <param name="toast">The toast to close.</param>
void Close(ToastInstance toast);
/// <summary>
/// Closes all currently shown toasts.
/// </summary>
void Clear();
}

View File

@@ -0,0 +1,66 @@
@using MUnique.OpenMU.Web.Shared.Components.Toast
@using MUnique.OpenMU.Web.Shared.Services
@inject ToastService ToastService
@implements IDisposable
@if (this.ToastService.Toasts.Count > 0)
{
<div class="toast-container position-fixed top-0 end-0 p-3" aria-live="polite" aria-atomic="true">
@foreach (var toast in this.ToastService.Toasts)
{
var (iconClass, accentClass) = this.GetStyling(toast.Level);
<div @key="toast.Key" class="toast @accentClass @(toast.IsClosing ? "closing" : "show")" role="alert" aria-live="assertive" aria-atomic="true">
<div class="d-flex">
<div class="toast-body d-flex align-items-start">
<span class="oi @iconClass toast__icon" aria-hidden="true"></span>
<div class="ms-2 w-100">
@if (toast.Heading is { } heading)
{
<strong class="d-block">@heading</strong>
}
<div>@toast.Message</div>
</div>
</div>
<button type="button" class="btn-close me-2 m-auto" data-bs-dismiss="toast" aria-label="Close" @onclick="() => this.CloseAsync(toast)"></button>
</div>
</div>
}
</div>
}
@code {
/// <inheritdoc />
protected override void OnInitialized()
{
this.ToastService.StateChanged += this.OnStateChanged;
}
/// <inheritdoc />
public void Dispose()
{
this.ToastService.StateChanged -= this.OnStateChanged;
}
private void OnStateChanged()
{
_ = this.InvokeAsync(this.StateHasChanged);
}
private Task CloseAsync(ToastInstance toast)
{
this.ToastService.Close(toast);
return Task.CompletedTask;
}
private (string IconClass, string AccentClass) GetStyling(ToastLevel level)
{
return level switch
{
ToastLevel.Success => ("oi-circle-check", "text-success"),
ToastLevel.Info => ("oi-info", "text-primary"),
ToastLevel.Warning => ("oi-warning", "text-warning"),
ToastLevel.Error => ("oi-bolt", "text-danger"),
_ => ("oi-info", "text-primary"),
};
}
}

View File

@@ -0,0 +1,34 @@
.toast-container {
z-index: 1080;
}
.toast {
min-width: 18rem;
border-left: 4px solid currentColor;
background-color: var(--bs-body-bg);
box-shadow: var(--bs-box-shadow-lg);
animation: toast-slide-in 0.2s ease-out;
opacity: 1;
transition: opacity 0.3s ease;
}
.toast.closing {
opacity: 0;
}
.toast__icon {
font-size: 1.1rem;
line-height: 1;
align-self: center;
}
@keyframes toast-slide-in {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}

View File

@@ -0,0 +1,52 @@
// <copyright file="ToastInstance.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Web.Shared.Components.Toast;
using System;
/// <summary>
/// Represents a single toast message shown in the <see cref="ToastContainer"/>.
/// </summary>
public sealed class ToastInstance
{
/// <summary>
/// Initializes a new instance of the <see cref="ToastInstance"/> class.
/// </summary>
/// <param name="level">The level.</param>
/// <param name="message">The message.</param>
/// <param name="heading">The optional heading.</param>
internal ToastInstance(ToastLevel level, string message, string? heading)
{
this.Key = Guid.NewGuid();
this.Level = level;
this.Message = message;
this.Heading = heading;
}
/// <summary>
/// Gets a stable key identifying this toast, used as a render key.
/// </summary>
public Guid Key { get; }
/// <summary>
/// Gets the level.
/// </summary>
public ToastLevel Level { get; }
/// <summary>
/// Gets the message.
/// </summary>
public string Message { get; }
/// <summary>
/// Gets the optional heading.
/// </summary>
public string? Heading { get; }
/// <summary>
/// Gets or sets a value indicating whether the toast is performing its closing animation.
/// </summary>
internal bool IsClosing { get; set; }
}

View File

@@ -0,0 +1,31 @@
// <copyright file="ToastLevel.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Web.Shared.Components.Toast;
/// <summary>
/// The level of a toast message.
/// </summary>
public enum ToastLevel
{
/// <summary>
/// Informational message.
/// </summary>
Info,
/// <summary>
/// Success message.
/// </summary>
Success,
/// <summary>
/// Warning message.
/// </summary>
Warning,
/// <summary>
/// Error message.
/// </summary>
Error,
}

View File

@@ -52,7 +52,6 @@
<ItemGroup>
<PackageReference Include="BCrypt.Net-Next" />
<PackageReference Include="Blazored.Toast" />
<PackageReference Include="BlazorInputFile" />
<PackageReference Include="Mapster" />
<PackageReference Include="Microsoft.AspNetCore.Components.QuickGrid" />

View File

@@ -0,0 +1,172 @@
// <copyright file="ToastService.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Web.Shared.Services;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using MUnique.OpenMU.Web.Shared.Components.Toast;
/// <summary>
/// Default implementation of <see cref="IToastService"/>.
/// </summary>
public sealed class ToastService : IToastService, IDisposable
{
private static readonly TimeSpan DefaultDuration = TimeSpan.FromSeconds(5);
private static readonly TimeSpan ClosingDuration = TimeSpan.FromMilliseconds(300);
private readonly object _lock = new();
private readonly List<ToastInstance> _toasts = new();
private readonly List<CancellationTokenSource> _cancellations = new();
/// <inheritdoc />
public event Action? StateChanged;
/// <inheritdoc />
public IReadOnlyList<ToastInstance> Toasts
{
get
{
lock (this._lock)
{
return this._toasts.ToArray();
}
}
}
/// <inheritdoc />
public void ShowSuccess(string message, string? heading = null)
{
this.Show(ToastLevel.Success, message, heading);
}
/// <inheritdoc />
public void ShowInfo(string message, string? heading = null)
{
this.Show(ToastLevel.Info, message, heading);
}
/// <inheritdoc />
public void ShowWarning(string message, string? heading = null)
{
this.Show(ToastLevel.Warning, message, heading);
}
/// <inheritdoc />
public void ShowError(string message, string? heading = null)
{
this.Show(ToastLevel.Error, message, heading);
}
/// <inheritdoc />
public void Close(ToastInstance toast)
{
this.StartClosing(toast);
}
/// <inheritdoc />
public void Clear()
{
lock (this._lock)
{
foreach (var cts in this._cancellations)
{
cts.Cancel();
}
this._cancellations.Clear();
this._toasts.Clear();
}
this.StateChanged?.Invoke();
}
/// <inheritdoc />
public void Dispose()
{
lock (this._lock)
{
foreach (var cts in this._cancellations)
{
cts.Dispose();
}
this._cancellations.Clear();
this._toasts.Clear();
}
}
private void Show(ToastLevel level, string message, string? heading)
{
var toast = new ToastInstance(level, message, heading);
var cts = new CancellationTokenSource();
lock (this._lock)
{
this._toasts.Add(toast);
this._cancellations.Add(cts);
}
this.StateChanged?.Invoke();
_ = this.AutoCloseAsync(toast, cts);
}
private async Task AutoCloseAsync(ToastInstance toast, CancellationTokenSource cts)
{
try
{
await Task.Delay(DefaultDuration, cts.Token).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
this.StartClosing(toast);
}
private void StartClosing(ToastInstance toast)
{
lock (this._lock)
{
var index = this._toasts.IndexOf(toast);
if (index < 0 || toast.IsClosing)
{
return;
}
toast.IsClosing = true;
this._cancellations[index].Cancel();
}
this.StateChanged?.Invoke();
_ = this.FinishClosingAsync(toast);
}
private Task FinishClosingAsync(ToastInstance toast)
{
return Task.Run(async () =>
{
await Task.Delay(ClosingDuration).ConfigureAwait(false);
lock (this._lock)
{
var index = this._toasts.IndexOf(toast);
if (index < 0)
{
return;
}
this._cancellations[index].Dispose();
this._cancellations.RemoveAt(index);
this._toasts.RemoveAt(index);
}
this.StateChanged?.Invoke();
});
}
}

View File

@@ -0,0 +1,26 @@
// <copyright file="ToastServiceCollectionExtensions.cs" company="MUnique">
// Licensed under the MIT License. See LICENSE file in the project root for full license information.
// </copyright>
namespace MUnique.OpenMU.Web.Shared.Services;
using Microsoft.Extensions.DependencyInjection;
using MUnique.OpenMU.Web.Shared.Components.Toast;
/// <summary>
/// Extension methods for registering the toast service.
/// </summary>
public static class ToastServiceCollectionExtensions
{
/// <summary>
/// Adds the toast service to the service collection.
/// </summary>
/// <param name="services">The service collection.</param>
/// <returns>The service collection, for chaining.</returns>
public static IServiceCollection AddToasts(this IServiceCollection services)
{
services.AddScoped<ToastService>();
services.AddScoped<IToastService>(sp => sp.GetRequiredService<ToastService>());
return services;
}
}

View File

@@ -6,9 +6,6 @@
@using Microsoft.Extensions.Localization
@using Microsoft.JSInterop
@using Blazored.Toast
@using Blazored.Toast.Services
@using BlazorInputFile
@using MUnique.OpenMU.Web.Shared