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

调试 Blazor 组件

starIconstarIconstarIconstarIconstarIcon

5.00/5 (1投票)

2023 年 9 月 5 日

CPOL

6分钟阅读

viewsIcon

12654

如何调试 Blazor 组件

概述

我将从一段简短的编码旅程开始本文:一位 Blazor 新手构建一个简单的数据页面。这展示了调试的困境,并为本文的其余部分提供了组件代码。

本文的其余部分将介绍如何在组件内记录事件序列,并引入 DocumentatedComponentBase 组件来实现自动日志记录。

最后的总结提供了一些关键进程的背景信息。

存储库和包

本文的代码是 Blazor.BaseComponent 库的一部分。

DocumentatedComponentBase 组件可在 Blazr.BaseComponents Nuget 包中找到。

我的第一个 Blazor 页面

我想进行数据库调用以获取一些数据。我认为这会花费一些时间,所以我想在发生时显示“正在加载”。我**保持简单**:避开**异步**的晦涩难懂之处。

我编写的代码如下。这一切都是同步的,带有阻塞的 Thread.Sleep 来模拟缓慢的数据存储调用。

我的期望是,当我设置 _state = "Loading" 时,组件将[以某种方式]注册该状态更改并立即重新渲染。

@page "/"

<PageTitle>The OnAfterRender Myth</PageTitle>

<h1>The OnAfterRender Myth</h1>

<div class="bg-dark text-white mt-5 m-2 p-2">
    <pre>@_state</pre>
</div>

@code {
    private string? _state = "New";

    protected override void OnInitialized()
    {
        _state = "Loading";
        TaskSync();
        _state = "Loaded";
    }

    // Emulate a synchronous blocking database operation
    private void TaskSync()
        => Thread.Sleep(1000);
}

我看到的是一个空白屏幕,然后是“已加载”:没有中间的“正在加载”。

我开始搜索。

StateHasChanged

我了解到 StateHasChanged 并更新了我的代码。

我现在期望在设置 _state 后组件立即渲染。

protected override void OnInitialized()
{
    _state = "Loading";
    StateHasChanged();
    TaskSync();
    _state = "Loaded";
}

但徒劳无功。这是怎么回事?“也许我发现了一个 MS Component 代码中的 bug”。

我进行了更多的搜索。

Task.Delay

我找到了 await Task.Delay(1)。看起来是异步的,但让我们在我的代码中尝试一下。我开始输入 await,Visual Studio 编辑器会自动将 async 添加到我的方法中

protected override async void OnInitialized()

我完成了更改。它可以编译,所以可能没问题。

我期望它能工作,但并不清楚为什么。

protected override async void OnInitialized()
{
    _state = "Loading";
    StateHasChanged();
    await Task.Delay(1);
    TaskSync();
    _state = "Loaded";
}

我得到了相反的结果。“正在加载”,但没有完成到“已加载”。

现在感到困惑和沮丧,我继续搜索。

OnAfterRender

我找到了一些关于 OnAfterRender 的内容。我将其添加到我的代码中。

protected override void OnAfterRender(bool firstRender)
{
    if (firstRender)
        StateHasChanged();
}

我希望它能起作用,并且[松了口气]它确实起作用了。我不知道为什么[也许我自欺欺人地说我知道]。它起作用了,所以**问题解决了**。

我学到了一种编写此类场景的新模式。我在其他地方也使用了它。

我未能学到的东西

问题的真正解决方案对更有经验的程序员来说很明显。你不能混淆**同步**和**异步**世界。在大多数情况下,async void 是一种致命的混合体。请使用 OnInitializedAsync 和**异步**数据库操作。

我。我才刚刚开始涉足 Blazor 和 SPA 的道路。我的 async void 翻车事故仍然在几天或几周之后。在此期间,我学到了一个“脏”的反模式,它“有效”。我甚至可能分享它!

如何调试组件

Debug.WriteLine/Console.WriteLine

要有效调试组件,您需要实时输出信息。Debug.WriteLineConsole.WriteLine 是您的生命线。

我称之为**记录**,而不是**调试**。您没有使用断点,只是记录正在发生的事情,以后再进行分析。

以上面的代码为例,添加一些如下所示的日志记录

@page "/AsyncOnInitialized"
@using System.Diagnostics;

<PageTitle>Documented Async OnInitialized</PageTitle>

@{
    this.Log($"Render Component.");
}

<h1>The OnAfterRender Myth</h1>

<div class="bg-dark text-white mt-5 m-2 p-2">
    <pre>@_state</pre>
</div>

@code {
    private string? _state = "New";

    private string _id = Guid.NewGuid().ToString().Substring(0, 4);
    private string _type => this.GetType().Name;

    public async override Task SetParametersAsync(ParameterView parameters)
    {
        this.Log($"SetParametersAsync started.");
        await base.SetParametersAsync(parameters);
        this.Log($"SetParametersAsync completed.");
    }

    protected override async void OnInitialized()
    {
        this.Log($"OnInitialized Started.");
        _state = "Loading";
        StateHasChanged();
        await Task.Delay(1);
        TaskSync();
        this.Log($"OnInitialized Continuation.");
        _state = "Loaded";
        this.Log($"OnInitialized Completed.");
    }

    protected override Task OnInitializedAsync()
    {
        this.Log($"OnInitializedAsync.");
        return Task.CompletedTask;
    }

    protected override void OnParametersSet()
        => this.Log($"OnParametersSet.");

    protected override Task OnParametersSetAsync()
    {
        this.Log($"OnParametersSetAsync.");
        return Task.CompletedTask;
    }

    protected override bool ShouldRender()
    {
        this.Log($"ShouldRender.");
        return true;
    }

    private void TaskSync()
        => Thread.Sleep(1000);

    private async Task TaskAsync()
        => await Task.Yield();

    protected override void OnAfterRender(bool firstRender)
    {
        if (firstRender)
        {
            this.Log($"First OnAfterRender.");
            StateHasChanged();
        }
        else
            this.Log($"Subsequent OnAfterRender.");
    }

    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            this.Log($"First OnAfterRenderAsync.");

        else
            this.Log($"Subsequent OnAfterRenderAsync.");

        return Task.CompletedTask;
    }

    private void Log(string message)
    {
        message = $"{_id} - {_type} => {message}";
        Debug.WriteLine(message);
        Console.WriteLine(message);
    }
}

运行此代码,我们现在可以看到事件序列。

30af - AsyncOnInitialized => SetParametersAsync started.
30af - AsyncOnInitialized => OnInitialized Started.
[3] => 30af - AsyncOnInitialized => OnInitializedAsync.
30af - AsyncOnInitialized => OnParametersSet.
30af - AsyncOnInitialized => OnParametersSetAsync.
30af - AsyncOnInitialized => SetParametersAsync completed.
30af - AsyncOnInitialized => Render Component.
[8] => 30af - AsyncOnInitialized => OnInitialized Continuation.
30af - AsyncOnInitialized => OnInitialized Completed.
[10] => 30af - AsyncOnInitialized => First OnAfterRender.
30af - AsyncOnInitialized => ShouldRender.
30af - AsyncOnInitialized => Render Component.
30af - AsyncOnInitialized => First OnAfterRenderAsync.
30af - AsyncOnInitialized => Subsequent OnAfterRender.
30af - AsyncOnInitialized => Subsequent OnAfterRenderAsync.

在第 3 行,事情开始出错。OnInitializedAsync 和其余的生命周期进程运行完毕[包括最终渲染],然后在第 8 行,OnInitialized 的续延运行,OnInitialized 完成。由于 SetParametersAsync 没有返回 Task 来 awaitOnInitialized 已与生命周期分离。

在第 10 行,运行 OnAfterRender 并调用 StateHasChanged,后者渲染组件,并触发第二个 OnAfterRender 周期。

已记录的 ComponentBase

在示例中,我添加了大量的手动日志记录代码。经常这样做既耗时又乏味。虽然大多数信息都可以记录下来,但它有点笨拙,因为无法访问内部的 ComponentBase 进程。

这就是 DocumentedComponentBase 的作用。它是 ComponentBase 的一个黑盒版本,提供了对内部进程的完整日志记录。

您可以从本文的存储库中复制代码,或者安装 Blazr.BaseComponents Nuget 包,并使用 Blazr.BaseComponents.ComponentBase 命名空间。

记录 AsyncOnInitialized

重构上面的初始同步代码很简单。将继承更改为 DocumentedComponentBase,并在设置 _state 的地方添加一个 Log 行。LogDocumentedComponentBase 提供的 protected 方法。

@page "/AsyncOnInitializedDocumented"
@inherits DocumentedComponentBase

<PageTitle>Documented Async OnInitialized</PageTitle>

<h1>Documented Async OnInitialized</h1>

<div class="bg-dark text-white mt-5 m-2 p-2">
    <pre>@_state</pre>
</div>

@code {
    private string? _state = "New";

    protected override void OnInitialized()
    {
        this.Log($"OnInitialized - State set to Loading.");
        _state = "Loading";
        TaskSync();
        this.Log($"OnInitialized - State set to Loaded.");
        _state = "Loaded";
    }

    private void TaskSync()
        => Thread.Sleep(1000);
}

这是输出。

我将输出复制粘贴到一个文本文件中,然后进行注释。
===========================================
2c5b - AsyncOnInitializedDocumented => Component Initialized
2c5b - AsyncOnInitializedDocumented => Component Attached
2c5b - AsyncOnInitializedDocumented => SetParametersAsync Started
2c5b - AsyncOnInitializedDocumented => OnInitialized sequence Started
[5] => 2c5b - AsyncOnInitializedDocumented => OnInitialized - State set to Loading.
[6] => 2c5b - AsyncOnInitializedDocumented => OnInitialized - State set to Loaded.
2c5b - AsyncOnInitializedDocumented => OnInitialized sequence Completed
2c5b - AsyncOnInitializedDocumented => OnParametersSet Sequence Started
[9] => 2c5b - AsyncOnInitializedDocumented => StateHasChanged Called
2c5b - AsyncOnInitializedDocumented => Render Queued
2c5b - AsyncOnInitializedDocumented => OnParametersSet Sequence Completed
2c5b - AsyncOnInitializedDocumented => SetParametersAsync Completed
[13] => 2c5b - AsyncOnInitializedDocumented => Component Rendered
2c5b - AsyncOnInitializedDocumented => OnAfterRenderAsync Started
2c5b - AsyncOnInitializedDocumented => OnAfterRenderAsync Completed

状态在第 5 行和第 6 行设置和重置,然后调用第 9 行的 StateHasChanged,并在第 13 行发生渲染。你可以清楚地看到,当组件在第 13 行实际渲染时,_stateLoaded。在第 5 行和第 6 行之间没有魔法渲染。

记录异步解决方案

现在转到完全**异步**版本

@page "/AsyncOnInitializedAsyncDocumented"
@inherits DocumentedComponentBase

<PageTitle>Documented Async OnInitializedAsync</PageTitle>

<h1>Documented Async OnInitializedAsync</h1>

<div class="bg-dark text-white mt-5 m-2 p-2">
    <pre>@_state</pre>
</div>

@code {
    private string? _state = "New";

    protected override async Task OnInitializedAsync()
    {
        this.Log($"OnInitialized - State set to Loading.");
        _state = "Loading";
        await TaskAsync();
        this.Log($"OnInitialized - State set to Loaded.");
        _state = "Loaded";
    }

    private async Task TaskAsync()
        => await Task.Delay(1000);
}

你会得到这个

===========================================
cf89 - AsyncOnInitializedAsyncDocumented => Component Initialized
cf89 - AsyncOnInitializedAsyncDocumented => Component Attached
cf89 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Started
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Started
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loading.
[6] => cf89 - AsyncOnInitializedAsyncDocumented => Awaiting Task completion
cf89 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
cf89 - AsyncOnInitializedAsyncDocumented => Render Queued
cf89 - AsyncOnInitializedAsyncDocumented => Component Rendered
[10] => cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
[11] => cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loaded.
cf89 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Completed
cf89 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Started
cf89 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
cf89 - AsyncOnInitializedAsyncDocumented => Render Queued
cf89 - AsyncOnInitializedAsyncDocumented => Component Rendered
cf89 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Completed
cf89 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Completed
cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
cf89 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed

注意

  1. 在第 5 行,await 产生了一个暂停,在第 5 行和第 11 行之间有一个完整的组件渲染周期。一旦 async 方法完成,就会有第二个完整的组件渲染周期。
  2. OnInitialized{Async}/OnParametersSet{Async} 序列按正确的顺序执行。

对代码进行一项更改[将延迟缩短到 1ms]

private async Task TaskAsync()
    => await Task.Delay(1);

检查输出,并注意第一个 OnAfterRenderAsync 已从第 10 行移动到第 18 行。它已从在第一次渲染后立即执行变为在过程结束时执行。

===========================================
e945 - AsyncOnInitializedAsyncDocumented => Component Initialized
e945 - AsyncOnInitializedAsyncDocumented => Component Attached
e945 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Started
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Started
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loading.
e945 - AsyncOnInitializedAsyncDocumented => Awaiting Task completion
e945 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
e945 - AsyncOnInitializedAsyncDocumented => Render Queued
e945 - AsyncOnInitializedAsyncDocumented => Component Rendered
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized - State set to Loaded.
e945 - AsyncOnInitializedAsyncDocumented => OnInitialized sequence Completed
e945 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Started
e945 - AsyncOnInitializedAsyncDocumented => StateHasChanged Called
e945 - AsyncOnInitializedAsyncDocumented => Render Queued
e945 - AsyncOnInitializedAsyncDocumented => Component Rendered
e945 - AsyncOnInitializedAsyncDocumented => OnParametersSet Sequence Completed
e945 - AsyncOnInitializedAsyncDocumented => SetParametersAsync Completed
[18] => e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed
e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Started
e945 - AsyncOnInitializedAsyncDocumented => OnAfterRenderAsync Completed

序列的变化是由进程完成所需的时间以及它们在 Synchronization Context 上的排队顺序驱动的。

总结

我们学到了什么

  1. 不要混合**异步**和**同步**代码。口号是**一切都异步**。
  2. StateHasChanged 很少能解决您的问题。它要么不起作用,要么掩盖了潜在的逻辑问题。
  3. OnAfterRender 中运行非 JSInterop 代码似乎可以解决问题,但您最终需要调用 StateHasChanged。上述第 2 点随即适用。您进行了比需要更多的渲染。
  4. 组件代码没有 bug。您看到的行为是故意的。
  5. 使您的代码逻辑正确,一切都会顺理成章。
  6. 不要相信组件中的断点会告诉您真实的状态故事。

一些重要的注意事项

  1. StateHasChanged 并不会渲染组件。它只是将组件的 RenderFragment 放入渲染队列。Renderer 需要在 Synchronization Context 上获得线程时间才能实际进行渲染。这只有在您的代码让出[通过一个让出 async 方法]或完成时才会发生。

  2. OnAfterRender 不是 OnInitialized{Async}/OnParametersSet{Async} 序列的一部分。它是一个事件处理程序,在组件渲染后[就像点击按钮时会调用按钮点击处理程序一样]被调用。因为它是由不同的进程触发的,所以不能保证它何时运行[如上面两个示例所示]。

  3. 组件状态突变属于 OnInitialized{Async}/OnParametersSet{Async}。不要在 OnAfterRender{Async} 中突变状态。这是不合逻辑的:然后您必须调用 StateHasChanged[并进行另一次渲染周期]才能在 UI 中反映这些更改。

同步上下文

Synchronization Context 是一个所有 UI 代码都在其上运行的虚拟线程。它是异步的,但它保证了单一的执行线程,也就是说,上下文中只有一个代码片段在执行。没有两个操作会并发执行。您可以在此处阅读更多关于它的信息。

历史

  • 2023 年 9 月 5 日:初始版本
© . All rights reserved.