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

Blazor UI 事件和渲染

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.67/5 (6投票s)

2021年8月17日

CPOL

4分钟阅读

viewsIcon

12973

揭秘 Blazor UI 事件和渲染

引言

对于 Blazor 新手程序员来说,最常见的问题之一就是 UI 事件和相关的渲染过程。这类问题每天都会出现在 StackOverflow 等网站上。希望本文能为您澄清一些疑虑!

代码

本文没有代码存储库。在本文的附录中有一个单页演示 Razor 文件,您可以使用它进行测试。

RenderFragment

什么是 RenderFragment

对许多人来说,它看起来是这样的

<div>
 Hello World
</div>

Razor 中的一个标记块 - 一个 string

深入 DotNetCore 代码库,您会发现

public delegate void RenderFragment(RenderTreeBuilder builder);

如果您不完全理解委托,可以将其视为一种模式。任何符合该模式的函数都可以作为 RenderFragment 传递。

该模式规定您的方法必须

  1. 具有一个且仅一个类型为 RenderTreeBuilder 的参数。
  2. 返回 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);
    };

首先,它检查是否已经有一个渲染在队列中 - _hasPendingQueuedRenderfalse。如果没有,它将 _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 事件都使用了相同的模式。您也可以在 OnInitializedAsyncOnParametersSetAsync 中看到它的使用。

那么最佳实践是什么?何时在事件处理程序中使用 voidTask

总的来说,不要将 async 关键字与 void 混合使用。如果有疑问,请使用 Task

总结

从本文中获得的关键信息是

  1. RenderFragment 是一个 delegate - 它是一个使用 RenderTreeBuilder 构建 HTML 标记的代码块。
  2. StateHasChanged 不会渲染组件或执行 RenderFragment。它将 RenderFragment 推送到 Renderer 的队列中。
  3. UI 事件处理程序需要让步,以便 Renderer 线程有时间运行其渲染队列。
  4. UI 事件处理程序不是即发即弃的。
  5. 不要像这样声明一个事件处理程序 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日:初始版本
© . All rights reserved.