Popping a MessageBox for the main app with Backgroundworker in WPF
Asked Answered
S

7

14

In a WPF app, I am using a BackgroundWorker to periodically check for a condition on the server. While that works fine, I want to pop a MessageBox notifing the users if something fails during the check.

Here's what I have:

public static void StartWorker()
{
    worker = new BackgroundWorker();

    worker.DoWork += DoSomeWork;
    worker.RunWorkerAsync();
}

private static void DoSomeWork(object sender, DoWorkEventArgs e)
{
    while (!worker.CancellationPending)
    {
        Thread.Sleep(5000);

        var isOkay = CheckCondition();

        if(!isOkay)
           MessageBox.Show("I should block the main window");                
    }   
}

But this MessageBox does not block the main window. I can still click on my WPF app and change anything I like with the MessageBox around.

How do I solve this? Thanks,


EDIT:

For reference, this is what I ended up doing:

public static void StartWorker()
{
    worker = new BackgroundWorker();

    worker.DoWork += DoSomeWork;
    worker.ProgressChanged += ShowWarning;
    worker.RunWorkerAsync();
}

private static void DoSomeWork(object sender, DoWorkEventArgs e)
{
    while (!worker.CancellationPending)
    {
        Thread.Sleep(5000);

        var isOkay = CheckCondition();

        if(!isOkay)
           worker.ReportProgress(1);                
    }   
}

private static void ShowWarning(object sender, ProgressChangedEventArgs e)
{
    MessageBox.Show("I block the main window");
}
Selfimprovement answered 23/6, 2010 at 19:2 Comment(0)
I
8

Call ReportProgress and pass this to MessageBox.Show.

Intelligentsia answered 23/6, 2010 at 19:11 Comment(1)
Keeping in mind that this method may open multiple MessageBox windows on top of each other without waiting for one of them to close, as it can be called multiple times.Ravo
G
13

Replace

MessageBox.Show("I should block the main window"); 

with

this.Invoke((Func<DialogResult>)(() => MessageBox.Show("I should block the main window")));

that will cause the message box to be on the main thread and will block all access to the UI until it gets a response. As a added bonus this.Invoke will return a object that can be cast in to the DialogResult.

Gastroenterostomy answered 23/6, 2010 at 19:53 Comment(5)
I want to try this solution too, but I can't get it to compile. I am calling this from a normal class (not a control) and it does not have Invoke on it. I also tried "Dispatcher.CurrentDispatcher.Invoke()" but it doesn't take the Func that was suggestedSelfimprovement
I assumed this was being called from the form. to be able to do this method you will need to pass the class a reference to the parent form and use _ParentForm.Invoke(...), there may be another way to do it but I do not know any off of the top of my head.Gastroenterostomy
This doesn't even compile on a formWorth
Sorry. Various errors indicating invalid expression terms, ), =>, and expectations of where the statement should be end. I gather that there's too many / too few parens or lambda.Worth
@ScottChamberlain I think you forgot one set of parenthesis: this.Invoke((Func<DialogResult>)(() => MessageBox.Show("I should block the main window")));Exploiter
M
11

It doesn't only not block the main window, it is also very likely to disappear behind it. That's a direct consequence of it running on a different thread. When you don't specify an owner for the message box then it goes looking for one with the GetActiveWindow() API function. It only considers windows that use the same message queue. Which is a thread-specific property. Losing a message box is quite hard to deal with of course.

Similarly, MessageBox only disables windows that belong to the same message queue. And thus won't block windows created by your main thread.

Fix your problem by letting the UI thread display the message box. Use Dispatcher.Invoke or leverage either the ReportProgress or RunWorkerCompleted events. Doesn't sound like the events are appropriate here.

Markhor answered 23/6, 2010 at 19:49 Comment(1)
Thank you, I had a feeling that this is thread related, but I have no idea how to solve it. Using "ReportProgess" or "RunWorkerCompleted" wasn't intuitive, but reading the explanations does makes a lot more senses.Selfimprovement
I
8

Call ReportProgress and pass this to MessageBox.Show.

Intelligentsia answered 23/6, 2010 at 19:11 Comment(1)
Keeping in mind that this method may open multiple MessageBox windows on top of each other without waiting for one of them to close, as it can be called multiple times.Ravo
D
2

As Stephen and Hans have said, use the ReportProgress event, and pass data to the UI thread. This is especially important if you want to do anything other tha a MessageBox (for isntance, update a control) because the background thread can't do this directly. You'll get a cross-thread exception.

So whatever you need to do (update progress bar, log messages to the UI, etc.), pass data to the UI thread, tell the UI thread what needs to be done, but let the UI thread do it.

Dardanus answered 23/6, 2010 at 19:54 Comment(0)
O
2

I modified it like this and worked fine for me

 return Application.Current.Dispatcher.Invoke(() => MessageBox.Show(messageBoxText, caption, button, icon));
Organ answered 29/9, 2015 at 12:48 Comment(1)
Shoudl be a comment on the answer under.Organ
R
0

To keep everything in one place, my preferred option is calling from within DoWork. Passing the main form as parameter owner is preventing MessageBox from appearing behind the main window:

MessageBoxResult mbr = MessageBoxResult.None;
Dispatcher.Invoke(() => mbr = MessageBox.Show(this, "TEST", "Win", MessageBoxButton.YesNo, MessageBoxImage.Warning));
while (mbr == MessageBoxResult.None) { Thread.Sleep(10); }
// handle result and/or continue
Ravo answered 30/4 at 8:32 Comment(0)
C
-1

To add some flair for the recommendations about using ReportProgress:

Here's a neat way to have your cake and eat it too! Or in other words, here's a way for you to write UI interactive code inside your BackgroundWorker and not encounter a cross-thread operation. I threw this together one day for a project, buyer beware!

' Init background worker
Dim _BGWorker As BackgroundWorker

' Your Windows Form's _Load event handler:
Private Sub Form_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles MyBase.Load
    ' Just for this example, we're newing up the _BGWorker here
    _BGWorker = New BackgroundWorker()
    _BGWorker.WorkerReportsProgress = True
End Sub

' Magical UI Action Handling ProgressChanged Event Handler Thing (v24.4.550-alpha9) ™ ©
Private Sub _BGWorker_ProgressChanged(ByVal sender As Object, ByVal e As ProgressChangedEventArgs) Handles _BGWorker.ProgressChanged
    ' Take the UserState object and cast to an Action delegate type
    Dim uiAction As Action = CType(e.UserState, Action)

    ' Check if an action was passed
    If uiAction IsNot Nothing Then
        ' Run it if so!
        uiAction()
    End If
End Sub

' Standard DoWork handler for BackroundWorker
Private Sub _BGWorker_DoWork(ByVal sender As Object, ByVal e As DoWorkEventArgs) Handles _BGWorker.DoWork
    '...your background worker code...

    ' EXAMPLE:
    '
    ' Me.Text = "Background worker is (trying to) change form title! :-O"
    '
    ' This line would normally fail with an exception when written directly in the DoWork
    ' handler of a BackgroundWorker. Exception you would see is:
    '   System.InvalidOperationException: Cross-thread operation not valid: Control 'FormName'
    '   accessed from a thread other than the thread it was created on.'

    ' BUT...

    ' If we write something like this:

    _BGWorker.ReportProgress(-1, New Action(
    Sub()
        '...and put that line inside this action:
        Me.Text = "Background worker is changing form title! :-O"

        ' Then NO PROBLEM! UI-interactive code MAGIC!

        ' Well, it's not magic... This works because this code is not executed here immediately.
        ' It is an Action Delegate: https://learn.microsoft.com/en-us/dotnet/api/system.action-1?view=net-7.0
        ' You can write ANY UI-interactive code inside blocks like this in your BackgroundWorkers
        ' and they will all execute on the main thread, because the Action delegate is being 
        ' shipped to the ProgressChanged event handler on the main Form thread.

        'TODO: Add your other UI code...

    End Sub))

    ' Now simply repeat the above section every time you wish write UI-interactive
    ' code in your BG workers! SHAZAM!

    '...your background worker code...
End Sub
Cerellia answered 17/8, 2023 at 22:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.