第二个事件劫持第一个处理程序
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();
});
}
}
我有一个 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();
});
}
}