Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling a computationally intensive routine from VBA without stalling Excel GUI

I have a set of numerically intensive routines (each takes up to 1 minute to complete) bundled in a COM object, implementing IDispatch cleanly.

I can therefore use them from an Excel worksheet, those routines will be called by VBA macros triggered by buttons.

Now, when one of these routines is called, the Excel user interface is frozen, which is quite uncomfortable for the end users of the sheet.

I'd like to find any mechanism to alleviate this problem.

This could be for instance launching the computation in another thread launched on the COM side, returning immediately, the spawned thread calling back a VBA procedure when results are computed.

Or something simpler, since I only need one computation to be performed at a time.

Now, there may be a lot of issues with calling VBA routines from other threads. I must confess that I am not that experienced with COM, that I only treat as a black box between my code and Excel (I use ATL).

So,

Is it possible to call back VBA routines from another thread ?

Is there a better way to do what I want to achieve ?

UPDATE

After weighing the options and reading a lot of stuff on the internet, I will do cooperative multithreading: in the COM object, instead of having one routine, I shall have three:

class CMyObject : ...
{
    ...

    STDMETHOD(ComputationLaunch)(...); // Spawn a thread and return immediately
    STDMETHOD(ComputationQuery)(DOUBLE* progress, BOOL* finished);
    STDMETHOD(ComputationResult)(VARIANT* out);

private:
    bool finished, progress;
    boost::mutex finished_lock, progress_lock;
    ResultObject result; // This will be marshaled to out
                         // when calling ComputationResult
};

And in the VBA:

Private computeActive as Boolean ' Poor man's lock

Public Sub Compute()

    OnError GoTo ErrHandler:

    If computeActive Then Exit Sub
    computeActive = True

    Dim o as MyObject
    call o.ComputationLaunch

    Dim finished as Boolean, progress as Double

    While Not o.ComputationQuery(progress)
        DoEvents
        ' Good place also to update a progress display
    End While

    Dim result as Variant
    result = o.ComputationResult

    ' Do Something with result (eg. display it somewhere)

    computeActive = False
    Exit Sub

ErrHandler:
    computeActive = False
    Call HandleErrors

End Sub

Indeed, by doing a depth-first-search on the internet for COM Add-Ins, I realized that VBA macros run in the same event loop as Excel's GUI, that you have the DoEvents facility, and that it is not safe (or at least very tricky) to call back VBA procedures from other threads. This would require eg. tricking the Accesibility facilities to obtain a synchronized handle to an Excel.Application object, and call the OnTime method to set up an asynchronous event handler. Not worth the trouble.

like image 989
Alexandre C. Avatar asked Apr 21 '11 18:04

Alexandre C.


2 Answers

If you want to do this well you need to give up on VBA and write a COM add-in.

like image 84
David Heffernan Avatar answered Oct 10 '22 11:10

David Heffernan


Posting my comment as an answer...

You could implement an event in your COM object and have it call back when done. See http://www.dailydoseofexcel.com/archives/2006/10/09/async-xmlhttp-calls/ for an example of how to run a COM object asynchronously.

like image 40
Tim Williams Avatar answered Oct 10 '22 09:10

Tim Williams