C# How to cancel previous button click event handlers in WPF - c#

I have a WPF application that fetches data from a database and displays information in the form a table. There is a refresh button that can be clicked and it updates the table. When the user clicks on the refresh button repeatedly the application hangs for a couple seconds.
I tried experimenting with async and await but it is still locking up.
Before:
public void Refresh_Handler(object sender, System.Windows.RoutedEventArgs e)
{
this.Refresh(); //asynchronous method that has several nested awaits
...
}
After:
var task = Task.Run(() =>
{
this.Refresh();
});
async void runTask()
{
this.cts = new CancellationTokenSource();
try
{
await task();
}
catch (OperationCanceledException ex)
{
Console.WriteLine($"{nameof(OperationCanceledException)} thrown with message: {ex.Message}");
}
finally
{
this.cts = null;
}
}
if (this.cts == null)
{
runTask();
}
else
{
this.cts.Cancel();
runTask();
}
How can I cancel previous/pending refresh clicks and make the UI responsive? Currently it is stacking the requests and locking the application when the refresh button is pressed repeatedly. I would like it to kick off a brand new request on the final button click so that the app doesn't hang.

Related

C# .netcore WPF Displaying window with details of background process failure

I have a WPF application. A window launches and asks the user to select a launch mode, "Demo" or "Live".
If they click "Live" then an indeterminate progress bar comes up and the application will run a mixture of synchronous and asynchronous tasks which acquire data from a COM+ service while the progress bar is animated.
if the requests for data are unsuccessful a window comes up and the window with the progress bar remains on screen. The unsuccessful window shows high level failure details and gives two options: retry, quit.
the problem is that data calls are blocking the UI so even though the progressbar is set to visible, it is not actually visible until the button click event has returned (either by the data being loaded successfully or not).
If i want to run my data requests in Tasks then the UI is unblocked and the button click event is returned, the progress bar is visible and animated but if the data request fails I have no way to launch the unsucessfull window. I cannot use exceptions thrown in the data request thread to notify the main thread without awaiting the data request Task but cannot await the task as it will freeze the UI thread and not show a responsive UI while data is loaded.
I clearly have an antipattern. What is a good way to display a window when a background task fails without blocking the UI thread?
App.xaml.cs
private void Application_Startup(object sender, StartupEventArgs e)
{
launch.ShowDialog(); //this window has two buttons for live or demo launch. on selection itll show an indeterminate progressbar
loadingWindow.ShowDialog(); //this shows the window but doesnt stop exectution to the next line
}
Launch.xaml.cs
private void btnDemo_click(object sender, EventArgs e)
{
ProgressBarVisibility = Visibility.Visible; //ProgressBarVisibility prop is model bound to the progress bars visibility
// Visibility is not updated yet because the UI is currently blocked by the lines below...
try
{
LoadConfigs();
}
catch
{
Close(ExitReasons.CONFIGLOADERROR);
}
// this is where the data requests might fail and i need to
//open a new window that says which task failed (getUser or getRecord etc...) and give the
//user an option to return or close. If i make this task async then i unblock the UI but
//cannot try catch any of the errors thrown in the call chain withouth awaiting the call
//which blocks the UI....
PreCacheEHRData();
LaunchMainApp(); // if the data loads successfully the launch window is closed
//an a new window with the primary functionality is launched.
}
Thanks for your help
Call any long-running operation on a background thread by creating an awaitable Task, e.g.:
private async void btnDemo_click(object sender, EventArgs e)
{
ProgressBarVisibility = Visibility.Visible;
try
{
await Task.Run(() => LoadConfigs());
}
catch
{
Close(ExitReasons.CONFIGLOADERROR);
}
await Task.Run(() => PreCacheEHRData());
LaunchMainApp();
}
Note that the methods that run on a background thread, i.e. LoadConfigs and PreCacheEHRData in the sample code above, cannot access any UI element.
You should do the I/O work on an awaited task. Wrap try/catch block with Task.Run, and do the UI calls inside the catch block on Application's dispatcher.
make btnDemo_click async void.
private async void btnDemo_click(object sender, EventArgs e)
{
ProgressBarVisibility = Visibility.Visible;
var isDataLoadedSuccessfully = await Task.Run(() =>
{
try
{
LoadConfigs();
return true;
}
catch
{
// Application.Current.Dispatcher.Invoke(() =>
Dispatcher.Invoke(() =>
{
// here you can do UI-related calls..
Close(ExitReasons.CONFIGLOADERROR);
});
return false;
}
});
// next code will be on UI thread
ProgressBarVisibility = Visibility.Hidden;
if (isDataLoadedSuccessfully)
{
// await Task.Run(() => PreCacheEHRData());
PreCacheEHRData();
LaunchMainApp();
}
else
{
// do something
}
}

Dynamically swapping out or toggling visibility of controls

I have a TreeView in a form, that is dock-filled to a groupbox. The problem to solve is, that there is an operation that is run on a Task, which loads data from a server application. When this is running, there should be a progress indicator displayed in the location of the TreeView. That is, it should be shown instead of the TreeView, and take its place fully. The following is what the code for this looks like:
private async void preload_data(object sender, System.EventArgs args)
{
try
{
// the treeView should be disabled/invisible at this point
// make the CircularProgressBar enabled/visible
// get the information from the server
await Configuration.network.fetch_stuff();
}
catch (Exception ex)
{
// something bad happened
}
finally
{
// whatever happened, the treeView should be back
}
}
The CircularProgressBar (a third-party control) should appear as in the code above, and should replace the TreeView. It should fill the exact same space as the TreeView would, which is dock-filled. Below is a screenshot of this:
This form and all its controls have been designed in the designer, and I don't want to do it there, I want to do it programmatically. What is the best way to go about this? I have looked at examples of Controls.Remove() and Controls.Add(), but it's not clear if that fits this purpose.
It is quite common to change the visual output while actions are running, like you do. Think of disabling buttons, to discourage operators to press the button again, or show something visually, to inform operators about the progress.
For simplicity, without the try-catch
private async Task PreloadDataAsync()
{
this.ShowFetchingData(true);
// start fetching data, do not await:
var taskFetchData = Configuration.network.fetch_stuff();
// while taskFetchData not completed, await some time
TimeSpan updateTime = TimeSpan.FromSeconds(0.250);
int progressCounter = 0;
while (!taskFetchData.IsCompleted)
{
this.ShowProgress(progressCounter);
var taskWait = Task.Delay(updateTime);
await Task.WhenAny(new Task[] {taskFetchData, taskWait};
// either taskFetchData.IsCompleted, or Delay time waited
++progressCounter;
}
this.ShowFetchingData(false);
}
private void ShowFetchindData(bool show)
{
// disable/enable certain buttons, menu items, show progressbar?
this.ButtonFetchData.Enabled = !show;
this.MenuFetchData.Enabled = !show;
this.ProgressBarFetchData.Visible = show;
}
private bool IsFetchingData => this.ProgressBarFetchData.Visible;
private void ShowProgress(int progress)
{
this.ProgressBarFetchData.Position = progress;
}
For simplicity, I've left out checks for the position in the progress bar, but you get the gist.
Usage:
private async void OnButtonFetchData(object sender, EventArgs e)
{
await this.PreloadDataAsync();
}
Room for improvement
The problem with this is that there is no timeout at all: if FetchStuff does not complete, you are in an endless wait. The method that microsoft proposes is the use of a CancellationToken. Almost every async method has an overload with a CancellationToken. Consider creating one yourself:
// existing method:
private async Task<MyData> fetch_Stuff()
{
await this.fetch_stuff(CancellationToken.None);
}
// added method with CancellationToken
private async Task<MyData> fetch_Stuff(CancellationToken token)
{
// Call async function overloads with the token,
// Regularly check if cancellation requested
while (!token.IsCancellationRequested)
{
... // fetch some more data, without waiting too long
}
}
Instead of IsCancellationRequested, consider to throw an exception: ThrowIfCancellationRequested.
Usage:
private async Task PreloadDataAsync()
{
// preloading should be finished within 30 seconds
// let the cancellationTokenSource request cancel after 30 seconds
TimeSpan maxPreloadTime = TimeSpan.FromSeconds(30);
using (var cancellationTokenSource = new CancellationTokenSource(maxPreloadTime))
{
await PreloadDataAsync(cancellationTokenSource.Token);
}
}
The overload with CancellationToken:
private async Task PreloadDataAsync(CancellationToken token)
{
this.ShowFetchingData(true);
// execute code similar to above, use overloads that accept token:
try
{
var taskFetchData = Configuration.network.fetch_stuff(token);
TimeSpan updateTime = TimeSpan.FromSeconds(0.250);
int progressCounter = 0;
while (!taskFetchData.IsCompleted)
{
token.ThrowIfCancellationRequested();
this.ShowProgress(progressCounter);
var taskWait = Task.Delay(updateTime, token);
await Task.WhenAny(new Task[] {taskFetchData, taskWait};
// either taskFetchData.IsCompleted, or Delay time waited
++progressCounter;
}
}
catch (TaskCancelledException exc)
{
this.ReportPreloadTimeout();
}
finally
{
this.ShowFetchingData(false);
}
}
Or if you want a button that cancels the task:
private CancellationTokenSource cancellationTokenSource = null;
private book IsPreloading => this.CancellationTokenSource != null;
private async Task StartStopPreload()
{
if (!this.IsPreloading)
StartPreload();
else
CancelPreload();
}
private async Task StartPreload()
{
// preload not started yet; start it without timeout;
try
{
this.cancellationTokenSource = new CancellationTokenSource();
await PreloadDataAsync(this.cancellationTokenSource.Token);
}
catch (TaskCancelledException exc)
{
this.ReportPreloadCancelled();
}
finally
{
this.cancellationTokenSource.Dispose();
this.cancellationTokenSource = null;
}
}
}
The method where operators can stop preloading:
private async void StopPreload()
{
this.cancellationTokenSource.Cancel();
// the method that created this source will Dispose it and assign null
}
All you have to do is create buttons / menu items to start / stop preloading
Solved using Visible properties of controls.

How to put a cancel button when connecting

I make a connection in C# with a login and a password and I would like
put a button to cancel the connection to the database if it gets too much
long.
I would like to know how to do it and put it in a thread if possible.
Here is the code:
private void btncon_Click(object sender, EventArgs e)
{
string strLogin = tblogin.Text.Trim();
string pass = tbpwd.Text;
bool success = false;
if (String.IsNullOrWhiteSpace(strLogin) || String.IsNullOrWhiteSpace(pass))
{
MessageBox.Show("Veuillez remplir tous les champs SVP ");
}
else if(String.IsNullOrEmpty(strLogin) || String.IsNullOrEmpty(pass)){
MessageBox.Show("Veuillez remplir tous les champs SVP ");
}
else
{
model.Connexion cm = new model.Connexion();
pass = Snippets.SHA1Util.SHA1HashStringForUTF8String(pass).ToString();
string[] user = cm.login(strLogin, pass);
if(user[0] != null)
{
Int32.TryParse(user[0], out iduser);
Int32.TryParse(user[1], out idGrp);
Int32.TryParse(user[2], out idbtq);
nom = user[3];
if (idGrp != 3)
success = true;
else
{
MessageBox.Show("Accès Non autorisé , Veuillez contacter l'administrateur");
success = false;
}
}
else
{
MessageBox.Show("Email ou mot de passe incorrect.");
success = false;
}
}
if (success)
{
main Principale = new main();
Principale.Show();
Hide();`
}
}
Problem is that when user clicks the Cancel button, there is no way to cancel the cm.login() method gracefully while it's executing. You could use Thread.Abort() to terminate login forcefully, but it's unsafe and strongly discouraged, because making it right would require execute code in another AppDomain and would make the code very complicated.
Fortunatelly, you can still implement Cancel button, if the following conditions are true:
It is safe to call cm.login() on another thread
Letting cm.login() finish in the background (on another thread) after user clicks on the Cancel button does not have undesired effects.
This code also assumes that it is an Winform app (but solution for WPF app would be very similar). It also assumes that main form has a button btnCancel that is hidden (Visible=false) and it's Click event handler is set to btnCancel_Click method.
TaskCompletionSource<Object> CancelLoginTcs = new TaskCompletionSource<Object>();
// Make button click handler method async
private async void btncon_Click(object sender, EventArgs e)
{
try
{
// Make Cancel button visible, so that user can click on it
btnCancel.Visible = true;
// Prepare everything needed to start login
//var strLogin = ...;
//var pass = ...;
//model.Connexion cm = new model.Connexion();
// ...
// Start login on another thread
var loginTask = Task<string[]>.Run(() => cm.login(strLogin, pass));
// Create task that is used to wake-up main thread, when user clicks
// on the Cancel button before login finishes.
CancelLoginTcs = new TaskCompletionSource<Object>();
// Wait for login task or cancel task to complete, whichever finishes first
await Task.WhenAny(loginTask, CancelLoginTcs.Task);
if (CancelLoginTcs.Task.IsCanceled)
{
// User clicked on the Cancel button.
// Login method will be running in the background, until it
// finishes. This assumes that it is safe to do so.
// Here you should do neccessary clean-up, inform user, etc.
// ...
}
else
{
// Login finished and user did NOT click on the Cancel button.
try
{
// Simply read result of login. If an Exception occured during login,
// it will be rethrow now, so you should handle it appropriatelly
var user = loginTask.Result;
// Here program continues in a usual way
// ...
}
catch(Exception E)
{
// Handle login exception
// ...
}
}
}
finally
{
// Hide Cancel button again
btnCancel.Visible = false;
CancelLoginTcs = null;
}
}
private void btnCancel_Click(object sender, EventArgs e)
{
// Set cancel task to cancelled state.
// This will wake-up main thread and let it continue
CancelLoginTcs.SetCanceled();
}

How to Show a MessageDialog while executing a process in background and close it after finished

I'm creating a simple desktop application using Gtk#, When the user clicks a button I want to show a "loading indicator" MessageDialog and make some processing in background, when the process is finished close the dialog and update some controls from the UI.
I'm very new to Gtk# and Mono, so my code looks like this:
protected void OnBtnClicked(object sender, EventArgs e)
{
try
{
Task.Factory.StartNew(() =>
{
var dlg = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.None, "Processing...");
dlg.Run();
//Some sync calls to remote services
//...
//The process finished so close the Dialog
dlg.Destroy();
//Here: Update the UI with remote service's response
//txtResult.Buffer.Text = result.Message;
});
}
catch (Exception ex)
{
var dlg = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Close, ex.Message);
dlg.Title = "Error";
dlg.Run();
dlg.Destroy();
}
}
This code is showing the MessageDialog but it never closes.
Mono version: 4.4.2
IDE: Xamarin Studio Community Edition 6.0.2
Gtk# version: 2.12.38
After reading a guide for responsive Mono applications and asking to Miguel de Icaza through Twitter I've found the way of doing this.
Things to take into account:
1) Never create or try to modify UI elements from another thread.
2) If you need to modify UI controls from another thread use the Application.Invoke() method inside that thread.
3) The Run() method from MessageDialog class waits for user interaction to get closed i.e Click the close button or something that calls a Close/Destroy event. The use of that method is wrong in this scenario because I will close the MessageDialog from my code, so the right method to show the dialog is Show().
With that in my mind, my final code looks like this:
protected void OnBtnClicked(object sender, EventArgs e)
{
try
{
var mdCalculate = new MessageDialog(this, DialogFlags.Modal, MessageType.Info, ButtonsType.None, "Processing...");
mdCalculate.Title = "Calculate";
mdCalculate.Show();
Task.Factory.StartNew(() =>
{
//Some sync calls to remote services
//...
//returns the data I will show in the UI, lets say it's a string
return someData;
}).ContinueWith((prevTask) =>
{
Application.Invoke((send, evnt) =>
{
txtResult.Buffer.Text = prevTask.Result; //this is the string I returned before (someData)
mdCalculate.Hide();
mdCalculate.Destroy();
});
});
}
catch (Exception ex)
{
var dlg = new MessageDialog(this, DialogFlags.Modal, MessageType.Error, ButtonsType.Close, ex.Message);
dlg.Title = "Error";
dlg.Run();
dlg.Destroy();
}
}
Demo:

How to get update from BackgroundWorker.DoWork Event

In my login window, when I click the login button, the configuration and login processes will be executed and those methods are in another class file. So far what I've achieved is when I clicked the login button, the loading animation will be displayed on top of the login window and those processes will be executed as well. There are some login error checking in the configuration class file, so when login error caught, a message box with relevant info will be prompted out and stop the login process, the problem is the message box will not be prompted out since I put those config and login process in the BackgroundWorker.DoWork event.
Here's the codes for Login.xaml.cs:
private void LoginBtn_Click(object sender, RoutedEventArgs e)
{
Loading.Visibility = Visibility.Visible; //The loading animation
Loading.Visibility = Visibility.Visible;
Cursor = Cursors.Wait;
bw.DoWork += new DoWorkEventHandler(LoginInfoVerification);
bw.RunWorkerCompleted += new RunWorkerCompletedEventHandler(RunWorkerCompleted);
bw.WorkerReportsProgress = true;
bw.RunWorkerAsync();
}
private void LoginInfoVerification(object sender, DoWorkEventArgs e) {
var loginInfoVerification = config.ServerConnection(loginInfo.userName,
loginInfo.galPassword, loginInfo.place,
loginInfo.host, loginInfo.port,
loginInfo.application);
}
private void RunWorkerCompleted(object sender, RunWorkerCompletedEventArgs e)
{
if (GlobalVariables.loginSuccess == true)
{
//Pass these variables to main window
var mainWindow = new MainWindow(loginInfo.userName, loginInfo.place, loginInfo.host, loginInfo.port,
loginInfo.application);
mainWindow.Show();
this.Close();
}
else
Loading.Visibility = Visibility.Collapsed;
Cursor = Cursors.Arrow;
}
For Configuration.cs:
public Configuration ConfigServerConnection(string loginUserName, string loginPassword, string loginPlace, string loginHost, int loginPort, string loginApplication)
{
//Server connection
//Login info verification
//If login error caught, prompt message box, different errors, different message
}
Is there any better suggestion in handling UI update and process update at the same time for my case? Please help.
To display the message box you need to switch back to the UI thread with Dispatcher.Invoke, see this.
Application.Current.Dispatcher.Invoke(() => /* show appropriate message box */);
Alternatively if you are using .NET 4.5 or higher you can make your life a lot easier with async-await by marking LoginBtn_Click with the async keyword and then awaiting the log in process.
If there is an asynchronous version of ServerConnection that returns a task you can just await that otherwise you can use Task.Run() to execute ServerConnection on a thread pool thread.
Await will kick off the log in operation asynchronously and once complete will resume the rest of the method on the GUI thread so you can manipulate GUI components without using Dispatcher.Invoke.
private async void LoginBtn_Click(object sender, RoutedEventArgs e)
{
Loading.Visibility = Visibility.Visible; //The loading animation
Loading.Visibility = Visibility.Visible;
Cursor = Cursors.Wait;
LoginVerification loginInfoVerification = null;
await Task.Run(() =>
{
loginInfoVerification = config.ServerConnection(loginInfo.userName,
loginInfo.galPassword,
loginInfo.place,
loginInfo.host,
loginInfo.port,
loginInfo.application);
});
.. rest of code, check login success, show message box..
}

Categories