1

I have a private async void Button_Click method in my WPF which runs a very complicated SQL query which can run for several minutes. I wish the user can stop this method by clicking another button. My code is like this:

public partial class MainWindow : Window  
{
  private async void Button_Click(object sender, RoutedEventArgs e)
  {
     string SQL_Query= " a very long and complicated SQL query ... "
     SqlCommand SQL_Query_cmd = new SqlCommand(SQL_Query, conn);
    
     DataTable dt = new DataTable();
     await Task.Run(() => {
     using (SqlDataAdapter a = new SqlDataAdapter(SQL_Query_cmd))
     { a.Fill(dt);}
     });

  }
}

I read about BackgroundWorker in this link How to use WPF Background Worker. But didn't understand how to integrate it into my code. I think, my "filling datatable" code is already asynchronous but I don't know how to stop it. Assume that the button which is going to end this method is called stop_btn and its Click method is called cancelButton_Click.

Please please please write your answer in a post, rather than comments. I will be greatly thankful.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Iraj
  • 319
  • 3
  • 17
  • 1
    You can use [ExecuteReaderAsync_](https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.executereaderasync?view=dotnet-plat-ext-5.0#System_Data_SqlClient_SqlCommand_ExecuteReaderAsync_System_Threading_CancellationToken_) and a `CancellationTokenSource` for this. – Klaus Gütter Mar 12 '21 at 11:24
  • @KlausGütter Thank you Klaus. Can you please write your answer in a post. I have no clue about how to implement what you said. Also I don't have a reader, I show the result in a datatable. I modified the post. – Iraj Mar 12 '21 at 11:33
  • 1
    Does this answer your question? [Fill DataTable asynchronously?](https://stackoverflow.com/questions/49078458/fill-datatable-asynchronously) – Charlieface Mar 12 '21 at 11:53
  • Thank you @Charlieface. Actually my dataTable is already filled asynchronously. what I don't know is how to stop it now. – Iraj Mar 12 '21 at 12:01
  • No it isn't filled asynchronously, the thread that fills it is run asynchronously, that is not the same thing. As @KlausGütter said, and in my link, you need to use the *reader* asynchronously and pass in a `CancellationToken`. When the `CancellationTokenSource` is signalled then `OperationCanceledException` is thrown (you need to make sure to catch that gracefully) – Charlieface Mar 12 '21 at 12:14
  • I thnk this might be a better link https://stackoverflow.com/questions/45105167/populate-c-sharp-datatable-asynchronously – Charlieface Mar 12 '21 at 12:15
  • @Charlieface may I ask you to please kindly write your answer in a post? I really don't understand how to implement what say in theory. I promise to upvote all answers. I will be really thankful. – Iraj Mar 12 '21 at 12:20
  • 1
    The `SqlCommand` has a `Cancel()` method. See https://learn.microsoft.com/en-us/dotnet/api/system.data.sqlclient.sqlcommand.cancel?view=dotnet-plat-ext-5.0 – Marius Bancila Mar 12 '21 at 12:47
  • Possibly related: [How to cancel a Task in await?](https://stackoverflow.com/questions/10134310/how-to-cancel-a-task-in-await) Do you want to stop the database from executing the query, or you want to stop waiting for the completion of the query? In the first case the cancellation may not be instantaneous. In the second case the cancellation will be instantaneous, but the task will keep running in the background, in a fire-and-forget fashion. – Theodor Zoulias Mar 12 '21 at 13:43
  • @TheodorZoulias Thank you. I wish to stop database execution and also stop the thread. Can you write the answer in a post? I will be really really thankful. This comment section is going to be moved to chat area. I will give upvote. It's really important for me and so far I only received theoretic and strategic answers, not how to actually stop it. – Iraj Mar 12 '21 at 14:00
  • read about your question here: https://stackoverflow.com/questions/35610840/how-to-cancel-a-task-using-cancellationtoken – Michael Gabbay Mar 12 '21 at 14:16
  • @MichaelGabbay Thank you. It seems like being relevant to my question but I really don't have any clue how to implement it. Would you please kindly write how it can be adopted to my code in a post? I promise to give upvote. It seems like there's a huge resistance about writing a post which I never understand. I thank you and others anyway. Sadly my problem is unsolved despite many many comments. – Iraj Mar 12 '21 at 14:38
  • "*I wish to stop database execution and also stop the thread.*" What thread? Do you mean the task that is launched with `Task.Run`? This task will be completed immediately after the database execution is completed. Do you want to do both of the options I mentioned earlier? For cancelling the database execution you could try the `SqlCommand.Cancel` method proposed by Marius Bancila. For cancelling the awaiting there are some helper methods [here](https://stackoverflow.com/questions/59243161/is-there-a-way-i-can-cause-a-running-method-to-stop-immediately-with-a-cts-cance/59267214#59267214). – Theodor Zoulias Mar 12 '21 at 14:42
  • @TheodorZoulias Ideally both but mainly I wish to cancel database execution. But I don't know how to implement SqlCommand.Cancel because I'm very inexperience that's why I'm BEGGING to write an answer in a post. Sadly the "do that technique, do this method" won't help me :( – Iraj Mar 12 '21 at 14:47
  • In my opinion, your question has nothing to do with either WPF or task cancellation in general. You simply need a way to cancel `SqlDataAdapter.Fill()`. – Pharaz Fadaei Mar 12 '21 at 15:36
  • 1
    Does this answer your question? [Canceling DataAdapter.Fill()](https://stackoverflow.com/questions/7096069/canceling-dataadapter-fill) – Pharaz Fadaei Mar 12 '21 at 15:38

1 Answers1

1

Here is how you could use the IDbCommand.Cancel method and a CancellationTokenSource, to perform cancellation both on the server side and on the client side.

private IDbCommand _activeSqlCommand;
private CancellationTokenSource _cts;

private async void btnExecute_Click(object sender, RoutedEventArgs e)
{
    // The _activeSqlCommand and _cts should be null here.
    // Otherwise, you may end up with multiple concurrent executions.
    Debug.Assert(_activeSqlCommand == null);
    Debug.Assert(_cts == null);
    var sqlQuery = "A very long and complicated SQL query...";
    var localSqlCommand = new SqlCommand(sqlQuery, _connection);
    var localCts = new CancellationTokenSource();
    _activeSqlCommand = localSqlCommand;
    _cts = localCts;
    btnExecute.IsEnabled = false;
    btnCancel.IsEnabled = true;
    try
    {
        DataTable dataTable = await AsCancelable(Task.Run(() =>
        {
            var dt = new DataTable();
            using (SqlDataAdapter a = new SqlDataAdapter(localSqlCommand))
                a.Fill(dt);
            return dt;
        }, localCts.Token), localCts.Token);
        // Here use the dataTable to update the UI
    }
    catch (OperationCanceledException) { } // Ignore
    catch (SqlException ex) when (ex.ErrorCode == CANCEL_ERROR_CODE) { } // Ignore
    finally
    {
        btnCancel.IsEnabled = false;
        btnExecute.IsEnabled = true;
        // The _activeSqlCommand and _cts should still have the local values here.
        Debug.Assert(_activeSqlCommand == localSqlCommand);
        Debug.Assert(_cts == localCts);
        _activeSqlCommand = null;
        _cts = null;
        localCts.Dispose();
    }
}

private void btnCancel_Click(object sender, RoutedEventArgs e)
{
    _activeSqlCommand?.Cancel();
    _cts?.Cancel();
}

private static Task<T> AsCancelable<T>(Task<T> task,
    CancellationToken cancellationToken)
{
    var cancelable = new Task<T>(() => default, cancellationToken);
    return Task.WhenAny(task, cancelable).Unwrap();
}

You'll have to figure out what kind of exception is thrown by the database server when the execution is canceled, and ignore this exception based on its ErrorCode or some other property.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Thank you very much. I'm working on it to understand. So far I received some error regarding my normal SQL query execution. May I ask you to separate the parts that are relevant to Cancel database execution and comment out the rest. I hope I can at least implement that part. – Iraj Mar 12 '21 at 16:04
  • @Iraj you could comment out the line `_cts?.Cancel();`, so that the `CancellationTokenSource` is not canceled. This way the `btnCancel_Click` will not be as responsive, and may not be responsive at all if the database server decides that the query cannot be canceled for some reason. – Theodor Zoulias Mar 12 '21 at 16:16
  • The problem that I have now is that the command which was supposed to show the information in the datagrid doesn't work. Namely: ```dataGrid1.ItemsSource = dt.DefaultView;``` – Iraj Mar 12 '21 at 16:38
  • @Iraj the `dt` is now internal variable inside the `Task.Run` delegate. For updating the UI you can use the `dataTable` variable. – Theodor Zoulias Mar 12 '21 at 16:53
  • ok, I tried and put ```dataGrid1.ItemsSource = dataTable.DefaultView;``` after ```}, localCts.Token), localCts.Token);``` and the basic SQL query code works, but the stop/cancel button doesn't work. – Iraj Mar 12 '21 at 17:14
  • @Iraj yeah, there is no guarantee that the `_activeSqlCommand?.Cancel()` will do anything. You can see it also on the Management Studio. Some queries take forever, and you press the Cancel button, and nothing happens. – Theodor Zoulias Mar 12 '21 at 17:28
  • @Iraj there is also the possibility that it's not the query execution that takes time, but the filling of the `DataTable` or the updating of the UI. You could rule out this possibility by adding a `TOP(10)` in the query (after the `SELECT`), so that only 10 rows are fetched. – Theodor Zoulias Mar 12 '21 at 17:34
  • I wish to thank you again. Your solution worked. I just had to modify the name of ```btnCancel_Click``` to ```cancelButton_Click``` and also choose a big timeout for the ```localSqlCommand``` But honestly it is still very ambiguous how it works. I understand the basis of the solution but not the details, specially the content of try-catch and the following methods. I would be greatly thankful if you can add comments for each line. Maybe it helps other people who search this question as well. – Iraj Mar 16 '21 at 14:20
  • @Iraj do you find unclear the `AsCancelable` method, or the code in the `finally` block? The `AsCancelable` creates a `Task` "proxy" that will be canceled instantly no mater what, potentially leaving the original task running in the background in a fire-and-forget fashion. Having to use such a technique is a compromise, because it means that your tasks are not behaving well. – Theodor Zoulias Mar 16 '21 at 14:42
  • Actually all part of the solution seems complicated to my mind.```Debug.Assert()``` method and ```Catch``` statements and ```finally``` block and those question marks and content of ```AsCancelable``` method . I don't understand ```cancelable``` variable and ```Task.WhenAny(task, cancelable).Unwrap();``` command as well. – Iraj Mar 16 '21 at 15:02
  • @Iraj the [`Debug.Assert`](https://learn.microsoft.com/en-us/dotnet/api/system.diagnostics.debug.assert) is a mechanism that checks our assumptions about the code we write. These lines are removed when the project is compiled in `Release` mode. They have effect only when we compile in `Debug` mode. In case you find them confusing, you could just remove them. Regarding the `?.`, it is the [null conditional operator](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/operators/member-access-operators#null-conditional-operators--and-). – Theodor Zoulias Mar 16 '21 at 15:16
  • Regarding the [`Task.WhenAny`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.task.whenany) and [`Unwrap`](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskextensions.unwrap), you could check the documentation for the details. These methods may look a bit daunting if your are not familiar with asynchronous programming. I can't really explain them better than the documentation! – Theodor Zoulias Mar 16 '21 at 15:16
  • 1
    Thank you very much – Iraj Mar 16 '21 at 15:28