Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations gkittelson on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Cross-Thread error/How to implement the Invoke method 1

Status
Not open for further replies.

andegre

MIS
Oct 20, 2005
275
US
I have 2 forms, 1 which monitors my batch jobs in SQL Server Agent, and the other that throws up a new form to show the results of the last refresh (jobs that have failed since the last refresh). The second form is started by a background worker so the main [monitoring] form can keep refreshing at it's set intervals.

If the status form is still open when the next refresh runs [and finds errors], I want to be able to close the status form, then re-open it showing the previous errors, along with the new ones.

To determine if the form is still open, I have a "ShowingAlerts" boolean variable on the parent form that is set in the "DoWork" background worker method, and it gets set back to false in the "RunWorkerCompleted" method.

Since the status form is still showing, I want to just close it by calling my own method [in the status form] called "CloseResults" which really just calls the forms "Close" method.

When trying to access the status form's "CloseResults" method from the main form, it throws the cross-thread error. I have another program where I implemented the delegate (method???) and that works great, but not sure if it will work here. But since I'm not sending any variables to the status form, just trying to close it, I'm not sure how to implement the:
Code:
if (frmStatus.InvokeRequired)
{
    {delegate} d = new {delegate}(??????);
    this.Invoke(d, new object[] {?????});
}

Can anyone give me some tips on this?

Thanks,
 
faq732-7259 may help. this FAQ demonstrates 2 approaches to marshalling actions across threads: ISynchronizeInvoke and SynchronizationContext. If you have any questions please post back.

Jason Meckley
Programmer
Specialty Bakers, Inc.

faq855-7190
faq732-7259
 
Maybe I am not smart enough because I wasn't able to really follow that FAQ...

I tried to implement it the way that I have in another program, here is the code:

Code:
delegate void CloseStatusForm(object o)//Doesn't need variable put I passed one as dummyData

And here's the method that I call to show the results:

Code:
        private void ShowErrorsInBackgroundWorker(object dummyData)
        {
            // Let's just skip the alerts if they are already shown right now
            if (ShowingAlerts)
            {
                if (statusUpdate.InvokeRequired)
                {
                    CloseStatusForm c = new CloseStatusForm(ShowErrorsInBackgroundWorker);
                    this.Invoke(c, new object[] { dummyData });
                }
                else
                {
                    statusUpdate.CloseResults();
                    //statusUpdate.Close();
                    statusUpdate = null;
                }
            }

            string message = GenerateErrorMessages();

            if (message.Length == 0) return;

            backgroundWorker1.RunWorkerAsync((object)message);
        }

This ends up just looping (stackoverflow exception) because it never hits the else statement.

Comparing this code to what I have in another program, the only difference I see is that I'm not really passing any values in "dummyData", but in the other program the same data would get passed back anyway.

What am I missing here?
 
Maybe I am not smart enough because I wasn't able to really follow that FAQ...
It's not you. threading is a difficult and complex topic to understand.

with an windows application there is a main thread. This thread is where all UI processing is done. if you do all the work on the main thread the IU can become slow and unresponsive because non-UI work (database calls, logical computation) is also being processed. the UI cannot continue until this work is done. To keep the UI responsive you can pass work to child/background threads. by doing this the main (UI) thread is open to process additional instructions.

Normally you want your UI to respond to changes after some action is complete. highlight row after saving, display a message on the form, etc. if that action was passed to a child thread then you need to synchronize the child thread with the main thread before you can alter the UI. You cannot change the UI on a background thread.

there are two ways this can be done:
1. ISynchronizeInvoke which each UI control inherits
2. SynchronizationContext which can sychronize a set of controls.

ISynchronizeInvoke works with an individual control. if you needed to update multiple controls you would need to call each controls Invoke individually. it also requires knowing whether Invoke is required or not.

the SynchronizationContext can take a set of controls and will determine if the control requires invocation. because of this you can create a delegate to set a UI property, pass it to the SynchronizationContext and that be done. You don't need to worry about a required invoke or not.

so how does this relate to your code? a Background worker uses the same approach. The ussage of ISynchronizeInvoke and/or SynchronizationContext is encapsulated within the BackgroundWorker object, so you don't need to interact with it directly.

For your scenario try moving all the UI processing you want done after the work completes to the RunWorkerCompleted event. this may solve the cross threading issue. if not, post the code related to the background worker and the operations you want to occur (both in the background and on the UI)

...
the stack overflow makes sense since you have a recursive call. calling Invoke and passing itself doesn't change the fact that an Invoke is required.

Jason Meckley
Programmer
Specialty Bakers, Inc.

faq855-7190
faq732-7259
 
I'm confused as well about the stackoverflow exception because my other program works just fine...

About moving the work to the RunWorkerCompleted method, I may not have explained my program very well.

The main form queries SQL Server Agent every 15 seconds (in place of the Job Activity Monitor SQL Server provides). So this does all database calls and updating of the form on the main UI thread. If any of the jobs have failed since the last refresh (15 seconds ago), then I want to bring up a new form that shows the jobs that have failed. The reason I want it to open in a background thread is because I want those 15 second refreshes to continue. At the next refresh, if the status form is still open, I want to close it, then re-open with the new list of jobs that failed. (It may still be open because our night-shift workers are not very attentive/reliable in monitoring the batch jobs).

One thing I thought of while I was at lunch, what if instead of creating a new form with my message being passed in the initialize step, maybe I will just initialize, then call a function of that form called "udpate" and populate my message that way. That, I think, will work like my other program does that has a background worker implemented and working (although from what you said I'm not sure why that doesn't get a stackoverflow exception also).

Lot's of stuff here... :)
 
if you're not getting a stack overflow in your other applications it may not be entering that code block to begin with. therefore no stack overflow would not occur.

you cannot open a new form (or any other UI operation) on a background thread. I don't see how opening a form would prevent the 15 refreshes from stopping. This may be influence by how your code is structured, but that can be changed.

I would see it working something like:
1. create a timer.
2. configure the timer to execute every 15 seconds.
3. when executed run sql jobs. this is all done on the background.
4. on the main thread. display a form with the list of messages. this could be as simple as
Code:
Form form = null;

public void BackgroundWorkCompleted(object sender, RunWorkerCompletedEventArgs e)
{
   DisposeOfCurrentForm();
   var error = e.Error == null ? "" : e.Error.ToString();
   form = new MyForm(error);
   form.Show();
}

private void DisposeOfCurrentForm()
{
   if(form == null) return;
   form.Close();
   form.Dispose();
}
this assumes you don't try to catch any exceptions when executing the sql jobs. I'm not familiar with how this is done, but there is someway that an exception message is return from the sql job (like a string property) you can throw an exception as well
Code:
var message = theSqlJob.GetError();
throw new Exception(message);
this would then bubble up and be caught by the BackgroundWorker object. the UI logic would then look the same above.

by taking the Exception.ToString() rather than Exception.Message you get the full stack trace to diagnose the problem. An exception message by itself is usually useless.


Jason Meckley
Programmer
Specialty Bakers, Inc.

faq855-7190
faq732-7259
 
Sorry to waste your time on this Jason. I completely forgot that I am already implementing this same functionality into the same program with a different form (open the other form while still being able to do work from the main form). I think I didn't think of this because my original intention for the status update was in a MessageBox.Show....

Thanks anyway.
 
Status
Not open for further replies.

Part and Inventory Search

Sponsor

Back
Top