我正在使用 .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/