views:

1451

answers:

6

Lets say I have a component called Tasking (that I cannot modify) which exposes a method “DoTask” that does some possibly lengthy calculations and returns the result in via an event TaskCompleted. Normally this is called in a windows form that the user closes after she gets the results.

In my particular scenario I need to associate some data (a database record) with the data returned in TaskCompleted and use that to update the database record.

I’ve investigated the use of AutoResetEvent to notify when the event is handled. The problem with that is AutoResetEvent.WaitOne() will block and the event handler will never get called. Normally AutoResetEvents is called be a separate thread, so I guess that means that the event handler is on the same thread as the method that calls.

Essentially I want to turn an asynchronous call, where the results are returned via an event, into a synchronous call (ie call DoSyncTask from another class) by blocking until the event is handled and the results placed in a location accessible to both the event handler and the method that called the method that started the async call.

public class SyncTask
{
    TaskCompletedEventArgs data;
    AutoResetEvent taskDone;

public SyncTask()
{
    taskDone = new AutoResetEvent(false);
}

public string DoSyncTask(int latitude, int longitude)
{
    Task t = new Task();
    t.Completed = new TaskCompletedEventHandler(TaskCompleted);
    t.DoTask(latitude, longitude);
    taskDone.WaitOne(); // but something more like Application.DoEvents(); in WinForms.
    taskDone.Reset();
    return data.Street;
}

private void TaskCompleted(object sender, TaskCompletedEventArgs e)
{
    data = e;
    taskDone.Set(); //or some other mechanism to signal to DoSyncTask that the work is complete.
}
}

In a Windows App the following works correctly.

public class SyncTask
{
    TaskCompletedEventArgs data;

public SyncTask()
{
    taskDone = new AutoResetEvent(false);
}

public string DoSyncTask(int latitude, int longitude)
{
    Task t = new Task();
    t.Completed = new TaskCompletedEventHandler(TaskCompleted);
    t.DoTask(latitude, longitude);
    while (data == null) Application.DoEvents();

    return data.Street;
}

private void TaskCompleted(object sender, TaskCompletedEventArgs e)
{
    data = e;
}
}

I just need to replicate that behaviour in a window service, where Application.Run isn’t called and the ApplicationContext object isn’t available.

A: 

Maybe you could get DoSyncTask to start a timer object that checks for the value of your data variable at some appropriate interval. Once data has a value, you could then have another event fire to tell you that data now has a value (and shut the timer off of course).

Pretty ugly hack, but it could work... in theory.

Sorry, that's the best I can come up with half asleep. Time for bed...

Jason Down
Sounds like it could work. Like you said though, very "hacky" :)
rob_g
A: 

If Task is a WinForms component, it might be very aware of threading issues and Invoke the event handler on the main thread -- which seems to be what you're seeing.

So, it might be that it relies on a message pump happening or something. Application.Run has overloads that are for non-GUI apps. You might consider getting a thread to startup and pump to see if that fixes the issue.

I'd also recommend using Reflector to get a look at the source code of the component to figure out what it's doing.

MichaelGG
A: 

You've almost got it. You need the DoTask method to run on a different thread so the WaitOne call won't prevent work from being done. Something like this:

Action<int, int> doTaskAction = t.DoTask;
doTaskAction.BeginInvoke(latitude, longitude, cb => doTaskAction.EndInvoke(cb), null);
taskDone.WaitOne();
Scott Weinstein
Looks promising. I'll give it a try tomorrow and vote accordingly :)
rob_g
Unfortunately if I hook up the event handler on the current thread (as above) and call DoTask on a different thread, the event handler is still blocked with taskDone.WaitOne();
rob_g
Is this some kind of COM component? Create its instance on the worker thread so it doesn't try to marshal to the STA thread.
Hans Passant
The thread the handler is called on is dependent on where the hander is invoked from, not where it has been wired up - you may have other issues preventin the DoTask method from working correctly
Scott Weinstein
I believe you can call WaitOne() with a timeout value (either in msec's or using a TimeSpan). You can put it in a while loop and make sure to call Application.DoEvents() everything you leave the WaitOne on a timeout so the UI thread can update itself.
dviljoen
+1  A: 

I worked out a solution to the async to sync problem, at least using all .NET classes.

http://geekswithblogs.net/rgray/archive/2009/01/29/turning-an-asynchronous-call-into-a-synchronous-call.aspx

It still doesn't work with COM. I suspect because of STA threading. The Event raised by the .NET component that hosts the COM OCX is never handled by my worker thread, so I get a deadlock on WaitOne().

someone else may appreciate the solution though :)

rob_g
A: 

My comment on Scott W's answer seems a little cryptic after I re-read it. So let me be more explicit:

while( !done )
{
    taskDone.WaitOne( 200 );
    Application.DoEvents();
}

The WaitOne( 200 ) will cause it to return control to your UI thread 5 times per second (you can adjust this as you wish). The DoEvents() call will flush the windows event queue (the one that handles all windows event handling like painting, etc.). Add two members to your class (one bool flag "done" in this example, and one return data "street" in your example).

That is the simplest way to get what you want done. (I have very similar code in an app of my own, so I know it works)

dviljoen
Oh! Also, the return value of WaitOne can tell you whether it returned because it timed out or it was signalled. You can use this for "done".
dviljoen
I wouldn't do it that way. Fore more information about my concerns see http://blogs.msdn.com/jfoscoding/archive/2005/08/06/448560.aspx
ollifant
Problem with Application.DoEvents is it needs Application, which is only available to WinForms apps. Calling Application.Run etc in a Class Library just doesn't work.
rob_g
If there's no Application instance then you are not running.
dviljoen
System.Windows.Forms.Application. I am not using a Windows Forms application. Do you recommend I try Application.Run(ApplicationContext) (also in the System.Windows.Forms namespace). seems a little odd given it's not a winforms app.
rob_g
+2  A: 

I've had some trouble lately with making asynchronous calls and events at threads and returning them to the main thread.

I used SynchronizationContext to keep track of things. The (pseudo)code below shows what is working for me at the moment.

SynchronizationContext context;

void start()
{
    //First store the current context
    //to call back to it later
    context = SynchronizationContext.Current; 

    //Start a thread and make it call
    //the async method, for example: 
    Proxy.BeginCodeLookup(aVariable, 
                    new AsyncCallback(LookupResult), 
                    AsyncState);
    //Now continue with what you were doing 
    //and let the lookup finish
}

void LookupResult(IAsyncResult result)
{
    //when the async function is finished
    //this method is called. It's on
    //the same thread as the the caller,
    //BeginCodeLookup in this case.
    result.AsyncWaitHandle.WaitOne();
    var LookupResult= Proxy.EndCodeLookup(result);
    //The SynchronizationContext.Send method
    //performs a callback to the thread of the 
    //context, in this case the main thread
    context.Send(new SendOrPostCallback(OnLookupCompleted),
                 result.AsyncState);                         
}

void OnLookupCompleted(object state)
{
    //now this code will be executed on the 
    //main thread.
}

I hope this helps, as it fixed the problem for me.

Sorskoot