第二个事件劫持第一个处理程序

Second Event Hijacks First Handler

我有一个 Blazor 组件,其中有两个按钮执行相同的底层服务 class 异步方法。当该方法运行时,它会迭代一些数据以进行充实,并在每次迭代时发出一个事件,组件处理该事件以更新每个按钮的进度条。

我已将服务事件处理程序设置为 @click 事件方法中的本地函数。此本地函数处理目标元素的进度计数器和 JS 互操作以更新 UI。当单独单击并允许在单击另一个按钮之前完成时,这一切都按预期工作。 问题在于,如果同时单击两个按钮的速度足够快,第一个操作仍在处理中,第二个事件处理程序就会劫持第一个,所有回调都会流向它。第一个进度条空闲,第二个进度条超过 100%。

Component.razor

@inject IFooService FooService

<a class="btn btn-light p-1 @(isExcelProcessing ? "disabled" : null) " role="button" @onclick="ExportExcel">
            <i class="fa fa-file-excel-o text-dark fa-2x"></i><br>
            <span class="small text-nowrap">
                Summary Excel
                @if (isExcelProcessing)
                {
                    <div class="progress" style="height: 15px">
                        <div id="excelProgressBar" ></div>
                    </div>
                }
            </span>
        </a>

<a class="btn btn-light @(isTextProcessing ? "disabled" : null) " role="button" @onclick="ExportText">
            <i class="fa fa-2x fa-file-text-o text-dark"></i><br />
            <span class="small text-nowrap">
                Instruction Text
                @if (isTextProcessing)
                {
                    <div class="progress" style="height: 15px">
                        <div id="textProgressBar" ></div>
                    </div>
                }
            </span>
        </a>

@code {

    private bool isExcelProcessing = false;
    private bool isTextProcessing = false;

    private async Task ExportExcel()
    {
        isExcelProcessing = true;
        var excelIssuesCounter = 0;
        var excelProcessingComplete = 0m;

        var itemsToEnrichCount = (await FooService.GetItemsToEnrich()).ToArray();

        void OnFetched(object sender, EventArgs args)
        {            
            excelIssuesCounter++;
            excelProcessingComplete = (Convert.ToDecimal(excelIssuesCounter) / itemsToEnrichCount) * 100;
            JS.InvokeVoidAsync("updateProgress",
                "excelProgressBar",
                excelProcessingComplete);
        }
        
        FooService.OnFetched += OnFetched;

        // this method has the iterations that call back to the defined event handler
        var fooData = await FooService.GetDataAsync("excel");

        //  do other stuff like building a model, 
        //  render a template from razor engine
        //  download the file        

        isExcelProcessing = false;
        FooService.OnFetched -= OnIssueFetched;
    }

    private async Task ExportText()
    {
        isTextProcessing = true;
        var textIssuesCounter = 0;
        var textProcessingComplete = 0m;

        var itemsToEnrichCount = (await FooService.GetItemsToEnrich()).ToArray();

        void OnFetched(object sender, EventArgs args)
        {            
            textIssuesCounter++;
            textProcessingComplete = (Convert.ToDecimal(textIssuesCounter) / itemsToEnrichCount) * 100;
            JS.InvokeVoidAsync("updateProgress",
                "textProgressBar",
                textProcessingComplete);
        }
        
        FooService.OnFetched += OnFetched;

        // this method has the iterations that call back to the defined event handler
        var fooData = await FooService.GetDataAsync("text");

        //  do other stuff like building a model, 
        //  render a template from razor engine
        //  download the file        

        isTextProcessing = false;
        FooService.OnFetched -= OnFetched;
    }
}

FooService.cs

public event EventHandler OnFetched;

public async Task<FooData> GetDataAsync()
{
    // not material to the problem. just here for example
    var dataToEnrich = GetRawData();
    var result = new FooData();    

    foreach(var itemToEnrich in dataToEnrich)
    {
        var enrichedFoo = await EnrichAsync(itemToEnrich);
        result.EnrichedItems.Add(enrichedFoo);

        // this is the material operation that doesn't always go to the desired handler
        OnFetched?.Invoke(this, null);
    }  

    return result;
}

我认为你这里的基本方法是有缺陷的。让我解释一下:

  • 点击 A
  • 将处理程序 A 连接到事件
  • 调用 GetDataAsync(A)
  • 发生了一些循环,但并不完整
  • 点击 B
  • 将处理程序 B 连接到事件
  • 调用 GetDataAsync(B)
  • GetDataAsync(B) 循环并触发事件
  • 处理程序 A 和处理程序 B 都触发(我认为你不是故意的!) ....

您对 GetDataAsync 的两次调用使用相同的事件处理程序。当两个 GetDataAsync 方法都是 运行 时,每个处理程序都从两者获取事件。

根据第二次点击的时间,事件的顺序变得相当混乱。异步行为并不总是一门精确的科学,您可能处于边缘地带。我认为这是由 Thread Task Scheduler 确定其任务优先级的方式引起的(但这只是一个有根据的猜测,我可能完全错了!)。我不会说存在错误,但是...需要更多调查。

更新

这是使用回调进行隔离的代码的简化版本:

public class MyService
{
    public async Task<SomeData> GetDataAsync(Action<SomeData> callback)
    {
        var result = new SomeData();

        foreach (var item in data)
        {
            await Task.Delay(1000);
            item.Value = $"updated at {DateTime.Now.ToLongTimeString()}";

            callback.Invoke(result);
        }

        return result;
    }

    private List<SomeData> data => new List<SomeData> {
       new SomeData { Key = "France" },
       new SomeData { Key = "Portugal" },
       new SomeData { Key = "Spain" },
   };
}

public class SomeData
{
    public string Key { get; set; } = String.Empty;

    public string Value { get; set; } = String.Empty ;
}

演示页面:

@page "/"
@inject MyService myService

<div class="p-2">
    <button class="btn btn-primary" @onclick=FirstClicked>First</button>
    <div class="p-2">
        Counter = @firstCounter;
    </div>
</div>
<div class="p-2">
    <button class="btn btn-info" @onclick=SecondClicked>First</button>
    <div class="p-2">
        Counter = @secondCounter;
    </div>
</div>

@code {
    int firstCounter = 0;
    int secondCounter = 0;

    private async void FirstClicked()
    {
        firstCounter = 0;
        void firstHandler(SomeData data)
        {
            firstCounter++;
            this.StateHasChanged();
        }
        var ret = await this.myService.GetDataAsync(firstHandler);
    }


    private async void SecondClicked()
    {
        secondCounter = 0;
        var ret = await this.myService.GetDataAsync((data) =>
          {
              secondCounter++;
              this.StateHasChanged();
          });
    }
}