Компоненты Blazor: уведомление об изменении коллекции, вызывающей конфликты потоков - PullRequest
0 голосов
/ 15 января 2020

Я работаю над ASP. NET приложением Core Blazor с. Net Core 3.0 (я знаю 3.1, но из-за Морды c я пока застрял с этой версией).

У меня есть многокомпонентная страница, и некоторым из этих компонентов требуется доступ к одним и тем же данным, и все они должны обновляться при обновлении коллекции. Я пытался использовать обратные вызовы на основе EventHandler, но они запускаются в своих собственных потоках примерно в одно и то же время (если я правильно понимаю ), в результате чего обратные вызовы в компонентах .razor пытаются сделать служба вызывает контекст одновременно.

Примечание: я пытался сделать переходный период жизни моего DbContext, но я все еще получаю условия гонки.

Вполне возможно, что я попал в асиновый c блендер и не знаю, как выбраться.

Я предварительно пришел к выводу, что методология event EventHandler здесь не сработает. Мне нужен какой-то способ для запуска обновлений «измененных наборов» компонентов без запуска условия гонки.

Я думал об обновлении служб, участвующих в этих условиях гонки, следующим образом:

  • Заменить каждую функцию поиска общедоступным свойством коллекции
  • При каждом вызове создания / обновления / удаления обновлять каждую из этих коллекций

Это будет разрешить компонентам связываться непосредственно с измененными коллекциями, что, как я думаю, заставит обновляться каждую привязку к нему в любом компоненте без необходимости явного уведомления, а это, в свою очередь, позволит мне отказаться обработка события «коллекция изменена» полностью.

Но я не решаюсь попробовать это и еще не сделал , потому что это приведет к значительным накладным расходам на каждую основную сервисную функцию.

Другие идеи? Пожалуйста, помогите. Если коллекция изменилась, я хочу, чтобы компоненты Blazor, которые полагаются на эту коллекцию, каким-либо образом могли обновляться, будь то с помощью уведомлений или связывания, или каким-либо другим способом.

Следующий код представляет собой значительное упрощение того, что я получил, и это все еще вызывает условия гонки, когда обработчики событий вызываются из службы.

Model

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

MyContext

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; }
}

ModelService

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 компонента, но это тоже не сработало, поскольку я не могу использовать await там.

Возможно плохая идея, с которой я экспериментировал (и которая до сих пор не избежала коллизии потоков):

    @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();
        }
    }

Пожалуйста, помогите. Я экспериментирую на (для меня) неизведанной территории.

1 Ответ

0 голосов
/ 19 марта 2020

Поскольку я сейчас нахожусь в ситуации, очень похожей на вашу, позвольте мне поделиться тем, что я узнал. Моя проблема была "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.ReloadMeasureAfterEdit(_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 (). Это говорит пользовательскому интерфейсу повторно сделать. Пользовательский интерфейс состоит из компонента сетки данных. Сетка данных вызывает запрос в службе данных для извлечения данных из БД. Это происходит как раз перед вызовом «ReloadAsyn c», что «ожидается». Как только ReloadAsyn c фактически выполняется, это происходит в другом потоке, вызывая страшное исключение «Вторая операция запущена в этом контексте до завершения предыдущей операции».

Мое решение состояло в том, чтобы полностью удалить строку StateHasChanged из где это было, и назовите это однажды после того, как все остальное было закончено. Больше нет проблем с вызывающим абонентом.

Удачи в решении этой проблемы, я чувствую вашу боль.

...