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



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

我已经将服务事件处理程序设置为@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到事件
  • 调用GetDataAsync (A)
  • 一些循环发生,但不完整
  • B
  • 点击
  • 将处理程序B连接到事件
  • 调用GetDataAsync (B)
  • getdatasync (B)循环并触发Event
  • 处理程序A和处理程序B都触发(我不认为你想这样!)…

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

基于第二次点击的时间,事件的顺序变得非常混乱。异步行为并不总是一门精确的科学,你可能处于边缘地带。我认为这是由线程任务调度程序优先处理任务的方式引起的(但这只不过是一个有根据的猜测,我可能完全错了!)。我不会说有漏洞,但是…这需要更多的调查。

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

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

最新更新