Blazor UI 事件和渲染






4.67/5 (6投票s)
揭秘 Blazor UI 事件和渲染
引言
对于 Blazor 新手程序员来说,最常见的问题之一就是 UI 事件和相关的渲染过程。这类问题每天都会出现在 StackOverflow 等网站上。希望本文能为您澄清一些疑虑!
代码
本文没有代码存储库。在本文的附录中有一个单页演示 Razor 文件,您可以使用它进行测试。
RenderFragment
什么是 RenderFragment?
对许多人来说,它看起来是这样的
<div>
 Hello World
</div>
Razor 中的一个标记块 - 一个 string。
深入 DotNetCore 代码库,您会发现
public delegate void RenderFragment(RenderTreeBuilder builder);
如果您不完全理解委托,可以将其视为一种模式。任何符合该模式的函数都可以作为 RenderFragment 传递。
该模式规定您的方法必须
- 具有一个且仅一个类型为 RenderTreeBuilder的参数。
- 返回 void。
让我们看一个例子
protected void BuildHelloWorld(RenderTreeBuilder builder)
{
    builder.OpenElement(0, "div");
    builder.AddContent(1, "Hello World");
    builder.CloseElement();
}
我们可以将其重写为一个属性
protected RenderFragment HelloWorldFragment => (RenderTreeBuilder builder) =>
{
    builder.OpenElement(0, "div");
    builder.AddContent(1, "Hello World");
    builder.CloseElement();
};
或
protected RenderFragment HelloWorldFragment => (builder) =>
    {
        builder.OpenElement(0, "div");
        builder.AddContent(1, "Hello World");
        builder.CloseElement();
    };
当 Razor 文件被编译时,它会被 Razor 编译器转换为一个 C# 类文件。
组件 ADiv.razor
<div>
 Hello World
</div>
被编译为
namespace Blazr.UIDemo.Pages
{
    public partial class ADiv : Microsoft.AspNetCore.Components.ComponentBase
    {
        protected override void BuildRenderTree
        (Microsoft.AspNetCore.Components.Rendering.RenderTreeBuilder __builder)
        {
            __builder.AddMarkupContent(0, "<div>\r\n Hello World\r\n</div>");
        }
    }
}
组件渲染
Razor 页面/组件的基类是 ComponentBase。这个类有一个 public 方法 StateHasChanged 用于渲染组件。
一个常见的代码片段问题
void ButtonClick()
{
    // set some message saying processing
    StateHasChanged();
    // Do some work
    // set some message saying complete
    StateHasChanged();
}
唯一显示的消息是 complete。为什么?在调用“Do Some Work”之前,第一次 StateHasChanged 没有重新渲染组件吗?
是的,StateHasChanged 确实运行了。但是,要理解这个问题,我们需要仔细看看 StateHasChanged 和组件渲染片段的简化版本。
protected void StateHasChanged()
{
    if (_hasPendingQueuedRender)
        return;
    else
    {
        _hasPendingQueuedRender = true;
        _renderHandle.Render(_renderFragment);
    }
}
_renderFragment = builder =>
    {
        _hasPendingQueuedRender = false;
        BuildRenderTree(builder);
    };
首先,它检查是否已经有一个渲染在队列中 - _hasPendingQueuedRender 为 false。如果没有,它将 _hasPendingQueuedRender 设置为 true,并调用 _renderHandle.Render,将 _renderFragment(组件的渲染片段)传递给它。就是这样。
当渲染片段实际运行时,_hasPendingQueuedRender 会被设置为 false。对于好奇的读者来说,当组件附加到 RenderTree 时(Renderer 调用 Attach),_renderHandle 会被传递给组件。
理解的关键在于,StateHasChanged 将组件渲染片段 _renderFragment 作为 delegate 放入 Renderer 的渲染队列中。它不会执行渲染片段。那是 Renderer 的工作。
如果我们回到按钮点击事件,所有顺序同步代码都在 UI 线程上运行。在 ButtonClick 完成之前,renderer 不会运行 - 因此也不会处理其渲染队列。没有发生让步。
Blazor UI 事件
让我们看另一个常见问题来理解 UI 事件处理过程
async void ButtonClick()
{
    // set some message saying processing
    // Call Task.Wait to simulate some yielding async work
    await Task.Wait(1000);
    // set some message saying complete
}
为什么我们只看到第一条消息?在代码末尾添加一个 StateHasChanged,它就可以正常工作了。
async void ButtonClick()
{
    // set some message saying processing
    // Call Task.Wait to simulate some yielding async work
    await Task.Wait(1000);
    // set some message saying complete
    StateHasChanged();
}
您可能已经解决了显示问题,但尚未解决根本问题。
Blazor UI 事件模式
Blazor UI 事件 **不是** 即发即弃的。使用的基本模式是
var task = InvokeAsync(EventMethod);
StateHasChanged();
if (!task.IsCompleted)
{
    await task;
    StateHasChanged();
}
我们的按钮事件获得了一个 Task 包装器 task。它要么运行到让步事件,要么运行完成。此时,将调用 StateHasChanged,并排队和执行一个渲染事件。如果 task 未完成,处理程序将等待该任务,并在完成后调用 StateHasChanged。
ButtonClick 中的问题在于它让步了,但由于将事件处理程序传递了一个 void,事件处理程序没有什么可以等待的。在让步代码完成运行之前,它就运行完成了。没有第二个渲染事件。
解决方案是让 ButtonClick 返回一个 Task
async Task ButtonClick()
{
    // set some message saying processing
    // Call Task.Wait to simulate some yielding async work
    await Task.Wait(1000);
    // set some message saying complete
    StateHasChanged();
}
现在事件处理程序 task 有东西可以等待了。
几乎所有的 UI 事件都使用了相同的模式。您也可以在 OnInitializedAsync 和 OnParametersSetAsync 中看到它的使用。
那么最佳实践是什么?何时在事件处理程序中使用 void 和 Task?
总的来说,不要将 async 关键字与 void 混合使用。如果有疑问,请使用 Task。
总结
从本文中获得的关键信息是
- RenderFragment是一个- delegate- 它是一个使用- RenderTreeBuilder构建 HTML 标记的代码块。
- StateHasChanged不会渲染组件或执行- RenderFragment。它将- RenderFragment推送到- Renderer的队列中。
- UI 事件处理程序需要让步,以便 Renderer线程有时间运行其渲染队列。
- UI 事件处理程序不是即发即弃的。
- 不要像这样声明一个事件处理程序 async void UiEvent()。如果它是async,那么它应该是async Task UiEvent()。
附录
演示页面
这是一个独立的页面,演示了上述的一些问题和解决方案。长时间运行的任务是真实的数字计算方法(寻找素数),用于演示真实的同步和异步长时间运行操作。异步版本在找到每个素数时调用 Task.Yield 来让出执行控制。您可以使用此页面测试各种场景。
@page "/"
@using System.Diagnostics;
@using Microsoft.AspNetCore.Components.Rendering;
<h1>UI Demo</h1>
@MyDiv
@MyOtherDiv
<div class="container">
    <div class="row">
        <div class="col-4">
            <span class="col-form-label">Primes to Calculate: </span>
            <input class="form-control" @bind-value="this.primesToCalculate" />
        </div>
        <div class="col-8">
            <button class="btn @buttoncolour" @onclick="Clicked1">Click Event</button>
            <button class="btn @buttoncolour" @onclick="Clicked2">
             Click Async Void Event</button>
            <button class="btn @buttoncolour ms-2" @onclick="ClickedAsync">
             Click Async Task Event</button>
            <button class="btn @buttoncolour" @onclick="Reset">Reset</button>
        </div>
    </div>
</div>
@code{
    bool workingstate;
    string buttoncolour => workingstate ? "btn-danger" : "btn-success";
    string MyDivColour => workingstate ? "bg-warning" : "bg-primary";
    string myOtherDivColour => workingstate ? "bg-danger" : "bg-dark";
    long tasklength = 0;
    long primesToCalculate = 10;
    string message = "Waiting for some action!";
    private async Task Reset()
    {
        message = "Waiting for some action!";
        workingstate = false;
    }
    private async Task ClickedAsync()
    {
        workingstate = true;
        message = "Processing";
        await LongYieldingTaskAsync();
        message = $"Complete : {DateTime.Now.ToLongTimeString()}";
        workingstate = false;
    }
    private void Clicked1()
    {
        workingstate = true;
        message = "Processing";
        LongTaskAsync();
        message = $"Complete : {DateTime.Now.ToLongTimeString()}";
        workingstate = false;
    }
    private async void Clicked2()
    {
        workingstate = true;
        message = "Processing";
        await Task.Yield();
        await LongTaskAsync();
        message = $"Complete : {DateTime.Now.ToLongTimeString()}";
        workingstate = false;
    }
    private RenderFragment MyDiv => (RenderTreeBuilder builder) =>
    {
        builder.AddMarkupContent(0, $"<div class='text-white {MyDivColour} m-2 p-2'>
        {message}</div>");
    };
    private RenderFragment MyOtherDiv => (builder) =>
    {
        builder.OpenElement(0, "div");
        builder.AddAttribute(1, "class", $"text-white {myOtherDivColour} m-2 p-2");
        builder.AddMarkupContent(0, message);
        builder.CloseElement();
    };
    public Task LongTaskAsync()
    {
        var watch = new Stopwatch();
        var num = primesToCalculate * 1;
        watch.Start();
        var counter = 0;
        for (long x = 0; x <= num; x++)
        {
            for (long i = 0; i <= (10000); i++)
            {
                bool isPrime = true;
                for (long j = 2; j < i; j++)
                {
                    if (i % j == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if (isPrime)
                {
                    counter++;
                }
            }
        }
        watch.Stop();
        tasklength = watch.ElapsedMilliseconds;
        return Task.CompletedTask;
    }
    public async Task LongYieldingTaskAsync()
    {
        var watch = new Stopwatch();
        var num = primesToCalculate * 1;
        watch.Start();
        var counter = 0;
        for (long x = 0; x <= num; x++)
        {
            for (long i = 0; i <= (10000); i++)
            {
                bool isPrime = true;
                for (long j = 2; j < i; j++)
                {
                    if (i % j == 0)
                    {
                        isPrime = false;
                        break;
                    }
                }
                if (isPrime)
                {
                    counter++;
                    await Task.Yield();
                }
            }
        }
        watch.Stop();
        tasklength = watch.ElapsedMilliseconds;
    }
}
历史
- 2021年8月17日:初始版本

