【问题标题】:StateHasChanged() re-render the component one time out of twoStateHasChanged() 两次重新渲染组件
【发布时间】:2020-03-14 21:39:23
【问题描述】:

我正在制作一个 Blazor 服务器端项目,我想制作一个在单击后停用的按钮,但不使用 <button>disabled 属性。代码很简单:

@functions {

    LogInForm logInForm = new LogInForm();
    bool IsDisabled;
    SignInResult result;

    protected override void OnInitialized()
    {
        IsDisabled = false;
    }

    async Task TryLogIn()
    {
        IsDisabled = true;
        StateHasChanged();
        result =  await _LogInService.TryLogIn(logInForm);
        Console.WriteLine("Logging status : " + (result.Succeeded ? "Sucess" : "Failure"));
        IsDisabled = false;
        StateHasChanged();
    }

}

出于奇怪的原因,第一个 StateHasChanged 没有触发,但第二个确实会重新呈现页面。通过进入调试模式并进入StateHasChanged() 方法可以很容易地进行测试。在第二次调用时,它确实在进入方法后停止在 HTML 代码上,但不是第一次。

为什么会这样?

NB : 我不是在寻找仅使用 Task.DelayTask.Run(...) 的任何解决方法,因为这些线程和 UI 刷新线程之间存在竞争条件,因此不是一个可靠的解决方案。我正在寻找有关StateHasChanged() 行为的答案或使用PropertyChangedEventCallback 之类的事件并将按钮作为子组件的解决方法。

编辑:经过一些测试,似乎StateHasChanged() 仅在对Task 执行await 操作后触发组件的重新渲染。可以通过在result = await _LogInService.TryLogIn(logInForm); 行中添加评论或将IsDisabled = ... 更改为await new Task.Run(() => { IsDisabled = ...}) 来轻松测试它。我现在有一些解决方法,但我仍然想知道为什么这是一个功能。 StateHasChanged() 不应该在任何操作后重新渲染吗?或者它认为只有 async 操作(主要是服务器调用)可以改变 UI 中的某些内容?

【问题讨论】:

  • 这能回答你的问题吗? Blazor - Display wait or spinner on API call
  • 不,因为您的答案似乎仅依赖于 Task.Delay(1) 解决方法,在 Nota Bene 中已经说过,由于竞争条件,它不可靠。此外,我更想知道StateHasChanged() 是如何工作的,或者如何通过使用事件来做到这一点,而不是解决方法。
  • 这是旧的 DoEvents() 问题的新伪装。
  • 真正的问题可能是_service.TryLogIn(),它真的是异步的吗?还是只是假装?
  • 不,TryLogIn() 不是假异步,因为它使用SignInManager 来检查和登录用户。

标签: c# asp.net razor blazor blazor-server-side


【解决方案1】:

Blazor 团队即将发布有关 StateHasChanged() 工作原理的文档。

你可以在这里追踪它:https://github.com/aspnet/AspNetCore/issues/14591

暂时,我认为这个取自github评论的解释是一个很好的解释:

添加对 StateHasChanged 的​​调用只是将组件排队 呈现。渲染器决定何时进行渲染。

这可以由四种情况触发:

  • 初始渲染,引导进程触发根组件及其所有子组件的初始渲染。
  • 一个事件,其中处理该事件的组件会在该事件之后自动触发新的渲染,并且可能 如果它呈现新的孩子或改变他们的参数,它的孩子。
  • 由于从 InvokeAsync 调用调用 StateHasChanged(本质上是编组回 UI 线程)
  • 由于父组件更改子组件的参数,这是差异过程的一部分,当 渲染器在子组件上调用 SetParametersAsync。

非常清楚,调用 StateHasChanged 只会为 组件或“将其标记为脏”。

渲染器决定何时以及如何生成 呈现。 BuildRenderTree 不会产生新的渲染输出,仅 在组件的“V-DOM”的新定义中 被调用。

通常,每个渲染批次都会渲染一次组件(这是一个 一起渲染/区分并发送到的组件的集合 用于更新的 UI)。组件只有两种情况 一次多次渲染:

  • 你有一个组件直接实现了IComponent并调用了RenderHandle.Render
  • 子组件和父组件之间存在循环依赖关系,这可能导致父组件作为子组件的一部分重新呈现 从父级调用一些回调参数作为其一部分 初始化

来源:https://github.com/aspnet/AspNetCore/issues/15175#issuecomment-544890549

【讨论】:

  • 这是一个非常好的解释,谢谢。所以我应该使用事件来“正确”触发重新渲染?我还是新手,特别是事件编程,所以我试图了解如何使用它们。
【解决方案2】:

以下是描述重新渲染如何发生的执行流程:

  1. 父组件调用StateHasChanged
  2. 生成新的渲染树
  3. 老树和新树之间的差异正在发生
  4. 传递给您的子组件的值被认为与其当前持有的不同。
  5. 正在子组件上调用 SetParameters 以使用父组件传递给它的值更新它们。

现在,当您在为局部变量 IsDisabled 赋值后调用 StateHasChanged 时,并不会真正改变组件的状态,也没有理由调用 StateHasChanged 会产生重新渲染。当您调用 StateHasChanged 时,它只是将该组件的呈现请求排队。但是没有理由重新渲染...

或者它认为只有异步操作(主要是服务器调用)可以 更改 UI 中的某些内容?

与操作类型是否为异步无关,除了 OnInitializedAsync 方法,在这种情况下,当 OnInitializedAsync 方法完成时会自动调用 StateHasChanged 方法再次重新渲染 UI,异步调用检索到的新数据在 OnInitializedAsync 方法中执行。

更新:

您可以通过多种方式完成您想要的,这里演示了其中最简单的一种:

   <input type="button" value="Click me now"  disabled="@IsDisabled" @onclick="TryLogIn" />


@code{ 

    bool IsDisabled;

    protected override void OnInitialized()
    {
        IsDisabled = false;
    }

    async Task TryLogIn()
    {
        IsDisabled = true;

        // Do some async work here...
        // Note: Replace your async method with Task.Delay 
        await Task.Delay(5000);

        IsDisabled = false;

    }

}

这应该可以... 注意:禁用按钮控件的唯一方法是使用 disabled 属性

无需调用 StateHasChanged 方法。当编译器(编译器)为您的组件创建 EventCallback 'delegate' 时,它会被编译器插入到源代码中的代码自动调用。

触发 UI 事件后会自动调用 StateHasChanged 方法。

【讨论】:

  • 感谢您的回答!那么,如果不是它包含的所有变量,我们如何定义组件的状态呢?
  • 我同意你的观点,局部变量 IsDisabled 等变量可以描述组件的状态,但不是需要重新渲染的。给这样一个局部变量赋一个新值,我认为,可能被认为是修改变量“状态”,但并不是真正修改组件本身,需要重新渲染。
  • 好的,因此状态仅由组件的参数定义。那么哪种方法是做我想做的“正确”方法?将按钮转换为组件并修改其参数,将事件处理程序设置为单个组件或父组件和他的子组件?
  • 再次感谢您的回答。我没有使用disabled 属性,因为我读到它可能被最终用户禁用,所以我想让我的按钮安全。似乎学习 Blazor 并不难,但学习元素有点分散,微软文档可能不完整,或者对于初学者来说感觉晦涩难懂,因为 blazor 有点新。我现在将坚持使用您的解决方案或我的解决方法,我将研究如何更正确地做到这一点,所以最后我可以拥有一个具有所需行为的 SubmitButton 组件。我暂时将您的答案标记为已接受。
  • 不使用disabled属性的理由是歪的,与现实无关。网络上的每一件事都可能被篡改,想要让你的按钮安全是个坏主意。安全性始终在服务器上实现,而不是在客户端上,如果我们应用一些测量,它们仅用于 UI 目的,例如在 Blazor WebAssembly 中使用 AuthorizeView 组件...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2021-09-19
  • 2020-07-14
  • 2019-05-26
  • 1970-01-01
  • 1970-01-01
  • 2017-12-29
  • 2020-10-16
相关资源
最近更新 更多