I'm using a custom TaskScheduler to execute a task queue in serial. The task is supposed to display a window and then block until the window closes itself. Unfortunately calling Window.ShowDialog() doesn't seem to block so the task completes and the window never displays.
If I put a breakpoint after the call to ShowDialog I can see the form has opened but under normal execution the Task seems to end so quickly you cant see it.
My TaskScheduler implementation taken from a previous question:
public sealed class StaTaskScheduler : TaskScheduler, IDisposable
{
private readonly List<Thread> threads;
private BlockingCollection<Task> tasks;
public override int MaximumConcurrencyLevel
{
get { return threads.Count; }
}
public StaTaskScheduler(int concurrencyLevel)
{
if (concurrencyLevel < 1) throw new ArgumentOutOfRangeException("concurrencyLevel");
this.tasks = new BlockingCollection<Task>();
this.threads = Enumerable.Range(0, concurrencyLevel).Select(i =>
{
var thread = new Thread(() =>
{
foreach (var t in this.tasks.GetConsumingEnumerable())
{
this.TryExecuteTask(t);
}
});
thread.IsBackground = true;
thread.SetApartmentState(ApartmentState.STA);
return thread;
}).ToList();
this.threads.ForEach(t => t.Start());
}
protected override void QueueTask(Task task)
{
tasks.Add(task);
}
protected override IEnumerable<Task> GetScheduledTasks()
{
return tasks.ToArray();
}
protected override bool TryExecuteTaskInline(Task task, bool taskWasPreviouslyQueued)
{
return Thread.CurrentThread.GetApartmentState() == ApartmentState.STA && TryExecuteTask(task);
}
public void Dispose()
{
if (tasks != null)
{
tasks.CompleteAdding();
foreach (var thread in threads) thread.Join();
tasks.Dispose();
tasks = null;
}
}
}
My Application Code:
private StaTaskScheduler taskScheduler;
...
this.taskScheduler = new StaTaskScheduler(1);
Task.Factory.StartNew(() =>
{
WarningWindow window = new WarningWindow(
ProcessControl.Properties.Settings.Default.WarningHeader,
ProcessControl.Properties.Settings.Default.WarningMessage,
processName,
ProcessControl.Properties.Settings.Default.WarningFooter,
ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);
window.ShowDialog();
}, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);
Nothing obviously wrong. Except what is missing, you are not doing anything to ensure that an exception that's raised in the task is reported. The way you wrote it, such an exception will never be reported and you'll just see code failing to run. Like a dialog that just disappears. You'll need to write something like this:
Task.Factory.StartNew(() => {
// Your code here
//...
}, CancellationToken.None, TaskCreationOptions.None, taskScheduler)
.ContinueWith((t) => {
MessageBox.Show(t.Exception.ToString());
}, TaskContinuationOptions.OnlyOnFaulted);
With good odds that you'll now see an InvalidOperationException reported. Further diagnose it with Debug + Exceptions, tick the Thrown checkbox for CLR exceptions.
Do beware that this task scheduler doesn't magically makes your code thread-safe or fit to run another UI thread. It wasn't made for that, it should only be used to keep single-threaded COM components happy. You must honor the sometimes draconian consequences of running UI on another thread. In other words, don't touch properties of UI on the main thread. And the dialog not acting like a dialog at all since it doesn't have an owner. And it thus randomly disappearing behind another window or accidentally getting closed by the user because he was clicking away and never counted on a window appearing from no-where.
And last but not least the long-lasting misery caused by the SystemEvents class. Which needs to guess which thread is the UI thread, it will pick the first STA thread. If that's your dialog then you'll have very hard to diagnose threading problems later.
Don't do it.
Apparently, you're using Stephen Toub's StaTaskScheduler. It is not intended to run tasks involving the UI. Essentially, you're trying to display a modal window with window.ShowDialog() on a background thread which has nothing to do with the main UI thread. I suspect the window.ShowDialog() instantly finishes with an error, and so does the task. Await the task and observe the errors:
try
{
await Task.Factory.StartNew(() =>
{
WarningWindow window = new WarningWindow(
ProcessControl.Properties.Settings.Default.WarningHeader,
ProcessControl.Properties.Settings.Default.WarningMessage,
processName,
ProcessControl.Properties.Settings.Default.WarningFooter,
ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);
window.ShowDialog();
}, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);
}
catch(Exception ex)
{
MessageBox.Show(ex.Message)
}
If you really want to show a window from a background STA thread, you need to run a Dispatcher loop:
var task = Task.Factory.StartNew(() =>
{
System.Windows.Threading.Dispatcher.InvokeAsync(() =>
{
WarningWindow window = new WarningWindow(
ProcessControl.Properties.Settings.Default.WarningHeader,
ProcessControl.Properties.Settings.Default.WarningMessage,
processName,
ProcessControl.Properties.Settings.Default.WarningFooter,
ProcessControl.Properties.Settings.Default.WarningTimeout * 1000);
window.Closed += (s, e) =>
window.Dispatcher.InvokeShutdown();
window.Show();
});
System.Windows.Threading.Dispatcher.Run();
}, CancellationToken.None, TaskCreationOptions.None, this.taskScheduler);
Note however, this window will not be modal as to any main UI thread window. Moreover, you'd block the StaTaskScheduler loop, so its other scheduled tasks won't run until the window is closed and Dispatcher.Run() exits.
I had a similar problem where on start up I could not manage to block execution via a ShowDialog. Eventually I discovered that inside the class properties were Command Line Arguments that was used to automatically log in with appropriate credentials. This saved time during development not to enter a username and password every time you compile. As soon as I removed that the Dialog behaved as expected so it's worth while to search for processes that might interfere with the normal execution path.
Related
I want to show other form with loading gif while executing something async. Any ideas why first TowerPinsVisu is executing and after that the ShowDialog?
private void RunTasks()
{
var tskShowLoadingForm=new Task(() => {
loadingForm.ShowDialog("OPTIMIZING...");
});
tskShowLoadingForm.Start();
var tskVisualizeTowerPins = new Task(() => TowerPinsVisu());
tskVisualizeTowerPins.Start();
}
public void ShowDialog(string text)
{
lblLoadingInfo.Dispatcher.Invoke(() => lblLoadingInfo.Content = text);
this.Dispatcher.Invoke(()=> ShowDialog());
}
Without changing your flow, you have to run TowerPinsVisu()on a background thread using Task.Run. While this thread is running you can then show the dialog.
You can attach a continuation to the Task, which will be executed once the Task has run to completion. By passing the UI thread's SynchronizationContext to the contuation, you can force this continuation to execute on the UI thread (which is necessary to close the dialog:
// If this method executes on the UI thread...
private void RunTasks()
{
// Start a background thread and continue immediately to show the modal screen.
Task.Run(TowerPinsVisu)
.ContinueWith(task => loadingForm.Close(), TaskScheduler.FromCurrentSynchronizationContext());
loadingForm.ShowDialog("OPTIMIZING...");
}
// ...then this method will too => No need to use a Dispatcher at this point
public void ShowDialog(string text)
{
lblLoadingInfo.Content = text;
ShowDialog();
}
I've created a new WebBrowser() control in a new Thread().
The problem I'm having, is that when invoking a delegate for my WebBrowser from the Main Thread, the call is occurring on the Main Thread. I would expect this to happen on browserThread.
private static WebBrowser defaultApiClient = null;
delegate void DocumentNavigator(string url);
private WebApi() {
// Create a new thread responsible
// for making API calls.
Thread browserThread = new Thread(() => {
defaultApiClient = new WebBrowser();
// Setup our delegates
documentNavigatorDelegate = new DocumentNavigator(defaultApiClient.Navigate);
// Anonymous event handler
defaultApiClient.DocumentCompleted += (object sender, WebBrowserDocumentCompletedEventArgs e) => {
// Do misc. things
};
Application.Run();
});
browserThread.SetApartmentState(ApartmentState.STA);
browserThread.Start();
}
DocumentNavigator documentNavigatorDelegate = null;
private void EnsureInitialized() {
// This always returns "false" for some reason
if (defaultApiClient.InvokeRequired) {
// If I jump ahead to this call
// and put a break point on System.Windows.Forms.dll!System.Windows.Forms.WebBrowser.Navigate(string urlString, string targetFrameName, byte[] postData, string additionalHeaders)
// I find that my call is being done in the "Main Thread".. I would expect this to be done in "browserThread" instead
object result = defaultApiClient.Invoke(documentNavigatorDelegate, WebApiUrl);
}
}
I've tried invoking the method a myriad of ways:
// Calls on Main Thread (as expected)
defaultApiClient.Navigate(WebApiUrl);
// Calls on Main Thread
defaultApiClient.Invoke(documentNavigatorDelegate, WebApiUrl);
// Calls on Main Thread
defaultApiClient.BeginInvoke(documentNavigatorDelegate, WebApiUrl);
// Calls on Main Thread
documentNavigatorDelegate.Invoke(WebApiUrl);
// Calls on random Worker Thread
documentNavigatorDelegate.BeginInvoke(WebApiUrl, new AsyncCallback((IAsyncResult result) => { .... }), null);
Update
Let me break down my end-goal a little bit to make things more clear: I have to make calls using WebBrowser.Document.InvokeScript(), however Document is not loaded until after I call WebBrowser.Navigate() and THEN the WebBrowser.DocumentComplete event fires. Essentially, I cannot make my intended call to InvokeScript() until after DocumentComplete fires... I would like to WAIT for the document to load (blocking my caller) so I can call InvokeScript and return my result in a synchronous fashion.
Basically I need to wait for my document to complete and the way I would like to do that is with a AutoResetEvent() class which I will trigger upon DocumentComplete being fired... and I need all this stuff to happen in a separate thread.
The other option I see is doing something like this:
private bool initialized = false;
private void EnsureInitialized(){
defaultApiClient.Navigate(WebApiUrl);
while(!initialized){
Thread.Sleep(1000); // This blocks so technically wouldn't work
}
}
private void defaultApiClient_DocumentComplete(object sender, WebBrowserDocumentCompletedEventArgs e){
initialized = true;
}
This is by design. The InvokeRequired/BeginInvoke/Invoke members of a control require the Handle property of the control to be created. That is the primary way by which it can figure out to what specific thread to invoke to.
But that did not happen in your code, the Handle is normally only created when you add a control to a parent's Controls collection and the parent was displayed with Show(). In other words, actually created the host window for the browser. None of this happened in your code so Handle is still IntPtr.Zero and InvokeRequired returns false.
This is not actually a problem. The WebBrowser class is special, it is a COM server under the hood. COM handles threading details itself instead of leaving it up to the programmer, very different from the way .NET works. And it will automatically marshal a call to its Navigate() method. This is entirely automatic and doesn't require any help. A hospitable home for the COM server is all that's needed, you made one by creating an STA thread and pumping a message loop with Application.Run(). It is the message loop that COM uses to do the automatic marshaling.
So you can simply call Navigate() on your main thread and nothing goes wrong. The DocumentCompleted event still fires on the helper thread and you can take your merry time tinkering with the Document on that thread.
Not sure why any of this is a problem, it should work all just fine. Maybe you were just mystified about its behavior. If not then this answer could help you with a more universal solution. Don't fear the nay-sayers too much btw, displaying UI on a worker thread is filled with traps but you never actually display any UI here and never create a window.
This answer is based on the updated question and the comments:
Basically I need to wait for my document to complete and the way I
would like to do that is with a AutoResetEvent() class which I will
trigger upon DocumentComplete being fired... and I need all this stuff
to happen in a separate thread.
...
I am aware that the main UI will be frozen. This will happen only once
during the lifetime of the application (upon initialization). I'm
struggling to find another way to do what I'm looking to accomplish.
I don't think you should be using a separate thread for this. You could disable the UI (e.g. with a modal "Please wait..." dialog) and do the WebBrowser-related work on the main UI thread.
Anyhow, the code below shows how to drive a WebBrowser object on a separate STA thread. It's based on the related answer I recently posted, but is compatible with .NET 4.0. With .NET 4+, you no longer need to use low-level synchronization primitives like AutoResetEvent. Use TaskCompletionSource instead, it allows to propagate the result and possible exceptions to the consumer side of the operation.
using System;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
namespace WinFroms_21790151
{
public partial class MainForm : Form
{
public MainForm()
{
InitializeComponent();
this.Load += MainForm_Load;
}
void MainForm_Load(object senderLoad, EventArgs eLoad)
{
using (var apartment = new MessageLoopApartment())
{
// create WebBrowser on a seprate thread with its own message loop
var webBrowser = apartment.Invoke(() => new WebBrowser());
// navigate and wait for the result
var bodyHtml = apartment.Invoke(() =>
{
WebBrowserDocumentCompletedEventHandler handler = null;
var pageLoadedTcs = new TaskCompletionSource<string>();
handler = (s, e) =>
{
try
{
webBrowser.DocumentCompleted -= handler;
pageLoadedTcs.SetResult(webBrowser.Document.Body.InnerHtml);
}
catch (Exception ex)
{
pageLoadedTcs.SetException(ex);
}
};
webBrowser.DocumentCompleted += handler;
webBrowser.Navigate("http://example.com");
// return Task<string>
return pageLoadedTcs.Task;
}).Result;
MessageBox.Show("body content:\n" + bodyHtml);
// execute some JavaScript
var documentHtml = apartment.Invoke(() =>
{
// at least one script element must be present for eval to work
var scriptElement = webBrowser.Document.CreateElement("script");
webBrowser.Document.Body.AppendChild(scriptElement);
// inject and run some script
var scriptResult = webBrowser.Document.InvokeScript("eval", new[] {
"(function(){ return document.documentElement.outerHTML; })();"
});
return scriptResult.ToString();
});
MessageBox.Show("document content:\n" + documentHtml);
// dispose of webBrowser
apartment.Invoke(() => webBrowser.Dispose());
webBrowser = null;
}
}
// MessageLoopApartment
public class MessageLoopApartment : IDisposable
{
Thread _thread; // the STA thread
TaskScheduler _taskScheduler; // the STA thread's task scheduler
public TaskScheduler TaskScheduler { get { return _taskScheduler; } }
/// <summary>MessageLoopApartment constructor</summary>
public MessageLoopApartment()
{
var tcs = new TaskCompletionSource<TaskScheduler>();
// start an STA thread and gets a task scheduler
_thread = new Thread(startArg =>
{
EventHandler idleHandler = null;
idleHandler = (s, e) =>
{
// handle Application.Idle just once
Application.Idle -= idleHandler;
// return the task scheduler
tcs.SetResult(TaskScheduler.FromCurrentSynchronizationContext());
};
// handle Application.Idle just once
// to make sure we're inside the message loop
// and SynchronizationContext has been correctly installed
Application.Idle += idleHandler;
Application.Run();
});
_thread.SetApartmentState(ApartmentState.STA);
_thread.IsBackground = true;
_thread.Start();
_taskScheduler = tcs.Task.Result;
}
/// <summary>shutdown the STA thread</summary>
public void Dispose()
{
if (_taskScheduler != null)
{
var taskScheduler = _taskScheduler;
_taskScheduler = null;
// execute Application.ExitThread() on the STA thread
Task.Factory.StartNew(
() => Application.ExitThread(),
CancellationToken.None,
TaskCreationOptions.None,
taskScheduler).Wait();
_thread.Join();
_thread = null;
}
}
/// <summary>Task.Factory.StartNew wrappers</summary>
public void Invoke(Action action)
{
Task.Factory.StartNew(action,
CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Wait();
}
public TResult Invoke<TResult>(Func<TResult> action)
{
return Task.Factory.StartNew(action,
CancellationToken.None, TaskCreationOptions.None, _taskScheduler).Result;
}
public Task Run(Action action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task<TResult> Run<TResult>(Func<TResult> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler);
}
public Task Run(Func<Task> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
public Task<TResult> Run<TResult>(Func<Task<TResult>> action, CancellationToken token)
{
return Task.Factory.StartNew(action, token, TaskCreationOptions.None, _taskScheduler).Unwrap();
}
}
}
}
please help someone.
I want to create and start a new task in the button click handler and it always causes aggregate exception. I'm doing the following:
private void btn_Click(object sender, EventArgs e)
{
Task<Image> t = Task<Image>.Factory.StartNew(InvertImage,
TaskCreationOptions.LongRunning);
t.ContinueWith( task => {
some code here;
pictureBox1.Image = t.Result;
},
TaskContinuationOptions.OnlyOnRanToCompletition);
t.ContinueWith( task => { some code here },
TaskContinuationOptions.OnlyOnFaulted);
}
private Image InvertImage()
{ some code here }
The code if run in the main thread works perfectly, so here is definetely something wrong with my understanding of using Tasks. Thank you in advance.
By default continuation runs on default scheduler which is Threadpool Scheduler. Threadpool threads are always background threads so they can't update the UI components (as UI components always run on foreground thread). So your code won't work.
Fix: Get the scheduler from UI thread.This will ensure that the continuation runs on the same thread which created the UI component
var scheduler = TaskScheduler.FromCurrentSynchronizationContext();
and than pass it to ContinueWith function.
t.ContinueWith( task => {
some code here;
pictureBox1.Image = t.Result;
},
TaskContinuationOptions.OnlyOnRanToCompletition,scheduler);
In Winforms(or even in WPF) only the thread who create the component can update it you should make your code thread-safe.
For this reason the debugger raises an InvalidOperationException with the message, "Control control name accessed from a thread other than the thread it was created on." which is encapsulated as AggregateException because tasks encapsulate all exceptions in aggregate exception
you can use this code to iterate through all exceptions in aggregate exception raised by the task
try
{
t.Wait();
}
catch (AggregateException ae)
{
// Assume we know what's going on with this particular exception.
// Rethrow anything else. AggregateException.Handle provides
// another way to express this. See later example.
foreach (var e in ae.InnerExceptions)
{
if (e is MyCustomException)
{
Console.WriteLine(e.Message);
}
else
{
throw;
}
}
}
To make your thread safe just do something like this
// If the calling thread is different from the thread that
// created the pictureBox control, this method creates a
// SetImageCallback and calls itself asynchronously using the
// Invoke method.
// This delegate enables asynchronous calls for setting
// the text property on a TextBox control.
delegate void SetPictureBoxCallback(Image image);
// If the calling thread is the same as the thread that created
// the PictureBox control, the Image property is set directly.
private void SetPictureBox(Image image)
{
// InvokeRequired required compares the thread ID of the
// calling thread to the thread ID of the creating thread.
// If these threads are different, it returns true.
if (this.picturebox1.InvokeRequired)
{
SetPictureBoxCallback d = new SetPictureBoxCallback(SetPictureBox);
this.Invoke(d, new object[] { image });
}
else
{
picturebox1.Image= image;
}
}
Another option to use a Task result within the calling thread is using async/await key word. This way compiler do the work of capture the right TaskScheduler for you. Look code below. You need to add try/catch statements for Exceptions handling.
This way, code is still asynchronous but looks like a synchronous one, remember that a code should be readable.
var _image = await Task<Image>.Factory.StartNew(InvertImage, TaskCreationOptions.LongRunning);
pictureBox1.Image = _image;
I connect to a webserive. While the webservice is connected i want to have a waiting form with an animated gif inside of it. The waiting form is correctly displayed but the animated give is not animated it is fixed.
Can anybody help me. I have already tried : DoEvents but the gif is still not animated.
// Create the new thread object
Thread NewThread = new Thread(new ThreadStart(RunThread));
// Start the new thread.
NewThread.Start();
// Inform everybody that the main thread is waiting
FRM_Wait waitingDialog = new FRM_Wait();
waitingDialog.Show();
waitingDialog.Activate();
Application.DoEvents();
// Wait for NewThread to terminate.
NewThread.Join();
// And it's done.
waitingDialog.Close();
MessageBox.Show("Upload erfolgreich erledigt.", "Upload Erfolgreich",
MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
}
public void RunThread()
{
mfsportservicedev.ServiceSoapClient servicedev = new mfsportservicedev.ServiceSoapClient();
int status = servicedev.addEvent(videosNames, videos);
}
Don't call Join on a thread from within the UI thread. Instead, disable any controls you don't want to act on until the task has completed (e.g. buttons) and then call back into the UI thread when the operation has completed - so move the "And it's done" code into a new method which is invoked at the end of the operation. If you're using .NET 4, I'd suggest using the TPL for this, as it makes it easier to represent "a task which is in progress" and to add a continuation to it. (It's also a good start for what will become the idiomatic way of doing async operations in .NET 4.5.)
The problem is coming from your join. join is synchronous, so basically you are making your UI wait till the thread finishes its work.
You want to use a callback function to come back to your UI.
Edit : ive been skeetified
You problem is here:
NewThread.Join();
This blocks the UI thread until NewThread ends.
Here's one way to do it:
private myDelegate;
// ...
myDelegate = new Action(RunThread);
myDelegate.BeginInvoke(new AsyncCallback(MyCallback),null);
// You RunThread method is now running on a separate thread
// Open your wait form here
// ...
// This callback function will be called when you delegate ends
private void MyCallback(IAsyncResult ar)
{
myDelegate.EndInvoke(ar);
// Note this is still not the UI thread, so if you want to do something with the UI you'll need to do it on the UI thread.
// using either Control.Invoke (for WinForms) or Dispatcher.Invoke (for WPF)
}
Thread.Join is a blocking call that does not pump messages so that is your problem. It is typically advised to avoid calling any kind of synchronization mechanism that causes the UI thread to block.
Here is a solution using the Task class and the Invoke marshaling technique.
private void async InitiateWebService_Click(object sender, EventArgs args)
{
FRM_Wait waitingDialog = new FRM_Wait();
waitingDialog.Show();
Task.Factory.StartNew(
() =>
{
mfsportservicedev.ServiceSoapClient servicedev = new mfsportservicedev.ServiceSoapClient();
int status = servicedev.addEvent(videosNames, videos);
waitingDialog.Invoke(
(Action)(() =>
{
waitingDialog.Close();
}));
});
}
Here is a solution using a raw Thread.
private void async InitiateWebService_Click(object sender, EventArgs args)
{
FRM_Wait waitingDialog = new FRM_Wait();
waitingDialog.Show();
var thread = new Thread(
() =>
{
mfsportservicedev.ServiceSoapClient servicedev = new mfsportservicedev.ServiceSoapClient();
int status = servicedev.addEvent(videosNames, videos);
waitingDialog.Invoke(
(Action)(() =>
{
waitingDialog.Close();
}));
});
thread.Start();
}
C# 5.0 makes this kind of pattern even easier with its new async and await keywords1.
private void async InitiateWebService_Click(object sender, EventArgs args)
{
FRM_Wait waitingDialog = new FRM_Wait();
waitingDialog.Show();
await Task.Run(
() =>
{
mfsportservicedev.ServiceSoapClient servicedev = new mfsportservicedev.ServiceSoapClient();
int status = servicedev.addEvent(videosNames, videos);
});
waitingDialog.Close();
}
1Not yet released.
I'm developing a C# operation and I would like to show a modal progress dialog, but only when an operation will be long (for example, more than 3 seconds). I execute my operations in a background thread.
The problem is that I don't know in advance whether the operation will be long or short.
Some software as IntelliJ has a timer aproach. If the operation takes more than x time, then show a dialog then.
What do you think that is a good pattern to implement this?
Wait the UI thread with a timer, and show dialog there?
Must I DoEvents() when I show the dialog?
Here's what I'd do:
1) Use a BackgroundWorker.
2) In before you call the method RunWorkerAsync, store the current time in a variable.
3) In the DoWork event, you'll need to call ReportProgress. In the ProgressChanged event, check to see if the time has elapsed greater than three seconds. If so, show dialog.
Here is a MSDN example for the BackgroundWorker: http://msdn.microsoft.com/en-us/library/cc221403(v=vs.95).aspx
Note: In general, I agree with Ramhound's comment. Just always display the progress. But if you're not using BackgroundWorker, I would start using it. It'll make your life easier.
I will go with the first choice here with some modifications:
First run the possible long running operation in different thread.
Then run a different thread to check the first one status by a wait handle with timeout to wait it for finish. if the time out triggers there show the progress bar.
Something like:
private ManualResetEvent _finishLoadingNotifier = new ManualResetEvent(false);
private const int ShowProgressTimeOut = 1000 * 3;//3 seconds
private void YourLongOperation()
{
....
_finishLoadingNotifier.Set();//after finish your work
}
private void StartProgressIfNeededThread()
{
int result = WaitHandle.WaitAny(new WaitHandle[] { _finishLoadingNotifier }, ShowProgressTimeOut);
if (result > 1)
{
//show the progress bar.
}
}
Assuming you have a DoPossiblyLongOperation(), ShowProgressDialog() and HideProgressDialog() methods, you could use the TPL to do the heavy lifting for you:
var longOperation = new Task(DoPossiblyLongOperation).ContinueWith(() => myProgressDialog.Invoke(new Action(HideProgressDialog)));
if (Task.WaitAny(longOperation, new Task(() => Thread.Sleep(3000))) == 1)
ShowProgressDialog();
I would keep the progress dialog separate from the background activity, to separate my UI logic from the rest of the application. So the sequence would be (This is essentially the same as what IntelliJ does):
UI starts the background operation (in a BackgroundWorker) and set up a timer for X seconds
When the timer expires UI shows the progress dialog (if the background task is still running)
When the background task completes the timer is cancelled and the dialog (if any) is closed
Using a timer instead of a separate thread is more resource-efficient.
Recommended non-blocking solution and no new Threads:
try
{
var t = DoLongProcessAsync();
if (await Task.WhenAny(t, Task.Delay(1000)) != t) ShowProgress();
await t;
}
finally
{
HideProgress();
}
I got the idea from Jalal Said answer. I required the need to timeout or cancel the progress display. Instead of passing an additional parameter (cancellation token handle) to the WaitAny I changed the design to depend on Task.Delay()
private const int ShowProgressTimeOut = 750;//750 ms seconds
public static void Report(CancellationTokenSource cts)
{
Task.Run(async () =>
{
await Task.Delay(ShowProgressTimeOut);
if (!cts.IsCancellationRequested)
{
// Report progress
}
});
}
Use it like so;
private async Task YourLongOperation()
{
CancellationTokenSource cts = new CancellationTokenSource();
try
{
// Long running task on background thread
await Task.Run(() => {
Report(cts);
// Do work
cts.Cancel();
});
}
catch (Exception ex) { }
finally {cts.Cancel();}
}