Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Control.EndInvoke resets call stack for exception

I don't do a lot of Windows GUI programming, so this may all be common knowledge to people more familiar with WinForms than I am. Unfortunately I have not been able to find any resources to explain the issue, I encountered today during debugging.

If we call EndInvoke on an async delegate. We will get any exception thrown during execution of the method re-thrown. The call stack will reflect the original source of the exception.

However, if we do something similar on a Windows.Forms.Control, the implementation of Control.EndInvoke resets the call stack. This can be observed by a simple test or by looking at the code in Reflector. The relevant code excerpt from EndInvoke is here:

if (entry.exception != null)
{
   throw entry.exception;
}

I understand that Begin/EndInvoke on Control and async delegates are different, but I would have expected similar behavior on Control.EndInvoke.

Is there any reason Control doesn't do whatever it is async delegates do to preserve the original call stack?

like image 676
Brian Rasmussen Avatar asked Apr 12 '10 11:04

Brian Rasmussen


3 Answers

I don't know the real reason but I can guess that async delegates are akin to RPC while control delegates may be based on Win32 Message sending. Different technologies so the impact of this feature may not be the same. Async delegate would benefit from all the remoting code, for which the developer would have written the code to transfer exception call stack between different process or computers, while control delegates will simulate RPC with PostMessage within the same process. Different team, different code.

like image 200
plodoc Avatar answered Sep 24 '22 23:09

plodoc


I'm not sure why Control doesn't do this (probably just an oversight), but you can work around it in .NET 4.0 by scheduling a Task to the UI form:

    private BackgroundWorker bgw;
    private TaskFactory uiTaskFactory;

    private void Form1_Load(object sender, EventArgs e)
    {
        this.uiTaskFactory = new TaskFactory(TaskScheduler.FromCurrentSynchronizationContext());
        this.bgw = new BackgroundWorker();
        this.bgw.DoWork += bgw_DoWork;
        this.bgw.RunWorkerAsync();
    }

    void bgw_DoWork(object sender, DoWorkEventArgs e)
    {
        var task = this.uiTaskFactory.StartNew(this.OuterTaskFunction);
        try
        {
            task.Wait();
        }
        catch (Exception ex)
        {
            // Note: Full stack trace preserved
            MessageBox.Show(ex.InnerException.ToString());
        }
    }

    void OuterTaskFunction()
    {
        this.InnerTaskFunction();
    }

    void InnerTaskFunction()
    {
        throw new InvalidOperationException("Blah.");
    }
like image 21
Stephen Cleary Avatar answered Sep 26 '22 23:09

Stephen Cleary


Note also that Control.EndInvoke is one of the few Managed EndInvokes in the Framework (so you can see the code in Reflector). They probably ought to have a non-managed helper that throws with the original stack in place.

In fact, I think it is the only managed EndInvoke, but there are other managed End* routines with an IAsyncResult parameter. I've not checked all of them, but it seems all of those I have reviewed just throw the exception, or effectively use Stephen Cleary's solution of diverting to use .NET 4's GetWaiter.GetResult, which does have some managed and unmanaged shenanigans to try to get the stack restored for exceptions.

like image 31
Mark Hurd Avatar answered Sep 26 '22 23:09

Mark Hurd