Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

WPF modal progress window

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 :)

like image 856
Mageician Avatar asked Aug 15 '13 17:08

Mageician


2 Answers

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.

like image 124
Marcus Kaseder Avatar answered Nov 05 '22 15:11

Marcus Kaseder


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");
    });
}
like image 33
Servy Avatar answered Nov 05 '22 16:11

Servy