c# - Blazor 组件 : notify of collection-changed event causing thread collisions

标签 c# multithreading asp.net-core async-await blazor

我正在使用 .Net Core 3.0 开发 ASP.NET Core Blazor 应用程序(我知道 3.1,但由于 Mordac,我现在只能使用此版本)。

我有一个多组件页面,其中一些组件需要访问相同的数据,并且需要在更新集合时全部更新。我一直在尝试使用基于 EventHandler 的回调,但它们几乎同时在自己的线程上调用(如果我 understand correctly ),导致 .razor 组件中的回调尝试对上下文进行服务调用同时。

注意:我已尝试使 DbContext 的生命周期为 transient ,但我仍然遇到竞争条件。

我很可能陷入了异步 blender ,并且不知道如何摆脱。

我初步得出结论,事件 EventHandler 方法在这里不起作用。我需要某种方法来触发对组件的“集合更改”更新,而不触发竞争条件。

考虑过使用以下内容更新这些竞争条件中涉及的服务:

  • 用可公开绑定(bind)的集合属性替换每个搜索函数
  • 让每个创建/更新/删除调用都会更新这些集合中的每一个

这将允许组件直接绑定(bind)到已更改的集合,我认为这将导致任何组件中与其的每个绑定(bind)都可以更新,而无需明确告知,这在Turn 可以让我完全放弃“集合更改”事件处理。

但我对尝试这个犹豫不决,而且还没有这样做,因为它会给每个主要服务功能带来相当多的开销。

还有其他想法吗?请帮忙。如果集合已更改,我希望依赖该集合的 Blazor 组件能够以某种方式进行更新,无论是通过通知、绑定(bind)还是其他方式。

以下代码是我所得到的代码的大幅简化,当从服务调用事件处理程序时,它仍然会导致竞争条件。

型号

public class Model
{
    public int Id { get; set; }
    public string Msg { get; set; }
}

我的上下文

public class MyContext : DbContext
{
    public MyContext() : base()
    {
        Models = Set<Model>();
    }

    public MyContext(DbContextOptions<MyContext> options) : base(options)
    {
        Models = Set<Model>();
    }

    public DbSet<Model> Models { get; set; }
}

模型服务

public class ModelService
{
    private readonly MyContext context;
    private event EventHandler? CollectionChangedCallbacks;

    public ModelService(MyContext context)
    {
        this.context = context;
    }

    public void RegisterCollectionChangedCallback(EventHandler callback)
    {
        CollectionChangedCallbacks += callback;
    }

    public void UnregisterCollectionChangedCallback(EventHandler callback)
    {
        CollectionChangedCallbacks -= callback;
    }

    public async Task<Model[]> FindAllAsync()
    {
        return await Task.FromResult(context.Models.ToArray());
    }

    public async Task CreateAsync(Model model)
    {
        context.Models.Add(model);
        await context.SaveChangesAsync();

        // No args necessary; the callbacks know what to do.
        CollectionChangedCallbacks?.Invoke(this, EventArgs.Empty);
    }
}

Startup.cs(摘录)

public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();

    string connString = Configuration["ConnectionStrings:DefaultConnection"];
    services.AddDbContext<MyContext>(optionsBuilder => optionsBuilder.UseSqlServer(connString), ServiceLifetime.Transient);
    services.AddScoped<ModelService>();
}

ParentPage.razor

@page "/simpleForm"

@using Data
@inject ModelService modelService
@implements IDisposable

@if (AllModels is null)
{
    <p>Loading...</p>
}
else
{
    @foreach (var model in AllModels)
    {
        <label>@model.Msg</label>
    }

    <label>Other view</label>
    <ChildComponent></ChildComponent>

    <button @onclick="(async () => await modelService.CreateAsync(new Model()))">Add</button>
}

@code {
    private Model[] AllModels { get; set; } = null!;
    public bool ShowForm { get; set; } = true;
    private object disposeLock = new object();
    private bool disposed = false;

    public void Dispose()
    {
        lock (disposeLock)
        {
            disposed = true;
            modelService.UnregisterCollectionChangedCallback(CollectionChangedCallback);
        }
    }

    protected override async Task OnInitializedAsync()
    {
        AllModels = await modelService.FindAllAsync();
        modelService.RegisterCollectionChangedCallback(CollectionChangedCallback);
    }

    private void CollectionChangedCallback(object? sender, EventArgs args)
    {
        // Feels dirty that I can't await this without changing the function signature. Adding async
        // will make it unable to be registered as a callback.
        InvokeAsync(async () =>
        {
            AllModels = await modelService.FindAllAsync();

            // Protect against event-handler-invocation race conditions with disposing.
            lock (disposeLock)
            {
                if (!disposed)
                {
                    StateHasChanged();
                }
            }
        });
    }
}

ChildComponent.razor

Copy-paste (for the sake of demonstration) of ParentPage minus the label, ChildComponent, and model-adding button.

注意:我还尝试过尝试将代码块插入组件的 HTML 部分,但这也不起作用,因为我无法使用 在那里等待

我尝试过的可能是个坏主意(但仍然没有避免线程冲突):

    @if (AllModels is null)
    {
        <p><em>Loading...</em></p>
        @Load();
        @*
            Won't compile.
            @((async () => await Load())());
        *@
    }
    else
    {
        ...every else
    }

    @code {
        ...Initialization, callbacks, etc.

        // Note: Have to return _something_ or else the @Load() call won't compile.
        private async Task<string> Load()
        {
            ActiveChargeCodes = await chargeCodeService.FindActiveAsync();
        }
    }

请帮忙。我正在(对我来说)未知的领域进行实验。

最佳答案

由于我目前的情况与您的情况非常相似,因此让我分享一下我的发现。我的问题是“StateHasChanged()”。由于我也在您的代码中看到了该调用,因此以下内容可能有所帮助:

我有一个非常简单的回调处理程序:

    case AEDCallbackType.Edit:
        // show a notification in the UI
        await ShowNotification(new NotificationMessage() { Severity = NotificationSeverity.Success, Summary = "Data Saved", Detail = "", Duration = 3000 });
        // reload entity in local context to update UI
        await dataService.ReloadCheckAfterEdit(_currentEntity.Id);

通知函数执行以下操作:

async Task ShowNotification(NotificationMessage message)
{
    notificationService.Notify(message);
    await InvokeAsync(() => { StateHasChanged(); });
}

重新加载函数执行以下操作:

public async Task ReloadCheckAfterEdit(int id)
{
    Check entity = context.Checks.Find(id);
    await context.Entry(entity).ReloadAsync();
}

问题出在 StateHasChanged() 调用上。它告诉 UI 重新渲染。 UI 由数据网格组件组成。数据网格调用数据服务中的查询,以从数据库中获取数据。 这发生在“ReloadAsync”被调用之前,这是“等待”的。一旦 ReloadAsync 实际执行,它就会在不同的线程中发生,从而导致可怕的“在上一个操作完成之前在此上下文上启动了第二个操作”异常。

我的解决方案是从原来的位置完全删除 StateHasChanged 行,并在其他所有操作完成后调用它一次。不再有并发调用者问题。

祝你好运解决这个问题,我感受到你的痛苦。

关于c# - Blazor 组件 : notify of collection-changed event causing thread collisions,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59742779/

相关文章:

c# - 无法从 ASP.NET Core 2.1 Controller 获取 IdentityUser 数据

c# - 允许用户登录到他们的 Microsoft 帐户。没有 UWP 文档?

Python select.select 泄漏线程

C++ 成员在非原子时更新关键部分内的可见性

c# - 更改 api Controller 答案上的 json 字段名称

c# - taghelpers intellisense 在 Preview5 中不起作用

c# - 如何从匿名委托(delegate)或 lambda 访问安全关键字段?

c# - .NET 网站崩溃

C# 模式搜索

c# - 有关线程和联接用法的基本查询C#