65.9K
CodeProject 正在变化。 阅读更多。
Home

Blazor Bootstrap Toaster

starIconstarIconstarIcon
emptyStarIcon
starIcon
emptyStarIcon

3.67/5 (4投票s)

2022 年 1 月 18 日

CPOL

4分钟阅读

viewsIcon

17391

如何在 Blazor 中构建 Bootstrap Toaster。

引言

本文介绍了如何在 Blazor 中构建一个简单的 Bootstrap Toaster。

它演示了几个适用于几乎所有 Blazor 应用程序的编程原则和编码模式。

  1. 关注点分离 - 数据不应存在于 UI 中。Toaster UI 组件不包含任何数据或数据管理。它的任务是显示提示消息。

  2. Blazor 显示/隐藏模式 - 我曾不愿称之为模式,但程序员尝试使用 JSInterop 实现此功能的次数之多让我改变了主意。此模式在组件中实现了 C# 中的 CSS 框架 .Show().Hide() JavaScript 功能。

  3. Blazor 通知模式 - 使用事件将 UI 组件与驱动其行为的底层数据解耦。

  4. 值对象 - 现代设计强调在适当的地方使用值对象。

代码库和演示站点

您可以在我的 Blazr.Demo.Toaster 代码库 中找到代码。

演示站点可以在 https://blazr-demo-database-server.azurewebsites.net 找到。

Example

代码类

Toast

首先是一个用于消息颜色的 enum。它直接使用 Bootstrap 的命名规范,以便于构建 Css 字符串。

public enum MessageColour
{
    Primary, Secondary, Dark, Light, Success, Danger, Warning, Info
}

Toast 被声明为一个值对象。一旦我们创建了一个实例,我们就没有理由去改变它。

  1. Toast 被声明为一个 record
  2. 有五个 public 属性供 UI 使用来显示 Toast。所有属性都声明为不可变的,使用 { get; init; }
  3. 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);

下一篇

  1. Posted 是一个对象创建时间戳。
  2. IsBurnt 是一个 bool,用于检查 Toast 是否过期。
  3. elapsedTime 用于构建 ElapsedTimeText
  4. 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 事件。

  1. 它是一个标准的类。
  2. 实现了 IDisposable,因为它向需要正确处理的定时器注册了一个事件处理程序。
  3. 拥有一个只读的私有 Toast 实例集合。列表在内部进行管理。
  4. 拥有一个内部定时器,驱动着典型的服务行为。
public class ToasterService : IDisposable
{
    private readonly List<Toast> _toastList = new List<Toast>();
    private System.Timers.Timer _timer = new System.Timers.Timer();

有两个 public 事件,其他服务或 UI 组件可以订阅。

  1. 每当 toast 列表发生更改时,都会引发 ToasterChanged
  2. 在每个定时器循环中都会引发 ToasterTimerElapsed
  3. 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 可以作为 ScopedSingleton 服务运行,具体取决于您的用途。

Toaster

Toaster 是 UI 组件。

Razor 标记实现了 Bootstrap Toast 标记,使用 foreach 循环添加每个 toast。标记将按堆叠方式显示 Toast,位于右上角。

  1. 组件检查是否有内容可显示 - this.toasterService.HasToasts。如果没有,则不渲染任何内容 - 这是 Blazor 显示/隐藏模式。
  2. @_toastCss 获取正确的消息颜色 Css 字符串。
  3. 关闭的 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>
}

后台代码类

  1. 继承自 ComponentBase 并实现 IDisposable 以取消注册事件处理程序。
  2. 注入 ToasterService
  3. 创建 ToasterService 的第二个 null 可空引用。我们处于可空世界,但 C# 编译器不知道 ToasterService 不会是 null,所以我们创建第二个 null 可空引用,这样我们在使用 ToasterService 实例时就不需要每次都进行 null 检查。
public partial class Toaster : ComponentBase, IDisposable
{
    [Inject] private ToasterService? _toasterService { get; set; }

    private ToasterService toasterService => _toasterService!;

ToastChangedToasterService 事件的处理程序。它在 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"
        };
    }
}

实施

  1. 在 Program 中将 ToasterService 添加到 DI 服务容器。

  2. 将组件添加到 LayoutApp 或您希望使用它的任何地方。

    <CascadingAuthenticationState>
        <Router AppAssembly="@typeof(App).Assembly" PreferExactMatches="@true">
        ....
        </Router>
    </CascadingAuthenticationState>
    <Toaster />
  3. 将服务注入到任何您想显示 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 日:初始版本
© . All rights reserved.