简体   繁体   中英

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

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");
    });
}

You can have your progress window's constructor take a Task and then ensure the window calls task.Start on the OnLoaded event. Then you use ShowDialog from the parent form, which will cause the progress window to start the task.

Note you could also call task.Start in the constructor, or in the parent form anywhere before calling ShowDialog . Whichever makes most sense to you.

Another option would be just to use a progress bar in the status strip of the main window, and get rid of the popup. This option seems to be more and more common these days.

You can use the Visibility property on Window to hide the whole window while the splash screen runs.

XAML

<Window ... Name="window" />

Code

window.Visibility = System.Windows.Visibility.Hidden;

//show splash 
//do work
//end splash

window.Visibility = System.Windows.Visibility.Visible;

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.

I found a way to make this work by calling ShowDialog() on a separate thread. I created my own ShowMe() and HideMe() methods in my dialog class that handle the work. I also capture the Closing event to prevent closing the dialog so I can re-use it.

Here's my code for my splash screen class:

public partial class StartupSplash : Window
{
    private Thread _showHideThread;

    public StartupSplash()
    {
        InitializeComponent();

        this.Closing += OnCloseDialog;
    }


    public string Message
    {
        get
        {
            return this.lb_progress.Content.ToString();
        }
        set
        {
            if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread)
                this.lb_progress.Content = value;
            else
                this.lb_progress.Dispatcher.Invoke(new Action(() => this.lb_progress.Content = value));
        }
    }


    public void ShowMe()
    {
        _showHideThread = new Thread(new ParameterizedThreadStart(doShowHideDialog));
        _showHideThread.Start(true);
    }

    public void HideMe()
    {
        //_showHideThread.Start(false);
        this.doShowHideDialog(false);
    }

    private void doShowHideDialog(object param)
    {
        bool show = (bool)param;

        if (show)
        {
            if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread)
                this.ShowDialog();
            else
                Application.Current.Dispatcher.Invoke(new Action(() => this.ShowDialog()));
        }
        else
        {
            if (Application.Current.Dispatcher.Thread == System.Threading.Thread.CurrentThread)
                this.Close();
            else
                Application.Current.Dispatcher.Invoke(new Action(() => this.Close()));
        }
    }


    private void OnCloseDialog(object sender, CancelEventArgs e)
    {
        e.Cancel = true;
        this.Hide();
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM