目录
介绍
本文介绍如何为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是具有一些附加Wrapper/Frame功能的ComponentBase完整替代品。
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。
- lambda方法分配Frame给_content如果它不是null,否则它分配给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。他们可以通过将SetParametersAsync设置为virtual或关闭来选择将其打开。
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处理程序。
- 绑定处理程序[由Razor编译器创建]更新_message为null。
- UI 处理程序完成并调用StateHasChanged。
- Index呈现。
- 渲染器检测到Alert上的Message参数已更改。它在Alert上调用SetParametersAsync,传入修改后的ParameterView。
- Alert呈现:因为Message是null,所以它隐藏了警报。
重要的教训是:总是测试你是否真的需要调用StateHasChanged。
AlertPage继承BlazrUIBase
我们可以将AlertPage上的继承降级为BlazrUIBase来进行渲染实验。
执行此操作后,将没有任何更新。不会显示警报,因为当UI事件发生时,没有发生任何StateHasChanged()调用[并且没有UI呈现更新]。
我们可以通过将调用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是中级组件。这是我的主力军。
它:
- 实现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的新版本,并演示了如何将ComponentBase页面替换为基于BlazrControlBase的页面。
修改后的天气预报数据管道
首先,修改后的天气预报数据类和服务。
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演示组件。
请注意,包装器在Frame呈现片段[而不是主要内容部分]中定义,并使用Razor内置__builderRenderTreeBuilder实例。
@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>
};
}
并且Index继承自Wrapper。
@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,但我必须承认从未使用过它。我只在使用从它继承的组件(如InputBase编辑控件)时使用ComponentBase。
我将在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();
}
}
https://www.codeproject.com/Articles/5364401/Building-Blazor-Base-Components