Blazor的StateHasChanged()方法未更新子组件

9
我有一个服务,其中包含项目目录。这些项目依赖于该服务的活动公司。
// Tell Service to refresh in context of Company 1
await Service.SetActiveCompany(1);
// Get Catalog (which will be in the context of Company 1)
Catalog catalog = Service.Catalog;

// Tell Service to refresh in context of Company 2
await Service.SetActiveCompany(2);
// Get Catalog (which will be in the context of Company 2)
catalog = Service.Catalog;

这段代码在独立环境下运行正常,目录会根据所选公司刷新展示的物品。

在我的应用程序中,我有一个布局包含下拉菜单,用于选择活动公司。当这个选项改变时,我会调用:

await Service.SetActiveCompany(dropdown.selectedCompanyId);

在Service中,我刷新目录并触发一个事件。
public class CompanyChangedEventArgs : EventArgs { }
public class Service
{
    public event EventHandle<CompanyChangedEventArgs> CompanyChanged;
    protected void OnCompanyChanged() 
        => CompanyChanged?.Invoke(this, new CompanyChangedEventArgs());
    public Catalog Catalog { get; private set; }
    public async Task SetActiveCompany(int companyId)
    {
        Catalog = await API.GetCatalogForCompany(companyId);
        OnCompanyChanged();
    }
}

在上述布局中的父组件中,我捕获了该事件。
<CatalogSectionA />
<CatalogSectionB />
@code {
   protected override void OnInitialized()
   {
       Service.CompanyChanged += HandleCompanyChanged;
       base.OnInitialized();
   }
   HandleCompanyChanged(object sender, EventArgs e) => StateHasChanged();
}

我期望这将导致CatalogSectionA和CatalogSectionB组件刷新。
目录区域A
<div>
  @foreach (Item i in Catalog.SectionA.Items)
  {
    <div>@i.Name</div>
  }
</div>
@code {
  Catalog Catalog => Service.Catalog;
}

目录部分B

<div>
  @foreach (Item i in Catalog.SectionB.Items)
  {
    <div>@i.Name</div>
  }
</div>
@code {
  Catalog Catalog => Service.Catalog;
}

我原本期望在父组件中调用StateHasChanged()会强制重新渲染 CatalogSectionA 和 CatalogSectionB,但它们没有重新渲染。
我可以在每个组件上添加一个 Refresh 方法,在父级处理 company 变更事件时调用那些方法,但我认为在 Blazor 中不需要这样做,StateHasChanged() 应该就可以解决问题?

你可以尝试在 InvokeAsync 中调用 StateHasChanged 吗?例如:HandleCompanyChanged(…) => InvokeAsync(() => StateHasChanged()) - poke
尝试了一下。还尝试将HandleCompanyChanged改为异步并“等待”InvokeAsync调用,但没有变化。 - Neil W
我应该澄清一下,我上面的代码是我认为最小化显示问题所需的。实际上,对SetActiveCompany的调用是异步的,因为它调用了一个异步API。这改变了对原始问题的分析吗?已更新问题以反映此情况。 - Neil W
2
啊,我之前没有仔细阅读;在父组件中调用StateHasChanged不会导致子组件重新渲染。那样会很昂贵。更改后的状态只适用于当前组件,因此该组件将检查是否需要重新渲染任何内容。由于子组件从外部看仍然是相同的(未更改任何参数或其他内容),因此父组件不会导致子组件重新渲染。 - 我建议您将“Catalog”作为参数传递给子组件。这使数据流更明确,并解决了您的问题。 - poke
感谢确认,@poke。我选择将目录作为级联值传递给所有需要它的子组件,而不是让子组件直接从服务获取它。很高兴听到第二个意见证实这种方法是有道理的。感谢您的帮助。 - Neil W
3个回答

12

在子组件上放置一个“冗余”的级联参数后,它就能正常工作。

因为状态(参数)是在服务中进行管理的,没有传递任何参数,我期望StateHasChanged()会提示子组件重新渲染并返回服务。

所以,我的结论是:如果我在父组件上调用StateHasChanged(),那么只有当Blazor检测到子组件对父组件更改的“依赖”时,子组件才会被重新渲染。

如果没有依赖关系(即没有参数或级联参数),那么Blazor似乎会认为在父组件中更改状态时不需要在子组件中进行任何更改。


3
是的,你是正确的。设置参数值会触发子组件上的 SetParametersAsync,而调用 StateHasChanged 是该过程的一部分。还有另一种方法取决于服务的范围。如果它是作用域限定的,每个需要知道 CompanyChanged 事件的组件都会注入服务 - 它们都使用的是同一个对象实例 - 注册事件,然后在事件处理程序中调用 StateHasChanged - MrC aka Shaun Curtis

6
在子组件中:
[Parameter] public Func<bool>? ShouldReRender { get; set; }

在父级元素中:
<MyComponent ShouldReRender="ShouldRender" />

这将创建所需的依赖关系,我认为这比虚拟的CascadingParameter或RenderFragment更清晰。

2
这是一个不错且简单的解决方案,谢谢。 - M.M

0
所以我刚刚遇到了这个问题,我想也许值得添加一个替代解决方案。
我的使用情况涉及到mDNS和BLE 5.3+在一个Blazor MAUI应用程序中,需要不断轮询和频繁但零散的更新场景。
最初我使用了一个定时器来定期更新组件,但这样做相当不优雅。然后我转向了Pawel提出的解决方案,但我的最终解决方法是创建了一个名为GlobalRenderManager的单例服务。
结构相当简单:
internal sealed class GlobalRenderManager
{
    internal event EventHandler<int>? ForceRender;

    internal void _Render(int _CID)
    {
        ForceRender?.Invoke(this, _CID);
    }
}

将其注入为单例,允许组件订阅事件处理程序,并且我可以从代码的任何其他位置调用单个组件的渲染。
各个组件可以订阅事件,并在事件触发特定整数时调用其内部的StateHasChanged()。
这可能仍然不是一个完美的解决方案,但它确实为需要更频繁基于事件更新的应用程序提供了更灵活的控制。

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