asp.net Blazor参数绑定问题

8ehkhllq  于 2023-03-20  发布在  .NET
关注(0)|答案(3)|浏览(220)

我有这样一个组件:

<h3>@IsEnabled</h3>

@code {
    [Parameter]
    public bool IsEnabled { get; set; }
}

还有这样一个页面:

!!!旧代码,已更改代码,下面没有无限循环!!!

@page "/"

<PageTitle>Binding Test</PageTitle>

<BlazorBindingTest.Components.MyComponent IsEnabled="@IsEnabled"></BlazorBindingTest.Components.MyComponent>

@code
{
    public bool IsEnabled { get; set; } = false;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            while (true)
            {
                await Task.Delay(1000);
                IsEnabled = !IsEnabled;
                //StateHasChanged(); // <<<<<<<<< Why is this neccessary?
            }
        }
    }
}

而且我注意到,StateHasChanged在Blazor Server和WASM中似乎都是必要的。然而,我读到StateHasChanged可能会对整个渲染产生重大的负面影响。
为什么这里有必要,如何避免?组件/绑定不是应该自动更新吗?
我试过字段、属性、私有、公共等。

编辑

无限循环只是一个例子来说明这个问题,我已经修改了代码如下,这个问题自然会继续存在。这个问题只是关于StateHasChanged的需要:

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        _ = Task.Run(myInfinityLoop); // Do not await
    }
}

private async Task myInfinityLoop()
{
    while (true)
    {
        IsEnabled = !IsEnabled;
        await Task.Delay(1000);
        //StateHasChanged(); <<<< Still neccessary - Why?
    }
}
m1m5dgzv

m1m5dgzv1#

为什么这里有必要,如何避免?组件/绑定不是应该自动更新吗?
回顾以下几点。您正在组件类中运行一个进程,并更改其内部状态:IsEnabled。没有组件观察器监视组件的内部状态,因此您必须通过调用StateHasChanged来初始化更新过程。渲染器处理该请求的过程的一部分是检查子组件参数的状态。它在参数 * 可能已更改 * 的任何子组件上调用SetParametersAsync。注意,渲染器无法检测对象中的更改,所以它假设它们已经改变。
考虑要点:
1.你应该非常小心你在AfterRender方法中运行什么代码。[个人]我建议只运行JSInterop代码,除非你不得不这样做。
1.生命周期方法OnInitialized{Async}/OnParametersSet{Async}/OnAfterRender{Async}中的代码应[尽快]运行并完成。不良做法:这是一个无限循环,意味着第一个OnAfterRenderAsync永远不会完成。
1.任何UI事件-按钮点击、输入、组件EventCallbacks -如果处理程序在等待和处理程序执行完成时产生,则自动调用StateHasChanged
1.你应该很少需要调用StateHasChanged。太多时候调用它是为了尝试和修复代码中的逻辑问题。修复了逻辑,调用StateHasChanged的需要就消失了。例外情况见5。
1.您需要在标准事件处理程序中调用StateHasChanged(例如下面的计时器超时事件)。
1.当您更新组件状态并希望更新组件及其兄弟组件的UI时,需要调用StateHasChanged,因为没有观察器监视内部组件状态并在组件状态发生变化时触发渲染。
1.对StateHasChanged的不必要调用触发渲染树级联。树中的组件越多,负载越高。
下面是代码的一个略有不同的实现,它使用计时器来执行循环,并使用事件处理程序来驱动UI更新。

  • 我的组件.剃须刀 *
<div class="@this.Css">@this.IsEnabled</div>

@code {
    [Parameter, EditorRequired] public bool IsEnabled { get; set; }

    private string Css => this.IsEnabled ? "alert alert-success" : "alert alert-danger";
}
  • 索引剃刀 *
@page "/"
@using System.Timers
@implements IDisposable

<PageTitle>Index</PageTitle>

<div class="m-2">
    <button class="@this.buttonCss" @onclick=this.Start>@this.buttonText</button>
</div>

<h1>Hello, world!</h1>

<MyComponent IsEnabled=this.isEnabled />
@code {
    private bool isEnabled { get; set; } = false;
    private string buttonCss => _timer.Enabled ? "btn btn-danger" : "btn btn-success";
    private string buttonText => _timer.Enabled ? "Stop" : "Start";

    private System.Timers.Timer _timer = new System.Timers.Timer(1000);

    protected override void OnInitialized()
    {
        _timer.AutoReset = true;
        _timer.Elapsed += this.OnTimerElapsed;
    }

    protected Task Start()
    {
        // No call to StateHasChanged needed as the UI Event Handler will do the calls automatically
        _timer.Enabled = !_timer.Enabled;
        return Task.CompletedTask;
    }

    // This invokes the handler on the UI Dispatcher context
    private async void OnTimerElapsed(object? sender, ElapsedEventArgs e)
        => await this.InvokeAsync(this.RefreshData);

    private Task RefreshData()
    {
        this.isEnabled = !this.isEnabled;
        // Need to call StateHasChanged as this is not a UI Event and there's no automatic calls to StateHasChanged
        // We call StateHasChanged directly here as the method is running in the Dispatcher context
        this.StateHasChanged();
        return Task.CompletedTask;
    }

    public void Dispose()
        => _timer.Elapsed -= this.OnTimerElapsed;
}
hof1towb

hof1towb2#

第二个答案解决了更新后的问题代码中的一些评论和问题。由于这是一个不同的问题,我添加了第二个答案,而不是一个很长的单一答案。
您是否在StateHasChanged();未注解的情况下运行过这段代码?
[我添加了一些调试代码,以便您可以查看正在运行的线程。]

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    Debug.WriteLine($"Current Thread:{Thread.CurrentThread.ManagedThreadId}");
    if (firstRender)
    {
        _ = Task.Run(myInfinityLoop); // Do not await
    }
}

private async Task myInfinityLoop()
{
    while (true)
    {
        Debug.WriteLine($"Current Thread:{Thread.CurrentThread.ManagedThreadId}");
        IsEnabled = !IsEnabled;
        await Task.Delay(1000);
        StateHasChanged();
    }
}

不会发生任何情况。如果选中Output,您将看到:

Exception thrown: 'System.InvalidOperationException' in Microsoft.AspNetCore.Components.dll

这突出了这样做的一个问题:

_ = Task.Run(myInfinityLoop);

发生异常的原因是,您通过调用Task.Run切换到线程池,然后尝试在Dispatcher上下文之外运行StateHasChanged
将以下扩展名添加到Task:

public static class TaskExtensions
{
    public static async Task Await(this Task task, Action? taskSuccess, Action<Exception>? taskFailure)
    {
        try
        {
            await task;
            taskSuccess?.Invoke();
        }
        catch (Exception ex)
        {
            taskFailure?.Invoke(ex);
        }
    }
}

然后

//...
<div hidden="@(!isError)" class="alert alert-danger">
    @errorMessage
</div>

//...
    private bool isError;
    private string? errorMessage;

    private void HandleFailure(Exception ex)
    {
        isError = true;
        errorMessage = ex.Message;
        this.InvokeAsync(StateHasChanged);
    }

    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        Debug.WriteLine($"Current Thread:{Thread.CurrentThread.ManagedThreadId}");
        if (firstRender)
        {
           _ = Task.Run(myInfinityLoop).Await(null, HandleFailure);
        }
        return Task.CompletedTask;
    }

您将发现异常,并能够处理它。
您可以像这样解决异常:

IsEnabled = !IsEnabled;
    await Task.Delay(1000);
    await this.InvokeAsync(StateHasChanged);

我假设您这样做是为了从生命周期中“触发并忘记”循环,并让生命周期方法完成。
还有一种不同的方法,它不涉及Task.Run

private Task _fireAndForget = Task.CompletedTask;

    protected override Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
            _fireAndForget = myInfinityLoop().Await(null, HandleFailure); 

        return Task.CompletedTask;
    }

这只是将任务分配给一个类变量,它还将解决异常错误,因为现在StateHasChanged将在Dispatcher上下文中运行。
然后,您可以将调用移动到OnInitialized

protected override void OnInitialized()
    {
        _fireAndForget = myInfinityLoop().Await(null, HandleFailure);
    }
cwdobuhd

cwdobuhd3#

Blazor会在检测到用户交互(如按钮单击等)时自动检测更改(属性值或字段)。这是因为Blazor内置了事件处理程序来检测用户交互。
但是,当您在UI事件处理程序之外更新属性或字段时(如在原始示例中,您在OnAfterRenderAsync方法中更新IsEnabled属性),Blazor不知道发生了更改,您需要手动调用StateHasChanged()来触发重新呈现。
通常,不建议更改OnAfterRenderAsync内的参数,因为这可能导致意外的永久循环。
您可以改用EventCallback

// Parent Component
@page "/"

<PageTitle>Binding Test</PageTitle>

<BlazorBindingTest.Components.MyComponent IsEnabled="@IsEnabled" OnToggleEnabled="@ToggleEnabled"></BlazorBindingTest.Components.MyComponent>

@code
{
    public bool IsEnabled { get; set; } = false;

    private void ToggleEnabled()
    {
        IsEnabled = !IsEnabled;
    }
}

// Child Component
@code {
    [Parameter]
    public bool IsEnabled { get; set; }

    [Parameter]
    public EventCallback OnToggleEnabled { get; set; }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            while (true)
            {
                await Task.Delay(1000);
                await OnToggleEnabled.InvokeAsync();
            }
        }
    }
}

相关问题