ASP.NET Core Blazor 组件呈现

当组件第一次通过父组件添加到组件层次结构时,它们必须呈现。 只有在这种情况下,组件才必须呈现。 在其他情况下,组件可以按照各自的逻辑和约定呈现。

ComponentBase 的呈现约定

默认情况下,Razor 组件继承自 ComponentBase 基类,该基类包含在以下时间触发重新呈现的逻辑:

如果满足以下任一条件,则继承自 ComponentBase 的组件会跳过因参数更新而触发的重新呈现:

  • 所有参数值都是已知的不可变基元类型(例如,intstringDateTime),并且自上一组参数设置后就没有改变过。
  • 组件的 ShouldRender 方法返回 false

控制呈现流

在大多数情况下,ComponentBase 约定会在某个事件发生后导致重新呈现正确的组件子集。 开发人员通常不需要提供手动逻辑来告诉框架,要重新呈现哪些组件以及何时重新呈现它们。 框架约定的总体效果如下:接收事件的组件重新呈现自身,从而递归触发参数值可能已更改的后代组件的重新呈现。

有关框架约定对性能的影响以及如何针对呈现优化应用的组件层次结构的详细信息,请参阅 ASP.NET Core Blazor 性能最佳做法

禁止 UI 刷新 (ShouldRender)

每次呈现组件时都会调用 ShouldRender。 替代 ShouldRender 以管理 UI 刷新。 如果实现返回 true,则刷新 UI。

即使 ShouldRender 被替代,组件也始终在最初呈现。

Pages/ControlRender.razor:

@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

有关与 ShouldRender 相关的性能最佳做法的详细信息,请参阅 ASP.NET Core Blazor 性能最佳做法

何时调用 StateHasChanged

通过调用 StateHasChanged,可以随时触发呈现。 但请注意,不必要时,不要调用 StateHasChanged,这是一个常见错误,会产生不必要的呈现成本。

对于以下情况,代码不需要调用 StateHasChanged

  • 定期处理事件(无论是同步还是异步),因为 ComponentBase 会触发大多数常规事件处理程序的呈现。
  • 实现 OnInitializedOnParametersSetAsync 等典型生命周期逻辑(无论是同步还是异步),因为 ComponentBase 会触发典型生命周期事件的呈现。

但是,在本文以下部分所述的情况下,可能适合调用 StateHasChanged

异步处理程序涉及多个异步阶段

由于在 .NET 中定义任务的方式,Task 的接收方只能观察到其最终完成状态,而观察不到中间异步状态。 因此,仅在第一次返回 TaskTask 最终完成时,ComponentBase 才能触发重新呈现。 框架无法知道要在其他中间点重新呈现组件。 如果要在中间点重新呈现,请在这些点调用 StateHasChanged

以下面的 CounterState1 组件为例,该组件在每次单击时都会更新计数四次:

  • 自动呈现在 currentCount 第一次和最后一次递增之后发生。
  • 当框架未在 currentCount 递增的中间处理点自动触发重新呈现时,通过调用 StateHasChanged 触发手动呈现。

Pages/CounterState1.razor:

@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

从 Blazor 呈现和事件处理系统外部接收调用

ComponentBase 只知道其自身的生命周期方法和 Blazor 触发的事件。 ComponentBase 不知道代码中可能发生的其他事件。 例如,Blazor 不知道自定义数据存储引发的任何 C# 事件。 为了使此类事件触发重新呈现,从而在 UI 中显示已更新的值,请调用 StateHasChanged

以下面的 CounterState2 组件为例,该组件使用 System.Timers.Timer 定期更新计数并调用 StateHasChanged 来更新 UI:

  • OnTimerCallback 在 Blazor 管理的任何呈现流或事件通知之外运行。 因此,OnTimerCallback 必须调用 StateHasChanged,因为 Blazor 不知道回调中的 currentCount 更改。
  • 组件实现 IDisposable,当框架调用 Dispose 方法时,其中的 Timer 将释放。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期

由于回调是在 Blazor 的同步上下文之外调用的,因此组件必须将 OnTimerCallback 的逻辑包装在 ComponentBase.InvokeAsync 中,以将其移到呈现器的同步上下文中。 StateHasChanged 只能从呈现器的同步上下文调用,否则会引发异常。 这等效于封送到其他 UI 框架中的 UI 线程。

Pages/CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

在由特定事件重新呈现的子树之外呈现组件

UI 可能涉及:

  1. 向一个组件调度事件。
  2. 更改某种状态。
  3. 重新呈现完全不同的组件(该组件不是接收事件的组件的后代)。

处理此情况的一种方法是,提供状态管理类(通常作为依赖关系注入 (DI) 服务)注入到多个组件。 当一个组件在状态管理器上调用方法时,状态管理器引发 C# 事件,然后由独立组件接收该事件。

由于这些 C# 事件位于 Blazor 呈现管道之外,因此,请对要呈现的其他组件调用 StateHasChanged,以响应状态管理器的事件。

这与前面的 System.Timers.Timer 案例(上一部分中)类似。 由于执行调用堆栈通常保留在呈现器的同步上下文中,因此通常不需要调用 InvokeAsync。 仅当逻辑转义同步上下文时才需要调用 InvokeAsync,例如,对 Task 调用 ContinueWith 或使用 ConfigureAwait(false) 等待 Task

当组件第一次通过父组件添加到组件层次结构时,它们必须呈现。 只有在这种情况下,组件才必须呈现。 在其他情况下,组件可以按照各自的逻辑和约定呈现。

ComponentBase 的呈现约定

默认情况下,Razor 组件继承自 ComponentBase 基类,该基类包含在以下时间触发重新呈现的逻辑:

如果满足以下任一条件,则继承自 ComponentBase 的组件会跳过因参数更新而触发的重新呈现:

  • 所有参数值都是已知的不可变基元类型(例如,intstringDateTime),并且自上一组参数设置后就没有改变过。
  • 组件的 ShouldRender 方法返回 false

控制呈现流

在大多数情况下,ComponentBase 约定会在某个事件发生后导致重新呈现正确的组件子集。 开发人员通常不需要提供手动逻辑来告诉框架,要重新呈现哪些组件以及何时重新呈现它们。 框架约定的总体效果如下:接收事件的组件重新呈现自身,从而递归触发参数值可能已更改的后代组件的重新呈现。

有关框架约定对性能的影响以及如何针对呈现优化应用的组件层次结构的详细信息,请参阅 ASP.NET Core Blazor 性能最佳做法

禁止 UI 刷新 (ShouldRender)

每次呈现组件时都会调用 ShouldRender。 替代 ShouldRender 以管理 UI 刷新。 如果实现返回 true,则刷新 UI。

即使 ShouldRender 被替代,组件也始终在最初呈现。

Pages/ControlRender.razor:

@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

有关与 ShouldRender 相关的性能最佳做法的详细信息,请参阅 ASP.NET Core Blazor 性能最佳做法

何时调用 StateHasChanged

通过调用 StateHasChanged,可以随时触发呈现。 但请注意,不必要时,不要调用 StateHasChanged,这是一个常见错误,会产生不必要的呈现成本。

对于以下情况,代码不需要调用 StateHasChanged

  • 定期处理事件(无论是同步还是异步),因为 ComponentBase 会触发大多数常规事件处理程序的呈现。
  • 实现 OnInitializedOnParametersSetAsync 等典型生命周期逻辑(无论是同步还是异步),因为 ComponentBase 会触发典型生命周期事件的呈现。

但是,在本文以下部分所述的情况下,可能适合调用 StateHasChanged

异步处理程序涉及多个异步阶段

由于在 .NET 中定义任务的方式,Task 的接收方只能观察到其最终完成状态,而观察不到中间异步状态。 因此,仅在第一次返回 TaskTask 最终完成时,ComponentBase 才能触发重新呈现。 框架无法知道要在其他中间点重新呈现组件。 如果要在中间点重新呈现,请在这些点调用 StateHasChanged

以下面的 CounterState1 组件为例,该组件在每次单击时都会更新计数四次:

  • 自动呈现在 currentCount 第一次和最后一次递增之后发生。
  • 当框架未在 currentCount 递增的中间处理点自动触发重新呈现时,通过调用 StateHasChanged 触发手动呈现。

Pages/CounterState1.razor:

@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

从 Blazor 呈现和事件处理系统外部接收调用

ComponentBase 只知道其自身的生命周期方法和 Blazor 触发的事件。 ComponentBase 不知道代码中可能发生的其他事件。 例如,Blazor 不知道自定义数据存储引发的任何 C# 事件。 为了使此类事件触发重新呈现,从而在 UI 中显示已更新的值,请调用 StateHasChanged

以下面的 CounterState2 组件为例,该组件使用 System.Timers.Timer 定期更新计数并调用 StateHasChanged 来更新 UI:

  • OnTimerCallback 在 Blazor 管理的任何呈现流或事件通知之外运行。 因此,OnTimerCallback 必须调用 StateHasChanged,因为 Blazor 不知道回调中的 currentCount 更改。
  • 组件实现 IDisposable,当框架调用 Dispose 方法时,其中的 Timer 将释放。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期

由于回调是在 Blazor 的同步上下文之外调用的,因此组件必须将 OnTimerCallback 的逻辑包装在 ComponentBase.InvokeAsync 中,以将其移到呈现器的同步上下文中。 StateHasChanged 只能从呈现器的同步上下文调用,否则会引发异常。 这等效于封送到其他 UI 框架中的 UI 线程。

Pages/CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

在由特定事件重新呈现的子树之外呈现组件

UI 可能涉及:

  1. 向一个组件调度事件。
  2. 更改某种状态。
  3. 重新呈现完全不同的组件(该组件不是接收事件的组件的后代)。

处理此情况的一种方法是,提供状态管理类(通常作为依赖关系注入 (DI) 服务)注入到多个组件。 当一个组件在状态管理器上调用方法时,状态管理器引发 C# 事件,然后由独立组件接收该事件。

由于这些 C# 事件位于 Blazor 呈现管道之外,因此,请对要呈现的其他组件调用 StateHasChanged,以响应状态管理器的事件。

这与前面的 System.Timers.Timer 案例(上一部分中)类似。 由于执行调用堆栈通常保留在呈现器的同步上下文中,因此通常不需要调用 InvokeAsync。 仅当逻辑转义同步上下文时才需要调用 InvokeAsync,例如,对 Task 调用 ContinueWith 或使用 ConfigureAwait(false) 等待 Task

当组件第一次通过父组件添加到组件层次结构时,它们必须呈现。 只有在这种情况下,组件才必须呈现。 在其他情况下,组件可以按照各自的逻辑和约定呈现。

ComponentBase 的呈现约定

默认情况下,Razor 组件继承自 ComponentBase 基类,该基类包含在以下时间触发重新呈现的逻辑:

如果满足以下任一条件,则继承自 ComponentBase 的组件会跳过因参数更新而触发的重新呈现:

  • 所有参数值都是已知的不可变基元类型(例如,intstringDateTime),并且自上一组参数设置后就没有改变过。
  • 组件的 ShouldRender 方法返回 false

控制呈现流

在大多数情况下,ComponentBase 约定会在某个事件发生后导致重新呈现正确的组件子集。 开发人员通常不需要提供手动逻辑来告诉框架,要重新呈现哪些组件以及何时重新呈现它们。 框架约定的总体效果如下:接收事件的组件重新呈现自身,从而递归触发参数值可能已更改的后代组件的重新呈现。

有关框架约定对性能的影响以及如何针对呈现优化应用的组件层次结构的详细信息,请参阅 ASP.NET Core Blazor 性能最佳做法

禁止 UI 刷新 (ShouldRender)

每次呈现组件时都会调用 ShouldRender。 替代 ShouldRender 以管理 UI 刷新。 如果实现返回 true,则刷新 UI。

即使 ShouldRender 被替代,组件也始终在最初呈现。

Pages/ControlRender.razor:

@page "/control-render"

<label>
    <input type="checkbox" @bind="shouldRender" />
    Should Render?
</label>

<p>Current count: @currentCount</p>

<p>
    <button @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;
    private bool shouldRender = true;

    protected override bool ShouldRender()
    {
        return shouldRender;
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

有关与 ShouldRender 相关的性能最佳做法的详细信息,请参阅 ASP.NET Core Blazor 性能最佳做法

何时调用 StateHasChanged

通过调用 StateHasChanged,可以随时触发呈现。 但请注意,不必要时,不要调用 StateHasChanged,这是一个常见错误,会产生不必要的呈现成本。

对于以下情况,代码不需要调用 StateHasChanged

  • 定期处理事件(无论是同步还是异步),因为 ComponentBase 会触发大多数常规事件处理程序的呈现。
  • 实现 OnInitializedOnParametersSetAsync 等典型生命周期逻辑(无论是同步还是异步),因为 ComponentBase 会触发典型生命周期事件的呈现。

但是,在本文以下部分所述的情况下,可能适合调用 StateHasChanged

异步处理程序涉及多个异步阶段

由于在 .NET 中定义任务的方式,Task 的接收方只能观察到其最终完成状态,而观察不到中间异步状态。 因此,仅在第一次返回 TaskTask 最终完成时,ComponentBase 才能触发重新呈现。 框架无法知道要在其他中间点重新呈现组件。 如果要在中间点重新呈现,请在这些点调用 StateHasChanged

以下面的 CounterState1 组件为例,该组件在每次单击时都会更新计数四次:

  • 自动呈现在 currentCount 第一次和最后一次递增之后发生。
  • 当框架未在 currentCount 递增的中间处理点自动触发重新呈现时,通过调用 StateHasChanged 触发手动呈现。

Pages/CounterState1.razor:

@page "/counter-state-1"

<p>
    Current count: @currentCount
</p>

<p>
    <button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
</p>

@code {
    private int currentCount = 0;

    private async Task IncrementCount()
    {
        currentCount++;
        // Renders here automatically

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        StateHasChanged();

        await Task.Delay(1000);
        currentCount++;
        // Renders here automatically
    }
}

从 Blazor 呈现和事件处理系统外部接收调用

ComponentBase 只知道其自身的生命周期方法和 Blazor 触发的事件。 ComponentBase 不知道代码中可能发生的其他事件。 例如,Blazor 不知道自定义数据存储引发的任何 C# 事件。 为了使此类事件触发重新呈现,从而在 UI 中显示已更新的值,请调用 StateHasChanged

以下面的 CounterState2 组件为例,该组件使用 System.Timers.Timer 定期更新计数并调用 StateHasChanged 来更新 UI:

  • OnTimerCallback 在 Blazor 管理的任何呈现流或事件通知之外运行。 因此,OnTimerCallback 必须调用 StateHasChanged,因为 Blazor 不知道回调中的 currentCount 更改。
  • 组件实现 IDisposable,当框架调用 Dispose 方法时,其中的 Timer 将释放。 有关详细信息,请参阅 ASP.NET Core Razor 组件生命周期

由于回调是在 Blazor 的同步上下文之外调用的,因此组件必须将 OnTimerCallback 的逻辑包装在 ComponentBase.InvokeAsync 中,以将其移到呈现器的同步上下文中。 StateHasChanged 只能从呈现器的同步上下文调用,否则会引发异常。 这等效于封送到其他 UI 框架中的 UI 线程。

Pages/CounterState2.razor:

@page "/counter-state-2"
@using System.Timers
@implements IDisposable

<h1>Counter with <code>Timer</code> disposal</h1>

<p>
    Current count: @currentCount
</p>

@code {
    private int currentCount = 0;
    private Timer timer = new Timer(1000);

    protected override void OnInitialized()
    {
        timer.Elapsed += (sender, eventArgs) => OnTimerCallback();
        timer.Start();
    }

    private void OnTimerCallback()
    {
        _ = InvokeAsync(() =>
        {
            currentCount++;
            StateHasChanged();
        });
    }

    public void Dispose() => timer.Dispose();
}

在由特定事件重新呈现的子树之外呈现组件

UI 可能涉及:

  1. 向一个组件调度事件。
  2. 更改某种状态。
  3. 重新呈现完全不同的组件(该组件不是接收事件的组件的后代)。

处理此情况的一种方法是,提供状态管理类(通常作为依赖关系注入 (DI) 服务)注入到多个组件。 当一个组件在状态管理器上调用方法时,状态管理器引发 C# 事件,然后由独立组件接收该事件。

由于这些 C# 事件位于 Blazor 呈现管道之外,因此,请对要呈现的其他组件调用 StateHasChanged,以响应状态管理器的事件。

这与前面的 System.Timers.Timer 案例(上一部分中)类似。 由于执行调用堆栈通常保留在呈现器的同步上下文中,因此通常不需要调用 InvokeAsync。 仅当逻辑转义同步上下文时才需要调用 InvokeAsync,例如,对 Task 调用 ContinueWith 或使用 ConfigureAwait(false) 等待 Task