-1

In my code, I download data from excel on clicking LoadTemplated button. I want to cancel the download as soon as user clicks on Cancel button. Somehow the task is not getting Cancelled as it continues. Need advice on what I am doing wrong here.

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

    }
  • 3
    There are several issues with your code, but the most important is, that _auditInspectionDataService.GetCheckList is not cancelable or you did not pass a CancellationToken – Sir Rufo Sep 08 '19 at 13:12
  • 1
    Also as I understood the code inside Task.Run will only check for cancellation on start and will not actually cancel the task if cancellation is requested later. – Vikrant Yadav Sep 08 '19 at 13:13
  • 1
    Yes, it is up to you to check for cancellation once the task has started – Sir Rufo Sep 08 '19 at 13:14
  • @Sir Rufo I tried doing that as well and passed cancelllationt Token in service but it still did not work. – Vikrant Yadav Sep 08 '19 at 13:16
  • BTW: What would happen, if you assign a new instance to cts and the task will check cts.Token.IsCancellationRequested? It will perform this on the new created CancellationTokenSource – Sir Rufo Sep 08 '19 at 13:16
  • @SirRufo my bad..thanks for pointing it out..done the editing but stil it is not working..just trying to figure out what is the best way to keep checking the status of cancellation token inside the Task.Run method till it is running – Vikrant Yadav Sep 08 '19 at 13:29
  • There is no "best way" - it totally depends on the code you execute within the task context. You are the programmer and only you know when and how it is safe to leave the task in case of cancellation. – Sir Rufo Sep 08 '19 at 13:38

1 Answers1

0

When a Task is cancelled an OperationCanceledException exception is thrown. The catch block that catches this exception should be the only place where you create a new instance of CancellationTokenSource, right after you disposed the cancelled instance. The CancellationTokenSource can only be used once (once the CancellationTokenSource.IsCancellationRequested was set to true by calling Cancel()).

The Task can only be cancelled if CancellationToken.ThrowIfCancellationRequested() is called and cancellation was requested. This means the observer of the CancellationToken is responsible to call CancellationToken.ThrowIfCancellationRequested() every time the running operation allows to get aborted but as often as possible, in order to cancel as soon the cancellation was requested and always before allocating resources.

So it only makes sense to pass a reference of the current CancellationToken to every method that is called during the execution of the cancelable Task. Also always make sure that all async methods return a Task or Task<T> (except for event handlers which require to return void based on the event delegate signature). The following example will work as intended.

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
  • 1
  • 4
  • 28
  • 44
  • Thanks, BionicCode. Your explanation makes sense. Also, I have gone through the link Peter Duniho given. So it looks like the issue lies in code which is called inside GetCheckList method. The implementation calls OleDbDataAdapter's Fill method to read data from excel and there is no way to cancel once adaptor's fill method is called. – Vikrant Yadav Sep 10 '19 at 16:58
  • Then you can only check for cancellation before reading the Excel table. Recommended alternative approach is to partition the reading: read N number of rows. After each read partition check for cancellation. To read from Excel by line check [`OleDbDataReader`](https://learn.microsoft.com/en-us/dotnet/api/system.data.oledb.oledbdatareader?view=netframework-4.8#examples) or check NuGet for _Microsoft.Office.Interop.Excel_. .NET alone offers multiple ways to access Excel sheets. Or try to find an asynchronous API. – BionicCode Sep 10 '19 at 19:42
  • I updated the answer _AuditInspectionDataService.cs_ to show an example using the `OleDbDataReader` – BionicCode Sep 10 '19 at 19:43
  • I will try your solution.Thanks a ton for code example! – Vikrant Yadav Sep 11 '19 at 04:32