生成Blazor基础组件

目录

介绍

存储库

三个组成部分

BlazrBaseComponent

BlazrUIBase

BlazrUIBase演示

AlertPage继承BlazrUIBase

BlazrControlBase

BlazrControlBase演示

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

WeatherForecastViewer

BlazrComponentBase

BaseComponent新增功能

Wrapper/Frame功能

RenderAsync

手动实现OnAfterRender

将它结合在一起

总结

附录

类图

BlazrComponentBase

CSSBuilder


介绍

本文介绍如何为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的黑盒替代品,带有一些添加的特性。

FetchDataCounter或您使用的任何其他组件的继承更改为BlazrControlBase,您可能不会看到任何差异。如果这样做,请更新BlazrComponentBase

存储库

本文的存储库是 Blazr.BaseComponents

三个组成部分

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

BlazrBaseComponent

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

它是一个标准类,用于实现所有组件使用的样板代码。它是抽象的,没有实现IComponent。继承类实现IComponent,可以设置SetParametersAsyncvirtual,也可以修复它。

它复制了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. lambda方法分配Frame_content如果它不是null,否则它分配给Body
  4. lambda方法设置Initializedtrue当期完成时。

稍后将详细介绍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

  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. Alert Dismiss方法调用MessageChanged传递一个null string
  2. UI处理程序调用Index中的Bind处理程序。
  3. 绑定处理程序[由Razor编译器创建]更新_messagenull
  4. UI 处理程序完成并调用StateHasChanged
  5. Index呈现。
  6. 渲染器检测到Alert上的Message参数已更改。它在Alert上调用SetParametersAsync,传入修改后的ParameterView
  7. Alert呈现:因为Messagenull,所以它隐藏了警报。

重要的教训是:总是测试你是否真的需要调用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是中级组件。这是我的主力军。

它:

  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提供对的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

  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演示组件。

请注意,包装器在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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值