I apologize if this question has been answered tons of times, but I can't seem to find an answer that works for me. I would like to create a modal window that shows various progress messages while my application performs long running tasks. These tasks are run on a separate thread and I am able to update the text on the progress window at different stages of the process. The cross-thread communication is all working nicely. The problem is that I can't get the window to be on top of only other application windows (not every application on the computer), stay on top, prevent interaction with the parent window, and still allow the work to continue.
Here's what I've tried so far:
First, my splash window is a custom class that extends the Window class and has methods to update the message box. I create a new instance of the splash class early on and Show/Hide it as needed.
In the simplest of cases, I instantiate the window and call .Show()
on it:
//from inside my secondary thread
this._splash.Dispatcher.Invoke(new Action(() => this._splash.Show());
//Do things
//update splash text
//Do more things
//close the splash when done
this._splash.Dispatcher.Invoke(new Action(() => this._splash.Hide());
This correctly displays the window and continues running my code to handle the initialization tasks, but it allows me to click on the parent window and bring that to the front.
Next I tried disabling the main window and re-enabling later:
Application.Current.Dispatcher.Invoke(new Action(() => this.MainWindow.IsEnabled = false));
//show splash, do things, etc
Application.Current.Dispatcher.Invoke(new Action(() => this.MainWindow.IsEnabled = true));
This disables all the elements in the window, but I can still click the main window and bring it in front of the splash screen, which is not what I want.
Next I tried using the topmost property on the splash window. This keeps it in front of everything, and in conjunction with setting the main window IsEnabled property I could prevent interaction, but this makes the splash screen appear in front of EVERYTHING, including other applications. I don't want that either. I just want it to be the topmost window within THIS application.
Then I found posts about using .ShowDialog()
instead of .Show()
. I tried this, and it correctly showed the dialog and did not allow me to click on the parent window, but calling .ShowDialog()
makes the program hang waiting for you to close the dialog before it will continue running code. This is obviously, not what I want either. I suppose I could call ShowDialog()
on a different thread so that that thread would hang but the thread doing the work would not...is that the recommended method?
I have also considered the possibility of not using a window at all and instead putting a full-sized window element in front of everything else on the page. This would work except that I have other windows I open and I'd like to be able to use the splash screen when those are open too. If I used a window element I would have to re-create it on every window and I wouldn't be able to use my handy UpdateSplashText
method in my custom splash class.
So this brings me to the question. What is the right way to handle this?
Thanks for your time and sorry for the long question but details are important :)
The accepted answer from @Servy helped me a lot! And I wanted to share my Version with the async
and MVVM approach. It also contains a small delay to avoid "window flickering" for too fast operations.
Dialog Method:
public static async void ShowModal(Func<IProgress<string>, Task> workAsync, string title = null, TimeSpan? waitTimeDialogShow = null)
{
if (!waitTimeDialogShow.HasValue)
{
waitTimeDialogShow = TimeSpan.FromMilliseconds(300);
}
var progressWindow = new ProgressWindow();
progressWindow.Owner = Application.Current.MainWindow;
var viewModel = progressWindow.DataContext as ProgressWindowViewModel;
Progress<string> progress = new Progress<string>(text => viewModel.Text = text);
if(!string.IsNullOrEmpty(title))
{
viewModel.Title = title;
}
var workingTask = workAsync(progress);
progressWindow.Loaded += async (s, e) =>
{
await workingTask;
progressWindow.Close();
};
await Task.Delay((int)waitTimeDialogShow.Value.TotalMilliseconds);
if (!workingTask.IsCompleted && !workingTask.IsFaulted)
{
progressWindow.ShowDialog();
}
}
Usage:
ShowModal(async progress =>
{
await Task.Delay(5000); // Task 1
progress.Report("Finished first task");
await Task.Delay(5000); // Task 2
progress.Report("Finished second task");
});
Thanks again @Servy, saved me a lot of time.
You are correct that ShowDialog
gives you most of the UI behavior that you want.
It does have the problem that as soon as you call it you block execution though. How could you possibly run some code after you show the form, but define what it should be before it's shown? That's your problem.
You could just do all of the work within the splash class, but that's rather poor practice due to tight coupling.
What you can do is leverage the Loaded
event of Window
to define code that should run after the window is shown, but where it is defined before you show it.
public static void DoWorkWithModal(Action<IProgress<string>> work)
{
SplashWindow splash = new SplashWindow();
splash.Loaded += (_, args) =>
{
BackgroundWorker worker = new BackgroundWorker();
Progress<string> progress = new Progress<string>(
data => splash.Text = data);
worker.DoWork += (s, workerArgs) => work(progress);
worker.RunWorkerCompleted +=
(s, workerArgs) => splash.Close();
worker.RunWorkerAsync();
};
splash.ShowDialog();
}
Note that this method is designed to encapsulate the boilerplate code here, so that you can pass in any worker method that accepts the progress indicator and it will do that work in a background thread while showing a generic splash screen that has progress indicated from the worker.
This could then be called something like this:
public void Foo()
{
DoWorkWithModal(progress =>
{
Thread.Sleep(5000);//placeholder for real work;
progress.Report("Finished First Task");
Thread.Sleep(5000);//placeholder for real work;
progress.Report("Finished Second Task");
Thread.Sleep(5000);//placeholder for real work;
progress.Report("Finished Third Task");
});
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With