c# - CancellationToken 永远不会取消我长时间运行的加载数据功能
问题描述
我有一个 Blazor 组件,它必须显示来自长时间运行的操作的数据。出于这个原因,我显示了一个微调器,但是由于它需要很长时间,因此我希望能够在例如用户导航离开时取消此加载(例如,用户在加载数据时单击登录)。
我在组件中使用 CancellationTokenSource 对象实现了 Dispose 模式,我使用 Token 作为参数使我的函数异步,但似乎在我的加载数据函数中,令牌的“IsCanceled”从未设置为 true,也不会引发 OperationCanceledException。如果我使用一个仅等待 Task.Delay 20 秒的虚拟函数进行测试,并且我通过了令牌,则它被正确取消。我究竟做错了什么?
最终结果是,当数据正在加载并且微调器正在显示时,如果用户单击按钮以导航离开,它会等待数据加载完成。
我显示数据的视图;“LoadingBox”在未创建列表时显示微调器。
<Card>
<CardHeader><h3>Ultime offerte</h3></CardHeader>
<CardBody>
<div class="overflow-auto" style="max-height: 550px;">
<div class="@(offersAreLoading ?"text-danger":"text-info")">LOADING: @offersAreLoading</div>
<LoadingBox IsLoading="lastOffers == null">
@if (lastOffers != null)
{
@if (lastOffers.Count == 0)
{
<em>Non sono presenti offerte.</em>
}
<div class="list-group list-group-flush">
@foreach (var off in lastOffers)
{
<div class="list-group-item list-group-item-action flex-column align-items-start">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1">
<a href="@(NavigationManager.BaseUri)offerta/@off.Oarti">
@off.CodiceOfferta4Humans-@off.Versione
</a>
</h5>
<small>@((int) ((DateTime.Now - off.Created).TotalDays)) giorni fa</small>
</div>
<p class="mb-1"><em>@(off.OggettoOfferta.Length > 50 ? off.OggettoOfferta.Substring(0, 50) + "..." : off.OggettoOfferta)</em></p>
<small>@off?.Redattore?.Username - @off.Created</small>
</div>
}
</div>
}
</LoadingBox>
</div>
</CardBody>
</Card>
组件代码隐藏。在这里,我调用了长时间运行的函数 ( GetRecentAsync ),当用户离开或执行其他操作时,我想取消该函数:
public partial class Test : IDisposable
{
private CancellationTokenSource cts = new();
private IList<CommercialOffer> lastOffers;
private bool offersAreLoading;
[Inject] public CommercialOfferService CommercialOfferService { get; set; }
async Task LoadLastOffers()
{
offersAreLoading = true;
await InvokeAsync(StateHasChanged);
var lo = await CommercialOfferService.GetRecentAsync(cancellationToken: cts.Token);
lastOffers = lo;
offersAreLoading = false;
await InvokeAsync(StateHasChanged);
}
async Task fakeLoad()
{
offersAreLoading = true;
await InvokeAsync(StateHasChanged);
await Task.Delay(TimeSpan.FromSeconds(20), cts.Token);
offersAreLoading = false;
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await LoadLastOffers();
}
await base.OnAfterRenderAsync(firstRender);
}
public void Dispose()
{
cts.Cancel();
cts.Dispose();
}
}
public async Task<List<CommercialOffer>> GetRecentAsync(CancellationToken cancellationToken)
{
try
{
cancellationToken.ThrowIfCancellationRequested();
var result = await _cache.GetOrCreateAsync<List<CommercialOffer>>("recentOffers", async entry =>
{
entry.AbsoluteExpiration = DateTimeOffset.Now.Add(new TimeSpan(0, 0, 0, 30));
var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync();
foreach (var commercialOffer in list)
{
// sta operazione è pesante, per questo ho dovuto cachare
// BOTH ISCANCELLATIONREQUESTED AND THROWIFCANCELLATINREQUESTED DOES NOT WORK, ISCANCELLATIONREQUESTED IS ALWAYS FALSE.
cancellationToken.ThrowIfCancellationRequested();
if (cancellationToken.IsCancellationRequested) return new List<CommercialOffer>();
await _populateOfferUsersAsync(commercialOffer);
}
return list.Take(15).OrderByDescending(o => o.Oarti).ToList();
});
return result;
}
catch (OperationCanceledException)
{
// HERE I SET A BREAKPOINT IN ORDER TO SEE IF IT RUNS, BUT IT DOESN'T WORK
}
}
谢谢!
编辑 20/07/2021
谢谢@Henk Holterman。 GetRecentAsync获取所有最近的商业报价,由一个简单的表格编译,并有一些数据作为通常的用例。这些商业报价中的每一个都涉及 4 个用户(管理报价、上级、批准者等),并且我为每个要显示的商业报价填充了每个这些用户的 foreach 循环。
我知道我应该从一开始就从 SQL 查询创建整个实体(商业报价),但我需要这个来解决顺序和关注点分离的问题。
因此,_populateOfferUsersAsync(commercialOffer)查询某个商品的 4 个用户,创建这 4 个实体并将它们分配给该商品:
private async Task _populateOfferUsersAsync(CommercialOffer commercialOffer)
{
commercialOffer.Responsabile = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdResponsabile);
commercialOffer.Redattore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdRedattore);
commercialOffer.Approvatore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdApprovatore);
commercialOffer.Revisore = await _unitOfWork.UserRepository.GetByIdAsync(commercialOffer.IdRevisore);
}
在后台,我使用 Dapper 进行数据库查询:
public async Task<User> GetByIdAsync(long id)
{
var queryBuilder = _dbTransaction.Connection.QueryBuilder($@"SELECT * FROM GEUTENTI /**where**/");
queryBuilder.Where($"CUSER = {id}");
queryBuilder.Where($"BSTOR = 'A'");
queryBuilder.Where($"BDELE = 'S'");
var users = await queryBuilder.QueryAsync<User>(_dbTransaction);
return users.FirstOrDefault();
}
从我所见,没有简单有效的方法来传递 CancellationToken 来停止 Dapper 查询,可能是我或 Dapper 不适合这些东西
解决方案
我究竟做错了什么?
将您的取消令牌转发给所有异步 I/O 方法很重要:
// var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync();
var list = await _unitOfWork.CommercialOfferRepository.GetAllWithOptionsAsync(cancellationToken);
然后当然进行相应的修改GetAllWithOptionsAsync()
。实体框架中的所有异步方法都有一个接受CancellationToken
.
...导航离开它等待数据加载完成。
当 GetAllWithOptionsAsync() 占用大部分时间时,这就是数字。下一个 foreach 循环应该在取消时中断,但这可能并不明显。
尽管如此,_populateOfferUsersAsync(commercialOffer)
还应该将 CancellationToken 作为参数。
从您自己的 FakeLoad() 中可以看出,Blazor 和 CancellationTokenSource 没有损坏。
推荐阅读
- c# - 在 foreach 循环中迭代 IQueryable
- android - 从 recyclerview 中的 firebase 实时数据库访问数据
- javascript - 滚动时转到顶部按钮完全消失
- android-studio - 选中单选按钮或复选框时,计算价格
- sas - 线性回归:使用 SAS 查找重要的类变量
- ios - 操作无法完成。(Starscream.WSError 错误 1。) RBSManager
- jquery-select2 - 如何在 Select2 APEX 插件中设置预选值?
- c# - 有没有一种简单的方法来确定当前方法在哪个线程上运行?
- python - Python:如何计算移动平均交叉的数量+每个交叉之间的最高点?
- powershell - 将 $args 变量传递给函数