【问题标题】:Task is not getting cancelled when clicking Cancel button [duplicate]单击“取消”按钮时任务未取消[重复]
【发布时间】:2020-01-10 12:33:02
【问题描述】:

在我的代码中,我在单击 LoadTemplated 按钮时从 excel 下载数据。 我想在用户单击“取消”按钮后立即取消下载。 不知何故,任务在继续时没有被取消。 需要关于我在这里做错了什么的建议。

public CheckListDetailViewModel(IAuditInspectionDataService auditInspectionDataService)
        {
            _auditInspectionDataService = auditInspectionDataService;

            LoadChecklistTemplateCommand = new DelegateCommand(OnLoadTemplate, CanLoadTemplate).ObservesProperty(() => ChecklistItems.Count);
        }

private CancellationTokenSource cts;        
        private async void OnLoadTemplate()
    {
        try
        {
            if (cts != null)
            {
                cts.Cancel();
            }
            else
            {
                cts = new CancellationTokenSource();
                IsBusy = true;
                LoadTemplateButtonValue = "Cancel";

                var items = await Task.Run(() =>
                {
                    if (cts.Token.IsCancellationRequested)
                        cts.Token.ThrowIfCancellationRequested();

                    var checklistItems = _auditInspectionDataService.GetCheckList(InspectionType.InspectionTypeId);

                    return checklistItems;

                }, cts.Token);

                LoadTemplateButtonValue = "Load Template";
                ChecklistItems = new ObservableCollection<ChecklistItem>(items);

            }


        }
        catch (Exception ex)
        {
            IsBusy = false;
            LoadTemplateButtonValue = "Load Template";
            ChecklistItems = null;
            cts = null;
            Debug.WriteLine(ex);
        }
        finally
        {
            IsBusy = false;
            LoadTemplateButtonValue = "Load Template";
            //cts.Dispose();
        }

    }

【问题讨论】:

  • 您的代码有几个问题,但最重要的是 _auditInspectionDataService.GetCheckList 不可取消或您没有传递 CancellationToken
  • 据我了解,Task.Run 中的代码只会在启动时检查是否取消,如果稍后请求取消,则不会实际取消任务。
  • 是的,任务开始后由您来检查是否取消
  • @Sir Rufo 我也尝试过这样做,并在服务中通过了 cancelllationt 令牌,但它仍然不起作用。
  • 顺便说一句:如果您为 cts 分配一个新实例并且任务将检查 cts.Token.IsCancellationRequested,会发生什么?它将在新创建的 CancellationTokenSource 上执行此操作

标签: c# wpf asynchronous mvvm async-await


【解决方案1】:

Task 被取消时,会引发 OperationCanceledException 异常。捕获此异常的 catch 块应该是您在处置已取消的实例之后立即创建 CancellationTokenSource 的新实例的唯一位置。 CancellationTokenSource 只能使用一次(一旦通过调用Cancel()CancellationTokenSource.IsCancellationRequested 设置为true)。

只有在调用CancellationToken.ThrowIfCancellationRequested() 并请求取消时,才能取消Task。这意味着CancellationToken 的观察者负责在每次运行操作允许中止时调用CancellationToken.ThrowIfCancellationRequested(),但应尽可能频繁地调用,以便在请求取消时立即取消,并且始终在分配资源之前取消。

因此,只有将当前CancellationToken 的引用传递给在可取消Task 执行期间调用的每个方法才有意义。还要始终确保所有async 方法都返回TaskTask&lt;T&gt;(除了需要基于event 委托签名返回void 的事件处理程序)。以下示例将按预期工作。

MainWindow.xaml

<Window>
  <Window.DataContext>
    <CheckListDetailViewModel />
  </Window.DataContext>

  <Button>
    <Button.Style>
      <Style>
        <Setter Property="Command" Value="{Binding LoadChecklistTemplateCommand}" />
        <Setter Property="Content" Value="Load Template" />

        <!-- Set the Button's Command to CancelCommand once the download has started -->
        <Style.Triggers>
          <DataTrigger Binding="{Binding IsBusy}" Value="True">
            <Setter Property="Content" Value="Cancel" />
            <Setter Property="Command" Value="{Binding CancelCommand}" />
          </DataTrigger>
        </Style.Triggers>
      </Style>
    </Button.Style>  
  </Button>
</Window>

CheckListDetailViewModel.cs

public CancellationTokenSource CancellationTokenSource { get; set; }  

private bool isBusy;
public bool IsBusy
{ 
  get => this.isBusy;
  set
  {
    this.isBusy = value; 
    OnPropertyChanged();
  }  
}

public ICommand LoadChecklistTemplateCommand { get; set; }
public ICommand CancelDownloadCommand => new DelegateCommand(CancelDownload, () => true);

public CheckListDetailViewModel(IAuditInspectionDataService auditInspectionDataService)
{
  this.CancellationTokenSource = new CancellationTokenSource();

  _auditInspectionDataService = auditInspectionDataService;
  LoadChecklistTemplateCommand = new DelegateCommand(OnLoadTemplate, CanLoadTemplate).ObservesProperty(() => ChecklistItems.Count);
}

// Cancel Task instance
public void CancelDownload()
{
  this.CancellationTokenSource.Cancel();
}

private async Task OnLoadTemplateAsync()
{
  if (this.IsBusy)
  {
    return;
  }

  CancellationToken cancellationToken = this.CancellationTokenSource.Token;
  this.IsBusy = true;

  try
  {
    var items = await Task.Run(() =>
    {
      cancellationToken.ThrowIfCancellationRequested();
      return this._auditInspectionDataService.GetCheckList(cancellationToken, InspectionType.InspectionTypeId);
    }, cancellationToken);

    this.ChecklistItems = new ObservableCollection<ChecklistItem>(items);
  }
  catch (OperationCanceledException) 
  {
    // CancellationTokenSource can only be used once. Therefore dispose and create new instance
    this.CancellationTokenSource.Dispose();
    this.CancellationTokenSource = new CancellationTokenSource();
    this.ChecklistItems = new ObservableCollection<ChecklistItem>();
  }

  this.IsBusy = false;
}

AuditInspectionDataService.cs

// Member of IAuditInspectionDataService 
public void GetCheckList(CancellationToken cancellationToken)
{ 
  using (OleDbConnection connection = new OleDbConnection("Provider=Microsoft.ACE.OLEDB.12.0;Data Source=\"myfile.xls\";Extended Properties=\"Excel 12.0;HDR=YES;IMEX=1\""))
  {
    OleDbCommand command = new OleDbCommand("SELECT * FROM [Sheet1$]", connection);

    connection.Open();
    OleDbDataReader reader = command.ExecuteReader();

    int partitionSize = 50;
    int linesRead = 0;
    while (reader.Read())
    {
      if (++linesRead == partitionSize)
      {
        cancellationToken.ThrowIfCancellationRequested();
        llnesRead = 0;
      }
      Console.WriteLine(reader[0].ToString());
    }
    reader.Close();
  }  
}

【讨论】:

  • 谢谢,BionicCode。你的解释很有道理。另外,我已经浏览了 Peter Duniho 给出的链接。所以看起来问题在于在 GetCheckList 方法中调用的代码。实现调用 OleDbDataAdapter 的 Fill 方法从 excel 中读取数据,一旦调用了适配器的 fill 方法就无法取消。
  • 那只能在读取Excel表格前检查取消。推荐的替代方法是对读取进行分区:读取 N 行。每次读取分区后检查是否取消。要逐行读取 Excel,请检查 OleDbDataReader 或检查 NuGet 以获取 Microsoft.Office.Interop.Excel。 .NET 单独提供了多种访问 Excel 工作表的方法。或者尝试寻找异步 API。
  • 我更新了答案 AuditInspectionDataService.cs 以显示使用 OleDbDataReader 的示例
  • 我会试试你的解决方案。非常感谢代码示例!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-03-01
  • 1970-01-01
  • 2011-12-28
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多