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

构建 Blazor 基础组件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (8投票s)

2023 年 7 月 14 日

MIT

8分钟阅读

viewsIcon

15094

如何为 Blazor 构建一套基础组件

引言

本文介绍如何为 Blazor 构建一套基础组件。

在深入探讨细节之前,请考虑这个简单的组件,它显示了一个 Bootstrap 警告框。

@if (Message is not null)
{
    <div class="alert @_alertType">
        @this.Message
    </div>
}

@code {
    [Parameter] public string? Message { get; set; }
    [Parameter] public AlertType MessageType { get; set; } = BasicAlert.AlertType.Info;

    private string _alertType => this.MessageType switch
    {
        AlertType.Success => "alert-success",
        AlertType.Warning => "alert-warning",
        AlertType.Error => "alert-danger",
        _ =>  "alert-primary"
    };

    public enum AlertType
    {
        Info,
        Success,
        Error,
        Warning,
    }
}

它只使用了 `ComponentBase` 中很少一部分功能。没有生命周期代码,没有 UI 事件,也没有渲染后代码。

想想每天有多少组件实例被加载到内存中,有多少次它们被不必要地重新渲染。大量的生命周期异步方法被调用,创建然后又因为没有原因而处置 Task 状态机。浪费了大量的 CPU 周期和内存,而这些都是您(以及地球)正在为此付费的。

这样的组件急需一个更简洁、占用空间更小的基础组件。

我冒着风险 [基于我自己的经验] 推测,99% 的组件都可以采用更轻量级的基类组件。

在本文中,我将介绍如何构建这些更简单、占用空间更小的基类组件。我有三个。它们形成了一个简单的继承体系:最低层的组件实现了所有组件所需的核心功能,更高级的组件添加了额外功能。顶层组件是 `ComponentBase` 的黑盒替代品,并带有一些附加功能。

将 `FetchData` 或 `Counter` 或您使用的任何其他组件的继承更改为 `BlazrControlBase`,您可能不会看到任何区别。如果需要,请更新为 `BlazrComponentBase`。

存储库

本文的存储库是 Blazr.BaseComponents

三个组件

  1. BlazrUIBase 是一个功能最少的简单 UI 组件。
  2. BlazrControlBase 是一个中级控件组件,具有单个生命周期方法和单个渲染模型。
  3. BlazrComponentBase 是一个完整的 `ComponentBase` 替代品,具有一些额外的 Wrapper/Frame 功能。

BlazrBaseComponent

所有组件都继承自 `BlazrBaseComponent`。它是基类组件的基础类!

这是一个实现所有组件使用的样板代码的标准类。它是抽象的,不实现 `IComponent`。继承类实现 `IComponent`,并且可以将 `SetParametersAsync` 设置为 virtual,或者固定它。

它复制了 `ComponentBase` 的大部分变量和属性,以保持熟悉感。

区别在于

  1. Initialized 标志已更改。它被反转了,现在是 protected,因此继承类可以访问它。它有一个 NotInitialized 对应项:无需使用笨拙的 if(!Initialized) 条件代码。
  2. 它有一个 Guid 标识符:在调试中跟踪实例很有用,并且在一些我更高级的组件中使用。
  3. 它有两个 RenderFragments 来实现 Wrapper/Frame 功能。Frame 定义了包裹 Body 的代码。Frame 是可空的:如果它是 null,则组件直接渲染 Body
public abstract class BlazrBaseComponent
{
    private RenderHandle _renderHandle;
    private RenderFragment _content;
    private bool _renderPending;
    private bool _hasNeverRendered = true;

    protected bool Initialized;
    protected bool NotInitialized => !this.Initialized;

    protected virtual RenderFragment? Frame { get; set; }
    protected RenderFragment Body { get; init; }

    public Guid ComponentUid { get; init; } = Guid.NewGuid();

构造函数实现了包装器功能

  1. 它将渲染代码 BuildRenderTree 分配给 Body
  2. 它设置分配给 _content 的 lambda 方法:StateHasChanged 传递给 Renderer 的渲染片段。
  3. 如果 Frame 不为 null,则 lambda 方法将 Frame 分配给 _content,否则分配 Body
  4. lambda 方法在完成时将 Initialized 设置为 true

稍后将详细介绍 frame/wrapper 功能。

public BlazrBaseComponent()
{
    this.Body = (builder) => this.BuildRenderTree(builder);

    _content = (builder) =>
    {
        _renderPending = false;
        _hasNeverRendered = false;
        if (Frame is not null)
            Frame.Invoke(builder);
        else
            BuildRenderTree(builder);

        this.Initialized = true;
    };
}

其余代码复制了 `ComponentBase` 的基本方法。

RenderAsync 是一个附加方法,可立即渲染组件。它通过调用 StateHasChanged 来工作,并通过调用 await Task.Yield() 立即返回。调用者返回到 Render 并释放 UI 同步上下文:Renderer 服务其队列并渲染组件。

public void Attach(RenderHandle renderHandle)
    => _renderHandle = renderHandle;

protected abstract void BuildRenderTree(RenderTreeBuilder builder);

public async Task RenderAsync()
{
    this.StateHasChanged();
    await Task.Yield();
}

public void StateHasChanged()
{
    if (_renderPending)
        return;

    var shouldRender = _hasNeverRendered || this.ShouldRender() || 
                       _renderHandle.IsRenderingOnMetadataUpdate;

    if (shouldRender)
    {
        _renderPending = true;
        _renderHandle.Render(_content);
    }
}

protected virtual bool ShouldRender() => true;

protected Task InvokeAsync(Action workItem)
    => _renderHandle.Dispatcher.InvokeAsync(workItem);

protected Task InvokeAsync(Func<Task> workItem)
    => _renderHandle.Dispatcher.InvokeAsync(workItem);

注意:没有生命周期方法或 `SetParametersAsync` 的实现。继承类实现 `IComponent`。它们可以选择通过将其设置为 virtual 来使 `SetParametersAsync` 开放,或者将其关闭。

BlazrUIBase

这是简单的实现

public class BlazrUIBase : BlazrBaseComponent, IComponent
{
    public Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);
        this.StateHasChanged();
        return Task.CompletedTask;
    }
}

它继承自 `BlazrBaseComponent` 并实现 `IComponent`。

  1. 它有一个固定的 `SetParametersAsync`:无法覆盖。
  2. 它没有生命周期方法。简单组件不需要它们。
  3. 它不实现 `IHandleEvent`,即它没有 UI 事件处理。如果您需要任何,请手动调用 `StateHasChanged`。
  4. 它不实现 `IHandleAfterRender`,即它没有渲染后处理。如果需要,请手动实现。

BlazrUIBase 演示

演示实现了上面的 BasicAlert,并添加了额外功能使其可关闭。

@inherits BlazrUIBase

@if (Message is not null)
{
    <div class="@_css">
        @this.Message
        @if(this.IsDismissible)
        {
            <button type="button" class="btn-close" @onclick=this.Dismiss>
            </button>
        }
    </div>
}

@code {
    [Parameter] public string? Message { get; set; }
    [Parameter] public bool IsDismissible { get; set; }
    [Parameter] public EventCallback<string?> MessageChanged { get; set; }
    [Parameter] public AlertType MessageType { get; set; } = Alert.AlertType.Info;

    private string _css => new CSSBuilder("alert")
        .AddClass(_alertType)
        .AddClass(this.IsDismissible, "alert-dismissible")
        .Build();
    
        private void Dismiss()
            => MessageChanged.InvokeAsync(null);
    
    //... AlertType and _alertType code
}

以及演示 AlertPage

@page "/AlertPage"
@inherits BlazrControlBase
<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<div class="m-2">
    <button class="btn btn-success" @onclick="() => 
     this.SetMessageAsync(_timeString)">Set Message</button>
    <button class="btn btn-danger" @onclick="() => 
     this.SetMessageAsync(null)">Clear Message</button>
</div>

<div class="m-3 p-2 border border-1 border-success rounded-3">
    <h5>Dismisses Correctly</h5>
    <Alert @bind-Message=@_message1 MessageType=Alert.AlertType.Success />
</div>

<div class="m-3 p-2 border border-1 border-danger rounded-3">
    <h5>Does Not Dismiss</h5>
    <Alert Message=@_message2 MessageType=Alert.AlertType.Error />
</div>

@code {
    private string? _message1;
    private string? _message2;
    private string _timeString => $"Set at {DateTime.Now.ToLongTimeString()}";

    private Task SetMessageAsync(string? message)
    {
        _message1 = message;
        _message2 = message;
        this.StateHasChanged();
        return Task.CompletedTask;
    }
}

此组件中有一些重要的设计要点需要消化。

Alert 实现组件绑定模式:一个传入的 Message getter 参数和一个出站的 MessageChanged EventCallback setter 参数。父级可以像这样将变量/属性绑定到组件 @bind-Message=_message

Alert 有一个 UI 事件,但没有实现 `IHandleEvent` 处理程序。Render 仍通过直接调用 UI 事件方法来处理事件。没有内置的 StateAsChanged() 调用。

在演示页面中,有两个 Alert 实例。一个通过 @bind-Message 连接,另一个通过 Message 参数连接。

当您运行代码并单击按钮时,第二个不会关闭 Alert。没有东西连接到 MessageChanged

另一方面,第一个可以工作,即使没有调用 StateHasChanged

Index 继承自 `BlazrControlBase`,因此在 UI 事件处理程序结束时有一个内置的 StateHasChanged 调用。

  1. AlertDismiss 方法调用 MessageChanged 并传递一个 null string
  2. UI 处理程序调用 Index 中的 Bind 处理程序。
  3. Bind 处理程序 [由 Razor 编译器创建] 将 _message 更新为 null
  4. UI 处理程序完成并调用 StateHasChanged
  5. Index 渲染。
  6. Renderer 检测到 Alert 上的 Message 参数已更改。它调用 Alert 上的 SetParametersAsync,传入修改后的 ParameterView
  7. Alert 渲染:Messagenull,因此它隐藏了警告框。
重要的教训是:始终测试您是否确实需要调用 StateHasChanged

继承 BlazrUIBase 的 AlertPage

我们可以将 AlertPage 上的继承降级为 BlazrUIBase 来试验渲染。

这样做后,什么都不会更新。没有警告框出现,因为在 UI 事件发生时 [并且没有 UI 渲染更新],没有发生 StateHasChanged() 调用。

我们可以通过在需要的地方添加 StateHasChanged 调用来修复此问题。

绑定将不再按宣传的那样工作,因为不再有注册的 UI 处理程序。渲染器直接调用绑定处理程序。没有内置的 StateHasChanged 调用。

要解决此问题,我们需要手动连接绑定。

  1. 添加一个处理程序来分配给 MessageChanged 回调。在设置 _message1 后,它会调用 StateHasChanged。我们已复制了原始过程。
    private Task OnUpdateMessage(string? value)
    {
        _message1 = value;
        this.StateHasChanged();
        return Task.CompletedTask;
    }
  2. 更改 Alert 组件上的绑定。
    <Alert @bind-Message:get=_message1 @bind-Message:set=
           this.OnUpdateMessage MessageType=Alert.AlertType.Success />
  3. 更新 SetMessageAsync 以调用 StateHasChanged
    private Task SetMessageAsync(string? message)
    {
        _message1 = message;
        _message2 = message;
        this.StateHasChanged();
        return Task.CompletedTask;
    }

现在一切正常,并且我们通过仅在需要时驱动渲染事件来提高效率。

BlazrControlBase

BlazrControlBase 是中间级别的组件。它是我的主力。

  1. 实现了 OnParametersSetAsync 生命周期方法。
  2. 实现单个渲染 UI 事件处理程序。
  3. 锁定了 `SetParametersAsync`:您无法覆盖它。
public abstract class BlazrControlBase : BlazrBaseComponent, IComponent, IHandleEvent
{
    public async Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);
        await this.OnParametersSetAsync();
        this.StateHasChanged();
    }

    protected virtual Task OnParametersSetAsync()
        => Task.CompletedTask;  

    async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? obj)
    {
        await item.InvokeAsync(obj);
        this.StateHasChanged();
    }
}

考虑这个。

您可以编写 OnParametersSetAsync 来运行初始化代码:BlazrBaseComponent 提供对 InitializedNotInitialized 的访问。OnInitialized{Async} 是多余的。

在简单场景中,您可以在 OnParametersSetAsync 中编写所有代码。在更复杂的场景中,您可以将初始化代码分解为一个或多个单独的方法。

protected override async Task OnParametersSetAsync()
 {
     if (this.NotInitialized)
     {
         // do initialization stuff here
     }
 }

您不需要同步版本。它们之间的开销没有区别

private Task DoParametersSet()
{
    OnParametersSet();
    return OnParametersSetAsync();
}

protected virtual void OnParametersSet()
{
    // Some sync code
}

protected virtual Task OnParametersSetAsync()
    => Task.CompletedTask;

并且

protected virtual Task OnParametersSetAsync() 
{
    // some sync code
    return Task.CompletedTask;
}

我想让它返回一个 ValueTask,但这会破坏兼容性。

BlazrControlBase 演示

演示构建了一个新版本的 FetchData,并展示了如何用基于 BlazrControlBase 的页面替换 ComponentBase 页面。

修改后的天气预报数据管道

首先,修改后的天气预报数据类和服务。

public class WeatherForecast
{
    public int Id { get; set; }
    public DateOnly Date { get; set; }
    public int TemperatureC { get; set; }
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
    public string? Summary { get; set; }
}
namespace Blazr.Server.Web.Data;

public class WeatherForecastService
{
    private List<WeatherForecast> _forecasts;
    private static readonly string[] Summaries = new[]
        { "Freezing", "Bracing", "Chilly", "Cool", "Mild", 
          "Warm", "Balmy", "Hot", "Sweltering", "Scorching"};

    public WeatherForecastService()
        => _forecasts = this.GetForecasts();

    public async ValueTask<IEnumerable<WeatherForecast>> GetForecastsAsync()
    {
        await Task.Delay(1000);
        return _forecasts.AsEnumerable();
    }

    public async ValueTask<WeatherForecast?> GetForecastAsync(int id)
    {
        await Task.Delay(1000);
        return _forecasts.FirstOrDefault(item => item.Id == id);
    }

    private List<WeatherForecast> GetForecasts()
    {
        var date = DateOnly.FromDateTime(DateTime.Now);
        return Enumerable.Range(1, 10).Select(index => new WeatherForecast
        {
            Id = index,
            Date = date.AddDays(index),
            TemperatureC = Random.Shared.Next(-20, 55),
            Summary = Summaries[Random.Shared.Next(Summaries.Length)]
        }).ToList();
    }
}

WeatherForecastViewer

此页面演示了各种功能,因此有一组按钮使用路由 [而不是仅更新 id 和显示的按钮事件处理程序] 在记录之间切换。它们路由到同一页面并修改 Id - /WeatherForecast/1

标记语言不言自明。它效率不高:这是保持简单的演示代码。

我想详细研究的代码是 OnParametersSetAsync

  1. NotInitialized 提供条件控制:仅在初始化时加载 WeatherForecast 列表。在 ComponentBase 中,此代码将在 OnInitializedAsync 中。
  2. hasIdChanged 检测 Id 是否已更改。它单独声明以使代码更清晰、更具表现力。编译器将对此进行优化。
  3. 仅在 Id 更改时获取新记录。
@page "/WeatherForecast/{Id:int}"
@inject WeatherForecastService service
@inherits BlazrControlBase

<h3>Country Viewer</h3>

<div class="bg-dark text-white m-2 p-2">
    @if (_record is not null)
    {
        <pre>Id : @_record.Id </pre>
        <pre>Name : @_record.Date </pre>
        <pre>Temp C : @_record.TemperatureC </pre>
        <pre>Temp F : @_record.TemperatureF </pre>
        <pre>Summary : @_record.Summary </pre>
    }
    else
    {
        <pre>No Record Loaded</pre>
    }
</div>

<div class="m-3 text-end">
    <div class="btn-group">
        @foreach (var forecast in _forecasts)
        {
            <a class="btn @this.SelectedCss(forecast.Id)" 
             href="@($"/WeatherForecast/{forecast.Id}")">@forecast.Id</a>
        }
    </div>
</div>
@code {
    [Parameter] public int Id { get; set; }

    private WeatherForecast? _record;
    private IEnumerable<WeatherForecast> _forecasts = 
                        Enumerable.Empty<WeatherForecast>();

    private int _id;

    private string SelectedCss(int value)
        => _id == value ? "btn-primary" : "btn-outline-primary";

    protected override async Task OnParametersSetAsync()
    {
        if (NotInitialized)
            _forecasts = await service.GetForecastsAsync();

        var hasIdChanged = this.Id != _id;

        _id = this.Id;

        if (hasIdChanged)
            _record = await service.GetForecastAsync(this.Id);
    }
}

BlazrComponentBase

完整的 `ComponentBase` 实现太长,无法在此处包含:它在附录中。

我不会用示例来烦扰您,因为它可以替换任何组件中的 `ComponentBase`。

BaseComponent 添加的功能

所有基类组件都附带一些额外功能。

Wrapper/Frame 功能

演示 Wrapper 组件。

请注意,wrapper 定义在 Frame 渲染片段中 [而不是主内容部分],并使用 Razor 内置的 __builder RenderTreeBuilder 实例。

@inherits BlazrControlBase

@*Code Here is redundant*@

@code {
    protected override RenderFragment Frame => (__builder) => 
    {
        <h2 class="text-primary">Welcome To Blazor</h2>
        <div class="border border-1 border-primary rounded-3 bg-light p-2">
            @this.Body
        </div>
    };
}

以及继承自 WrapperIndex

@page "/"
@page "/WrapperDemo"

@inherits Wrapper

<PageTitle>Index</PageTitle>

<h1>Hello, world!</h1>

Welcome to your new app.

<SurveyPrompt />

您得到的是

RenderAsync

当您转向单次渲染完成或手动渲染 UI 事件处理时,您 [编码员] 可以控制何时以及如何进行中间渲染。RenderAsync 可确保组件立即渲染。

以下页面演示了它的工作原理

@page "/Load"
@inherits BlazrControlBase
<h3>SequentialLoadPage</h3>

<div class="bg-dark text-white m-2 p-2">
    <pre>@this.Log.ToString()</pre>
</div>
@code {
    private StringBuilder Log = new();

    protected override async Task OnParametersSetAsync()
    {
        await GetData();
    }

    private async Task GetData()
    {
        for(var counter = 1; counter <= 10; counter++)
        {
            this.Log.AppendLine($"Fetched Record {counter}");
            await this.RenderAsync();
            await Task.Delay(500);
        }
    }
}

省略 await this.RenderAsync();,您将只获得最终结果。如果您在 ComponentBase 中运行此代码,您将获得第一次渲染,然后直到最后才发生任何事情。注释掉 RenderAsync,更改继承并尝试一下。

手动实现 OnAfterRender

如果您需要实现 OnAfterRender,这相对简单。

@implements IHandleAfterRender

//...  markup

@code {
    // Implement if need to detect first after render
    private bool _firstRender = true;

    Task IHandleAfterRender.OnAfterRenderAsync()
    {
        if (_firstRender)
        {
            // Do first render stuff
            _firstRender = false;
        }

        // Do subsequent render stuff
    }
}

整合

此演示页面扩展了 WeatherForecastViewer,在页面加载时使用我们之前开发的 Alert 组件添加状态信息。

同样,重要的代码在 OnParametersSetAsync 中。

代码使用 _message_alertType_dismissible 类变量来控制警告框和切换消息。最终完成的警告框设置为可关闭。

@page "/WeatherForecastWithStatus/{Id:int}"
@inject WeatherForecastService service
@inherits BlazrControlBase

<h3>Weather Forecast Viewer</h3>

<Alert @bind-Message=_message IsDismissible=_dismissible MessageType=_alertType/>

<div class="bg-dark text-white m-2 p-2">
    @if (_record is not null)
    {
        <pre>Id : @_record.Id </pre>
        <pre>Name : @_record.Date </pre>
        <pre>Temp C : @_record.TemperatureC </pre>
        <pre>Temp F : @_record.TemperatureF </pre>
        <pre>Summary : @_record.Summary </pre>
    }
    else
    {
        <pre>No Record Loaded</pre>
    }
</div>

<div class="m-3 text-end">
    <div class="btn-group">
        @foreach (var forecast in _forecasts)
        {
            <a class="btn @this.SelectedCss(forecast.Id)" 
             href="@($"/WeatherForecastWithStatus/{forecast.Id}")">@forecast.Id</a>
        }
    </div>
</div>
@code {
    [Parameter] public int Id { get; set; }

    private WeatherForecast? _record;
    private IEnumerable<WeatherForecast> _forecasts = 
                        Enumerable.Empty<WeatherForecast>();
    private string? _message;
    private bool _dismissible;
    private Alert.AlertType _alertType = Alert.AlertType.Info;

    private int _id;

    private string SelectedCss(int value)
        => _id == value ? <span class="pl-s">"btn-primary" : 
                           "btn-outline-primary"</span>;

    protected override async Task OnParametersSetAsync()
    {
        _dismissible = false;

        if (NotInitialized)
        {
            _message = "Initializing";
            _alertType = Alert.AlertType.Warning;
            await this.RenderAsync();
            _forecasts = await service.GetForecastsAsync();
        }

        var hasIdChanged = this.Id != _id;

        _id = this.Id;

        if (hasIdChanged)
        {
            _message = "Loading";
            _alertType = Alert.AlertType.Info;
            await this.RenderAsync();
            _record = await service.GetForecastAsync(this.Id);
        }

        _message = "Loaded";
        _alertType = Alert.AlertType.Success;
        _dismissible = true;
        await this.RenderAsync();
    }
}

总结

本文演示了如何在 ComponentBase 之外编写 Blazor 应用程序。您不会失去任何东西,只会获得一些重要的额外功能,并能更好地控制渲染过程。

大胆尝试。开始使用我的组件套件。将 BlazrControlBase 作为您的主要基类组件。

我包含了 BlazrComponentBase,但必须承认我从未用过它。我只在使用继承自它的组件时使用 ComponentBase,例如 InputBase 编辑控件。

我将引用 ComponentBase 源代码顶部的一条评论作为结束

// Most of the developer-facing component lifecycle concepts are encapsulated in this
// base class. The core components rendering system doesn't know about them 
// (it only knows about IComponent). 
// This gives us flexibility to change the lifecycle concepts easily,
// or for developers to design their own lifecycles as different base classes. 

附录

类图

BlazrComponentBase

BlazrComponentBase 的完整类代码 如下

public class BlazrComponentBase : BlazrBaseComponent, 
             IComponent, IHandleEvent, IHandleAfterRender
{
    private bool _hasCalledOnAfterRender;

    public virtual async Task SetParametersAsync(ParameterView parameters)
    {
        parameters.SetParameterProperties(this);
        await this.ParametersSetAsync();
    }

    protected async Task ParametersSetAsync()
    {
        Task? initTask = null;
        var hasRenderedOnYield = false;

        // If this is the initial call then we need to run the OnInitialized methods
        if (this.NotInitialized)
        {
            this.OnInitialized();
            initTask = this.OnInitializedAsync();
            hasRenderedOnYield = await this.CheckIfShouldRunStateHasChanged(initTask);
            Initialized = true;
        }

        this.OnParametersSet();
        var task = this.OnParametersSetAsync();

        // check if we need to do the render on Yield i.e.
        //  - this is not the initial run or
        //  - OnInitializedAsync did not yield
        var shouldRenderOnYield = initTask is null || !hasRenderedOnYield;

        if (shouldRenderOnYield)
            await this.CheckIfShouldRunStateHasChanged(task);
        else
            await task;

        // run the final state has changed to update the UI.
        this.StateHasChanged();
    }

    protected virtual void OnInitialized() { }

    protected virtual Task OnInitializedAsync() => Task.CompletedTask;

    protected virtual void OnParametersSet() { }

    protected virtual Task OnParametersSetAsync() => Task.CompletedTask;

    protected virtual void OnAfterRender(bool firstRender) { }

    protected virtual Task OnAfterRenderAsync(bool firstRender) => Task.CompletedTask;

    async Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem item, object? obj)
    {
        var uiTask = item.InvokeAsync(obj);

        await this.CheckIfShouldRunStateHasChanged(uiTask);

        this.StateHasChanged();
    }

    Task IHandleAfterRender.OnAfterRenderAsync()
    {
        var firstRender = !_hasCalledOnAfterRender;
        _hasCalledOnAfterRender = true;

        OnAfterRender(firstRender);

        return OnAfterRenderAsync(firstRender);
    }

    protected async Task<bool> CheckIfShouldRunStateHasChanged(Task task)
    {
        var isCompleted = task.IsCompleted || task.IsCanceled;

        if (!isCompleted)
        {
            this.StateHasChanged();
            await task;
            return true;
        }

        return false;
    }
}

CSSBuilder

/// ============================================================
/// Modification Author: Shaun Curtis, Cold Elm Coders
/// License: Use And Donate
/// If you use it, donate something to a charity somewhere
/// 
/// Original code based on CSSBuilder by Ed Charbeneau
/// and other implementations
/// 
/// https://github.com/EdCharbeneau/BlazorComponentUtilities/blob/
/// master/BlazorComponentUtilities/CssBuilder.cs
/// ============================================================
namespace Blazr.Components;

public sealed class CSSBuilder
{
    private Queue<string> _cssQueue = new Queue<string>();

    public static CSSBuilder Class(string? cssFragment = null)
        => new CSSBuilder(cssFragment);

    public CSSBuilder() { }

    public CSSBuilder(string? cssFragment)
        => AddClass(cssFragment ?? String.Empty);

    public CSSBuilder AddClass(string? cssFragment)
    {
        if (!string.IsNullOrWhiteSpace(cssFragment))
            _cssQueue.Enqueue(cssFragment);
        return this;
    }

    public CSSBuilder AddClass(IEnumerable<string> cssFragments)
    {
        cssFragments.ToList().ForEach(item => _cssQueue.Enqueue(item));
        return this;
    }

    public CSSBuilder AddClass(bool WhenTrue, string cssFragment)
        => WhenTrue ? this.AddClass(cssFragment) : this;

    public CSSBuilder AddClass(bool WhenTrue, 
           string? trueCssFragment, string? falseCssFragment)
        => WhenTrue ? this.AddClass(trueCssFragment) : this.AddClass(falseCssFragment);

    public CSSBuilder AddClassFromAttributes
    (IReadOnlyDictionary<string, object> additionalAttributes)
    {
        if (additionalAttributes != null 
        && additionalAttributes.TryGetValue("class", out var val))
            _cssQueue.Enqueue(val.ToString() ?? string.Empty);
        return this;
    }

    public CSSBuilder AddClassFromAttributes
           (IDictionary<string, object> additionalAttributes)
    {
        if (additionalAttributes != null 
        && additionalAttributes.TryGetValue("class", out var val))
            _cssQueue.Enqueue(val.ToString() ?? string.Empty);
        return this;
    }

    public string Build(string? CssFragment = null)
    {
        if (!string.IsNullOrWhiteSpace(CssFragment)) _cssQueue.Enqueue(CssFragment);
        if (_cssQueue.Count == 0)
            return string.Empty;
        var sb = new StringBuilder();
        foreach (var str in _cssQueue)
        {
            if (!string.IsNullOrWhiteSpace(str)) sb.Append($" {str}");
        }
        return sb.ToString().Trim();
    }
}

历史

  • 2023 年 7 月 14 日:初始版本
© . All rights reserved.