Blazor Bootstrap Toaster






3.67/5 (4投票s)
如何在 Blazor 中构建 Bootstrap Toaster。
引言
本文介绍了如何在 Blazor 中构建一个简单的 Bootstrap Toaster。
它演示了几个适用于几乎所有 Blazor 应用程序的编程原则和编码模式。
-
关注点分离 - 数据不应存在于 UI 中。Toaster UI 组件不包含任何数据或数据管理。它的任务是显示提示消息。
-
Blazor 显示/隐藏模式 - 我曾不愿称之为模式,但程序员尝试使用 JSInterop 实现此功能的次数之多让我改变了主意。此模式在组件中实现了 C# 中的 CSS 框架
.Show()
和.Hide()
JavaScript 功能。 -
Blazor 通知模式 - 使用事件将 UI 组件与驱动其行为的底层数据解耦。
-
值对象 - 现代设计强调在适当的地方使用值对象。
代码库和演示站点
您可以在我的 Blazr.Demo.Toaster 代码库 中找到代码。
演示站点可以在 https://blazr-demo-database-server.azurewebsites.net 找到。
代码类
Toast
首先是一个用于消息颜色的 enum
。它直接使用 Bootstrap 的命名规范,以便于构建 Css 字符串。
public enum MessageColour
{
Primary, Secondary, Dark, Light, Success, Danger, Warning, Info
}
Toast
被声明为一个值对象。一旦我们创建了一个实例,我们就没有理由去改变它。
- Toast 被声明为一个
record
。 - 有五个
public
属性供 UI 使用来显示 Toast。所有属性都声明为不可变的,使用{ get; init; }
。 TimeToBurn
使用DateTimeOffset
来提供与时区无关的绝对时间。
public record Toast
{
public Guid Id = Guid.NewGuid();
public string Title { get; init; } = string.Empty;
public string Message { get; init; } = string.Empty;
public MessageColour MessageColour { get; init; } = MessageColour.Primary;
public DateTimeOffset TimeToBurn { get; init; } = DateTimeOffset.Now.AddSeconds(30);
下一篇
Posted
是一个对象创建时间戳。IsBurnt
是一个bool
,用于检查 Toast 是否过期。elapsedTime
用于构建ElapsedTimeText
。ElapsedTimeText
由 UI 组件使用。
public readonly DateTimeOffset Posted = DateTimeOffset.Now;
public bool IsBurnt => TimeToBurn < DateTimeOffset.Now;
private TimeSpan elapsedTime => Posted - DateTimeOffset.Now;
public string ElapsedTimeText =>
elapsedTime.Seconds > 60
? $"posted {-elapsedTime.Minutes} mins ago"
: $"posted {-elapsedTime.Seconds} secs ago";
最后,一个 static
构造函数辅助方法。
public static Toast NewToast(string title, string message,
MessageColour messageColour, int secsToLive)
=> new Toast
{
Title = title,
Message = message,
MessageColour = messageColour,
TimeToBurn = DateTimeOffset.Now.AddSeconds(secsToLive)
};
Toaster Service
ToasterService
是一个依赖注入服务,用于保存和管理 Toasts。它有一个私有列表来保存 toasts,并提供添加和清除方法。还有一个定时器用于触发 ClearBurntToast
来清除过期的 toasts,并在必要时引发 ToasterChanged
事件。它还在每个定时器周期引发 ToasterTimerElapsed
事件。
- 它是一个标准的类。
- 实现了
IDisposable
,因为它向需要正确处理的定时器注册了一个事件处理程序。 - 拥有一个只读的私有
Toast
实例集合。列表在内部进行管理。 - 拥有一个内部定时器,驱动着典型的服务行为。
public class ToasterService : IDisposable
{
private readonly List<Toast> _toastList = new List<Toast>();
private System.Timers.Timer _timer = new System.Timers.Timer();
有两个 public
事件,其他服务或 UI 组件可以订阅。
- 每当 toast 列表发生更改时,都会引发
ToasterChanged
。 - 在每个定时器循环中都会引发
ToasterTimerElapsed
。 HasToasts
是一个简单的状态布尔值。
public event EventHandler? ToasterChanged;
public event EventHandler? ToasterTimerElapsed;
public bool HasToasts => _toastList.Count > 0;
ClearBurntToast
是我们的 toast 列表管理方法。它检查是否有任何过期的 toast。如果有,它会清除它们并引发 ToasterChanged
事件。
private bool ClearBurntToast()
{
var toastsToDelete = _toastList.Where(item => item.IsBurnt).ToList();
if (toastsToDelete is not null && toastsToDelete.Count > 0)
{
toastsToDelete.ForEach(toast => _toastList.Remove(toast));
this.ToasterChanged?.Invoke(this, EventArgs.Empty);
return true;
}
return false;
}
TimerElapsed
是我们的定时器已过事件的处理程序。它清除任何过期的 toast 并引发 ToasterTimerElapsed
事件。
private void TimerElapsed(object? sender, ElapsedEventArgs e)
{
this.ClearBurntToast();
this.ToasterTimerElapsed?.Invoke(this, EventArgs.Empty);
}
构造函数添加了一个欢迎 Toast
,设置了定时器并注册了事件处理程序。
public ToasterService()
{
AddToast(new Toast { Title = "Welcome Toast",
Message = "Welcome to this Application. I'll disappear after 15 seconds.",
TTD = DateTimeOffset.Now.AddSeconds(10) });
_timer.Interval = 5000;
_timer.AutoReset = true;
_timer.Elapsed += this.TimerElapsed;
_timer.Start();
}
CRUD 类型操作是自明的。每个操作都会调用 ClearBurntToast
来运行管理方法。
public List<Toast> GetToasts()
{
ClearBurntToast();
return _toastList;
}
public void AddToast(Toast toast)
{
_toastList.Add(toast);
<span class="pl-c"> // only raise the ToasterChanged event if it hasn't already
// been raised by ClearBurntToast
if (!this.ClearBurntToast())
this.ToasterChanged?.Invoke(this, EventArgs.Empty);
}
public void ClearToast(Toast toast)
{
if (_toastList.Contains(toast))
{
_toastList.Remove(toast);
<span class="pl-c"> // only raise the ToasterChanged event if it hasn't already
// been raised by ClearBurntToast
if (!this.ClearBurntToast())
this.ToasterChanged?.Invoke(this, EventArgs.Empty);
}
}
最后,Dispose
方法清除定时器事件处理程序。
public void Dispose()
{
if (_timer is not null)
{
_timer.Elapsed += this.TimerElapsed;
_timer.Stop();
}
}
}
ToasterService
可以作为 Scoped
或 Singleton
服务运行,具体取决于您的用途。
Toaster
Toaster
是 UI 组件。
Razor 标记实现了 Bootstrap Toast 标记,使用 foreach
循环添加每个 toast。标记将按堆叠方式显示 Toast,位于右上角。
- 组件检查是否有内容可显示 -
this.toasterService.HasToasts
。如果没有,则不渲染任何内容 - 这是 Blazor 显示/隐藏模式。 @_toastCss
获取正确的消息颜色 Css 字符串。- 关闭的 X 调用
this.ClearToast(toast)
来删除一个 toast。
@implements IDisposable
@if (this.toasterService.HasToasts)
{
<div class="">
<div class="toast-container position-absolute top-0 end-0 mt-5 pt-5 pe-2">
@foreach (var toast in this.toasterService.GetToasts())
{
var _toastCss = toastCss(toast);
<div class="toast show" role="alert" aria-live="assertive" aria-atomic="true">
<div class="toast-header @_toastCss">
<strong class="me-auto">@toast.Title</strong>
<small class="@_toastCss">@toast.ElapsedTimeText</small>
<button type="button" class="btn-close btn-close-white"
aria-label="Close" @onclick="() => this.ClearToast(toast)"></button>
</div>
<div class="toast-body">
@toast.Message
</div>
</div>
}
</div>
</div>
}
后台代码类
- 继承自
ComponentBase
并实现IDisposable
以取消注册事件处理程序。 - 注入
ToasterService
。 - 创建
ToasterService
的第二个null
可空引用。我们处于可空世界,但 C# 编译器不知道ToasterService
不会是null
,所以我们创建第二个null
可空引用,这样我们在使用ToasterService
实例时就不需要每次都进行null
检查。
public partial class Toaster : ComponentBase, IDisposable
{
[Inject] private ToasterService? _toasterService { get; set; }
private ToasterService toasterService => _toasterService!;
ToastChanged
是 ToasterService
事件的处理程序。它在 UI 线程上调用 StateHasChanged
。
private void ToastChanged(object? sender, EventArgs e)
=> this.InvokeAsync(this.StateHasChanged);
OnInitialized
将两个 ToasterService
事件注册到 ToastChanged
,而 Dispose
则移除它们。
protected override void OnInitialized()
{
this.toasterService.ToasterChanged += ToastChanged;
this.toasterService.ToasterTimerElapsed += ToastChanged;
}
public void Dispose()
{
this.toasterService.ToasterChanged -= ToastChanged;
this.toasterService.ToasterTimerElapsed -= ToastChanged;
}
最后,ClearToast
清除服务中的选定 toast,toastCss
获取 toast 的背景颜色。
private void ClearToast(Toast toast)
=> toasterService.ClearToast(toast);
private string toastCss(Toast toast)
{
var colour = Enum.GetName(typeof(MessageColour), toast.MessageColour)?.ToLower();
return toast.MessageColour switch
{
MessageColour.Light => "bg-light",
_ => $"bg-{colour} text-white"
};
}
}
实施
-
在 Program 中将
ToasterService
添加到 DI 服务容器。 -
将组件添加到
Layout
或App
或您希望使用它的任何地方。<CascadingAuthenticationState> <Router AppAssembly="@typeof(App).Assembly" PreferExactMatches="@true"> .... </Router> </CascadingAuthenticationState> <Toaster />
- 将服务注入到任何您想显示 Toast 的页面中,并调用
AddToast
。下面的示例显示了一个演示Index
页面。@page "/" <PageTitle>Index</PageTitle> <h1>Hello, world!</h1> Welcome to your new app. <SurveyPrompt Title="How is Blazor working for you?" /> <div class="m-2 p-2"> <button class="btn btn-primary" @onclick="AddToast" >Add a Toast</button> </div> @code { [Inject] private ToasterService? _toasterService {get; set;} private ToasterService toasterService => _toasterService!; private void AddToast() => toasterService.AddToast(Toast.NewToast ("Hello World", "Hello from Blazor", MessageColour.Info, 30)); }
总结
该设计展示了数据与 UI 的清晰分离。所有数据处理都在 ToasterService
中进行。Toaster
使用 ToasterService
中数据对象的引用。
Blazor 通知模式用于在 toast 列表发生更改时更新 UI。Toaster
向两个 ToasterService
事件注册一个事件处理程序,该处理程序会在事件发生时重新渲染组件。
Toaster
展示了如何根据状态显示和隐藏 UI 标记。
Toast
是一个值对象。它简化了相等性检查(我们在这里不进行任何检查),并确保 toast 一旦创建就无法被修改。
历史
- 2022 年 1 月 18 日:初始版本