I want to wait for a WebBrowser control to finish navigation. So i create an Event, and then i want to wait for it to be set:
procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
FEvent.ResetEvent;
WebBrowser.Navigate2('about:blank'); //Event is signalled in the DocumentComplete event
Self.WaitFor;
end;
And then i set the event in the DocumentComplete
event:
procedure TContoso.DocumentComplete(ASender: TObject; const pDisp: IDispatch; const URL: OleVariant);
var
doc: IHTMLDocument2;
begin
if (pDisp <> FWebBrowser.DefaultInterface) then
begin
//This DocumentComplete event is for another frame
Exit;
end;
//Set the event that it's complete
FEvent.SetEvent;
end;
The problem comes in how to wait for this event to happen.
First reaction would be to wait for the event to become triggered:
procedure TContoso.WaitFor;
begin
FEvent.WaitFor;
end;
The problem with that is that the DocumentComplete
event can never fire, because the application never goes idle enough to allow the COM event to get through.
My first reaction was to do a busy sleep, waiting for a flag:
procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
FIsDocumentComplete := False;
WebBrowser.Navigate2('about:blank'); //Flag is set in the DocumentComplete event
Self.WaitFor;
end;
procedure TContoso.WaitFor;
var
n: Iterations;
const
MaxIterations = 25; //100ms each * 10 * 5 = 5 seconds
begin
while n < MaxIterations do
begin
if FIsDocumentComplete then
Exit;
Inc(n);
Sleep(100); //100ms
end;
end;
The problem with a Sleep
, is that it doesn't allow the application to do idle enough to allow the COM event messages to get through.
After research, it seems that COM folks created a function created exactly for this situation: (archive)
While a thread in a Single-Threaded Apartment (STA) blocks, we will pump certain messages for you. Message pumping during blocking is one of the black arts at Microsoft. Pumping too much can cause reentrancy that invalidates assumptions made by your application. Pumping too little causes deadlocks. Starting with Windows 2000, OLE32 exposes CoWaitForMultipleHandles so that you can pump “just the right amount.”
So i tried that:
procedure TContoso.WaitFor;
var
hr: HRESULT;
dwIndex: DWORD;
begin
hr := CoWaitForMultipleHandles(0, 5000, 1, @FEvent.Handle, {out}dwIndex);
OleCheck(hr);
end;
i could also try Delphi's own mostly secret feature of TEvent: UseCOMWait
Set
UseCOMWait
to True to ensure that when a thread is blocked and waiting for the object, any STA COM calls can be made back into this thread.
Excellent! Lets use that:
FEvent := TEvent.Create(True);
function TContoso.WaitFor: Boolean;
begin
FEvent.WaitFor;
end;
Except that doesn't work; because the callback event never gets fired.
So now i start to delve into the awful, awful, awful, awful, buggy, error-prone, re-entrancy inducing, sloppy, requires a mouse nudge, sometimes crashes world of MsgWaitForMultipleObjects
:
function TContoso.WaitFor: Boolean;
var
// hr: HRESULT;
// dwIndex: DWORD;
// msg: TMsg;
dwRes: DWORD;
begin
// hr := CoWaitForMultipleHandles(0, 5000, 1, @FEvent.Handle, {out}dwIndex);
// OleCheck(hr);
// Result := (hr = S_OK);
Result := False;
while (True) do
begin
dwRes := MsgWaitForMultipleObjects(1, @FEvent.Handle, False, 5000, QS_SENDMESSAGE);
if (dwRes = WAIT_OBJECT_0) then
begin
//Our event signalled
Result := True;
Exit;
end
else if (dwRes = WAIT_TIMEOUT) then
begin
//We waited our five seconds; give up
Exit;
end
else if (dwRes = WAIT_ABANDONED_0) then
begin
//Our event object was destroyed; something's wrong
Exit;
end
else if (dwRes = (WAIT_OBJECT_0+1)) then
begin
GetMessage(msg, 0, 0, 0);
if msg.message = WM_QUIT then
begin
{
http://blogs.msdn.com/oldnewthing/archive/2005/02/22/378018.aspx
PeekMessage will always return WM_QUIT. If we get it, we need to
cancel what we're doing and "re-throw" the quit message.
The other important thing about modality is that a WM_QUIT message
always breaks the modal loop. Remember this in your own modal loops!
If ever you call the PeekMessage function or The GetMessage
function and get a WM_QUIT message, you must not only exit your
modal loop, but you must also re-generate the WM_QUIT message
(via the PostQuitMessage message) so the next outer layer will
see the WM_QUIT message and do its cleanup as well. If you fail
to propagate the message, the next outer layer will not know that
it needs to quit, and the program will seem to "get stuck" in its
shutdown code, forcing the user to terminate the process the hard way.
}
PostQuitMessage(msg.wParam);
Exit;
end;
TranslateMessage(msg);
DispatchMessage(msg);
end;
end;
The above code is wrong because:
I've been programming long enough to run away, far away, if i'm going to pump my own messages.
So i have four questions. All related. This post is one of the four:
I am writing in, and using Delphi. But obviously any native code would work (C, C++, Assembly, Machine code).
The short and long of it is that you have to pump ALL messages normally, you can't just single out COM messages by themselves (and besides, there is no documented messages that you can peek/pump by themselves, they are known only to COM's internals).
How to make WebBrower.Navigate2 synchronous?
You can't. But you don't have to wait for the OnDocumentComplete
event, either. You can busy-loop inside of NavigateToEmpty()
itself until the WebBrowser's ReadyState
property is READYSTATE_COMPLETE
, pumping the message queue when messages are waiting to be processed:
procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
begin
WebBrowser.Navigate2('about:blank');
while (WebBrowser.ReadyState <> READYSTATE_COMPLETE) and (not Application.Terminated) do
begin
// if MsgWaitForMultipleObjects(0, Pointer(nil)^, False, 5000, QS_ALLINPUT) = WAIT_OBJECT_0 then
// if GetQueueStatus(QS_ALLINPUT) <> 0 then
Application.ProcessMessages;
end;
end;
How to pump COM messages?
You can't, not by themselves anyway. Pump everything, and be prepared to handle any reentry issues that result from that.
Does pumping COM messages cause COM events to callback?
Yes.
How to use CoWaitForMultipleHandles
Try something like this:
procedure TContoso.NavigateToEmpty(WebBrowser: IWebBrowser2);
var
hEvent: THandle;
dwIndex: DWORD;
hr: HRESULT;
begin
// when UseCOMWait() is true, TEvent.WaitFor() does not wait for, or
// notify, when messages are pending in the queue, so use
// CoWaitForMultipleHandles() directly instead. But you have to still
// use a waitable object, just don't signal it...
hEvent := CreateEvent(nil, True, False, nil);
if hEvent = 0 then RaiseLastOSError;
try
WebBrowser.Navigate2('about:blank');
while (WebBrowser.ReadyState <> READYSTATE_COMPLETE) and (not Application.Terminated) do
begin
hr := CoWaitForMultipleHandles(COWAIT_INPUTAVAILABLE, 5000, 1, hEvent, dwIndex);
case hr of
S_OK: Application.ProcessMessages;
RPC_S_CALLPENDING, RPC_E_TIMEOUT: begin end;
else
RaiseLastOSError(hr);
end;
end;
finally
CloseHandle(hEvent);
end;
end;