StateHasChanged() 函数会使组件重新渲染,但每两次只会有一次重新渲染。

5

我正在制作一个 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 代码上,但第一次不会。
为什么会这样?
注意:我不希望使用 Task.Delay 或者 Task.Run(...) 来解决问题,因为这些线程和 UI 刷新线程之间存在竞争关系,所以这不是可靠的解决方案。我正在寻找关于 StateHasChanged() 行为的答案,或者通过使用诸如 PropertyChangedEventCallback 事件并将按钮作为子组件来实现绕过。
编辑:经过一些测试,似乎只有在 Task 上进行 await 操作后,StateHasChanged() 才会触发组件的重新渲染。可以轻松测试将 result = await _LogInService.TryLogIn(logInForm); 行注释掉或将 IsDisabled = ... 更改为 await new Task.Run(() => { IsDisabled = ...})。现在我已经有了一些解决方法,但我仍然想知道为什么会出现这样的特性。难道 StateHasChanged() 不应该在任何操作后都重新渲染?或者它只认为只有 async 操作(因此主要是服务器调用)才能改变 UI 中的内容?

这个回答解决了你的问题吗?Blazor - 在API调用时显示等待或旋转器 - dani herrera
不行,因为你的答案似乎仅依赖于“Task.Delay(1)”解决方法,而如Nota Bene所述,由于竞争条件,这种方法并不可靠。此外,我更想要一个关于StateHasChanged()如何工作或如何使用事件来实现它的答案,而不是一个解决方法。 - PepperTiger
这是一个新伪装下的老问题,就是那个经典的DoEvents()问题。 - H H
真正的问题可能出在 _service.TryLogIn() 上,它是真的异步的吗?还是只是假装而已? - H H
不,TryLogIn() 不是一个虚假的异步函数,它使用 SignInManager 来检查并登录用户。 - PepperTiger
这意味着你不知道 - 我猜测SignIn并不总是需要访问数据库。 - H H
2个回答

7
Blazor团队即将发布关于StateHasChanged()如何工作的文档。
您可以在此处跟踪它:https://github.com/aspnet/AspNetCore/issues/14591 目前而言,我认为从GitHub评论中提取的解释是一种很好的解释:
添加对StateHasChanged的调用仅将组件排队以进行渲染。渲染器决定何时进行渲染。
这可以通过以下4种情况触发:
1. 初始呈现,其中引导过程会触发根组件及其所有子组件的初始呈现。 2. 事件,处理事件的组件在事件之后自动触发新的呈现,可能还会呈现新的子级或更改其参数。 3. 在调用InvokeAsync后调用StateHasChanged的结果(本质上是回调到UI线程)。 4. 由于父组件为子组件更改参数而导致的。这是在呈现器在子组件上调用SetParametersAsync的差异处理过程的一部分。
要非常清楚的是,仅调用StateHasChanged会为组件排队呈现或“标记为脏”状态。
渲染器决定何时以及如何生成呈现。BuildRenderTree不会导致新的呈现输出,只会在被调用时为组件的“V-DOM”定义提供新的定义。
通常,每个呈现批处理(一组一起呈现/比较并发送到UI进行更新的组件)渲染一次组件。只有两种情况下,组件才会在一个批处理中呈现多次:
1.您有一个直接实现IComponent并调用RenderHandle.Render的组件。 2.您有一个子级和父级组件之间的循环依赖关系,可能会导致作为其初始化的一部分从父项调用某些回调参数而重新呈现父项。
来源:https://github.com/aspnet/AspNetCore/issues/15175#issuecomment-544890549

1
这是一个非常好的解释,谢谢。那么我应该使用事件来“正确”触发重新渲染吗?我还很新手,特别是在事件编程方面,所以我正在努力理解如何使用它们。 - PepperTiger

3
以下是执行流程,描述了重新渲染发生的方式:
  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“委托”时,它会自动插入代码,并自动调用该方法。

在触发UI事件后,将自动调用StateHasChanged方法。


谢谢你的回答!那么,如果不是它所持有的所有变量,我们如何定义组件的状态呢? - PepperTiger
我同意你的观点,例如局部变量IsDisabled可以描述组件的状态,但不需要重新渲染。我认为,给这样的局部变量赋新值可以被视为修改变量“state”,但并不是真正修改组件本身并需要重新渲染的情况。 - enet
好的,所以状态仅由组件的参数定义。那么,哪种方法是实现我想要的功能的“正确”方法?将按钮转换为组件并修改其参数,将事件处理程序设置为单个组件或设置为父级及其子级? - PepperTiger
再次感谢您的回答。我没有使用“disabled”属性,因为我读到它可以被最终用户禁用,所以我想让我的按钮更安全。似乎学习Blazor并不困难,但是学习元素有点分散在各处,而且Microsoft的文档对于初学者来说可能不完整或感觉模糊,因为Blazor是比较新的技术。我现在会使用您的解决方案或我的解决方法,并学习如何更好地实现它,这样最终我就可以拥有一个符合要求的“SubmitButton”组件。我现在将标记您的答案为已接受。 - PepperTiger
1
不使用disabled属性的理由是扭曲的,与现实无关。网络上的所有内容都可以被篡改,想要使您的按钮安全是个坏主意。安全性始终在服务器上实现,而不是在客户端上实现,如果我们应用某些措施,它们仅适用于UI目的,例如在Blazor WebAssembly中使用AuthorizeView组件... - enet
这是来自文档的内容:Blazor WebAssembly 应用程序在客户端上运行。授权仅用于确定要显示哪些 UI 选项。由于用户可以修改或绕过客户端检查,因此 Blazor WebAssembly 应用程序无法强制执行授权访问规则。 - enet

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接