简体   繁体   中英

Second Event Hijacks First Handler

I have a Blazor component with two buttons that execute the same underlying service class async method. As that method operates, it iterates on some data for enrichment and emits an event per iteration that the component handles to update a progress bar per button.

I've set the service event handlers up as local functions within the @click event method. This local function handles the target element's counters for progress and the JS interop to update the UI. This all works as expected when clicked individually and allowed to complete before clicking the other button. The problem is that if both buttons are clicked fast enough together, whereby the first operation is still processing, the second event handler hijacks the first and all callbacks flow to it . The first progress bar idles and the second one goes over 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;
}

I think your basic approach here is flawed. Let me explain:

  • Click A
  • Hook up Handler A to Event
  • Call GetDataAsync(A)
  • Some loops happen, but are not complete
  • Click B
  • Hook up Handler B to Event
  • Call GetDataAsync(B)
  • GetDataAsync(B) loops and fires Event
  • Both Handler A and Handler B fires (I don't think you intend that.)....

You are using the same Event Handler for both calls to GetDataAsync . When both GetDataAsync methods are running each handler is getting the events from both.

The sequencing of events gets pretty confusing based on the timing of the second click. Async behaviour is not always an exact science and you may be in edge territory. I think this is caused by the way the Thread Task Scheduler prioritises it's tasks (but that's no more than an educated guess and I may be totally wrong.), I won't go as far as saying there's a bug. but... it needs more investigation.

Update

Here's a simplified version of your code that uses callbacks for isolation:

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

Demo Page:

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM