构建 Blazor 基础组件





5.00/5 (8投票s)
如何为 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。
三个组件
BlazrUIBase
是一个功能最少的简单 UI 组件。BlazrControlBase
是一个中级控件组件,具有单个生命周期方法和单个渲染模型。BlazrComponentBase
是一个完整的 `ComponentBase` 替代品,具有一些额外的Wrapper
/Frame
功能。
BlazrBaseComponent
所有组件都继承自 `BlazrBaseComponent`。它是基类组件的基础类!
这是一个实现所有组件使用的样板代码的标准类。它是抽象的,不实现 `IComponent`。继承类实现 `IComponent`,并且可以将 `SetParametersAsync` 设置为 virtual
,或者固定它。
它复制了 `ComponentBase` 的大部分变量和属性,以保持熟悉感。
区别在于
Initialized
标志已更改。它被反转了,现在是protected
,因此继承类可以访问它。它有一个NotInitialized
对应项:无需使用笨拙的if(!Initialized)
条件代码。- 它有一个
Guid
标识符:在调试中跟踪实例很有用,并且在一些我更高级的组件中使用。 - 它有两个
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();
构造函数实现了包装器功能
- 它将渲染代码
BuildRenderTree
分配给Body
。 - 它设置分配给
_content
的 lambda 方法:StateHasChanged
传递给Renderer
的渲染片段。 - 如果
Frame
不为null
,则 lambda 方法将Frame
分配给_content
,否则分配Body
。 - 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`。
- 它有一个固定的 `SetParametersAsync`:无法覆盖。
- 它没有生命周期方法。简单组件不需要它们。
- 它不实现 `IHandleEvent`,即它没有 UI 事件处理。如果您需要任何,请手动调用 `StateHasChanged`。
- 它不实现 `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
调用。
Alert
的Dismiss
方法调用MessageChanged
并传递一个null
string
。- UI 处理程序调用
Index
中的Bind
处理程序。 - Bind 处理程序 [由 Razor 编译器创建] 将
_message
更新为null
。 - UI 处理程序完成并调用
StateHasChanged
。 Index
渲染。Renderer
检测到Alert
上的Message
参数已更改。它调用Alert
上的SetParametersAsync
,传入修改后的ParameterView
。Alert
渲染:Message
为null
,因此它隐藏了警告框。
重要的教训是:始终测试您是否确实需要调用 StateHasChanged
。
继承 BlazrUIBase 的 AlertPage
我们可以将 AlertPage
上的继承降级为 BlazrUIBase
来试验渲染。
这样做后,什么都不会更新。没有警告框出现,因为在 UI 事件发生时 [并且没有 UI 渲染更新],没有发生 StateHasChanged()
调用。
我们可以通过在需要的地方添加 StateHasChanged
调用来修复此问题。
绑定将不再按宣传的那样工作,因为不再有注册的 UI 处理程序。渲染器直接调用绑定处理程序。没有内置的 StateHasChanged
调用。
要解决此问题,我们需要手动连接绑定。
- 添加一个处理程序来分配给
MessageChanged
回调。在设置_message1
后,它会调用StateHasChanged
。我们已复制了原始过程。private Task OnUpdateMessage(string? value) { _message1 = value; this.StateHasChanged(); return Task.CompletedTask; }
- 更改
Alert
组件上的绑定。<Alert @bind-Message:get=_message1 @bind-Message:set= this.OnUpdateMessage MessageType=Alert.AlertType.Success />
- 更新
SetMessageAsync
以调用StateHasChanged
。private Task SetMessageAsync(string? message) { _message1 = message; _message2 = message; this.StateHasChanged(); return Task.CompletedTask; }
现在一切正常,并且我们通过仅在需要时驱动渲染事件来提高效率。
BlazrControlBase
BlazrControlBase
是中间级别的组件。它是我的主力。
它
- 实现了
OnParametersSetAsync
生命周期方法。 - 实现单个渲染 UI 事件处理程序。
- 锁定了 `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
提供对 Initialized
和 NotInitialized
的访问。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
。
NotInitialized
提供条件控制:仅在初始化时加载WeatherForecast
列表。在ComponentBase
中,此代码将在OnInitializedAsync
中。hasIdChanged
检测Id
是否已更改。它单独声明以使代码更清晰、更具表现力。编译器将对此进行优化。- 仅在
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>
};
}
以及继承自 Wrapper
的 Index
。
@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 日:初始版本