4

I'm having a TWebBrowser control which implements IDocHostUIHandler to extend the control of a JavaScript interop through the IDispatch container. This works fine, except that I don't know, how to dispatch event from JavaScript back to the web browser control.

The extension object is a container based on TAutoIntfObject like in this example. As you can see in the example, there is no interop with the web browser control. Ideally, I'd like to implement events on that extension object, but I don't know how to properly declare the TAutoIntfObject object in my web browser control. Let's say I'm having this extension object:

type
  TZoomChangeEvent = procedure(Sender: TObject; ZoomLevel: Integer) of object;
  TOpenLayersExt = class(TAutoIntfObject, IOpenLayers)
  private
    FOnZoomChange: TZoomChangeEvent;
    // the ZoomChange method is invoked from JavaScript
    procedure ZoomChange(ZoomLevel: Integer); safecall;
  public
    property OnZoomChange: TZoomChangeEvent read FOnZoomChange write FOnZoomChange;
  end;

implementation

procedure TOpenLayersExt.ZoomChange(ZoomLevel: Integer);
begin
  if Assigned(FOnZoomChange) then
    FOnZoomChange(Self, ZoomLevel);
end;

And a TWebBrowser control like this:

type
  TMapBrowser = class(TWebBrowser, IDocHostUIHandler)
  private
    // the extension object
    FExtObj: TOpenLayersExt;
    // IDocHostUIHandler::GetExternal method
    function GetExternal(out ppDispatch: IDispatch): HRESULT; stdcall;
    // this is the TOpenLayersExt.OnZoomChange event method implementation
    procedure OnZoomChange(Sender: TObject; Zoom: Integer);
  public
    // ordinary constructor
    constructor Create(AOwner: TComponent); override;
  end;

implementation

constructor TMapBrowser.Create(AOwner: TComponent);
begin
  inherited Create(AOwner);
  // create extension object
  FExtObj := TOpenLayersExt.Create;
  // here the event method is properly binded; if I'd change the FExtObj type
  // to IDispatch with TOpenLayersExt(FExtObj) typecast, it wouldn't
  FExtObj.OnZoomChange := OnZoomChange;
end;

function TMapBrowser.GetExternal(out ppDispatch: IDispatch): HRESULT;
begin
  // the problem is that I don't know how to properly pass this object to the
  // ppDispatch parameter; if this GetExternal method is called second time,
  // the FExtObj seems to be released, but I don't get why
  ppDispatch := FExtObj as IDispatch;
  Result := S_OK;
end;

The problem is that if I declare the FExtObj object as TOpenLayersExt, the event method is binded but the FExtObj object reference seems to be released after the first extension object method invoke (from JavaScript).

If I declare it as IDispatch, the reference is not released after the JavaScript function invoke, but the OnZoomChange event is not binded.

It's hard to post the full code here as it's composed from more parts, here is a complete project made in Delphi 7.

So my question is, how to consume events from TAutoIntfObject extension object in a web browser control; how to declare the extension object, so I'll be able to handle events from a web browser control and pass it to the IDocHostUIHandler::GetExternal method parameter still keeping the interface object reference ?

TLama
  • 75,147
  • 17
  • 214
  • 392
  • I suppose you saw this article? : http://www.delphidabbler.com/articles?article=22&part=3. They use an external container via IOleClientSite. – whosrdaddy Apr 12 '13 at 13:30
  • Or checkout the sourcecode from EmbeddedWB... – whosrdaddy Apr 12 '13 at 13:33
  • @whosrdaddy, that's where I originated my code (even linked in my question), but I don't know [`how the IOleClientSite`](http://support.microsoft.com/kb/188015/en-us) comes into play there. Whenever you call a method from JavaScript through `external`, the `IDocHostUIHandler::GetExternal` method asks for an external object, which must have a dispatch method of the name you're calling. There's no need for `IOleClientSite` here. About EWB, how does it help me ? There's just a `OnGetExternal` event in which you're passing the extension container, but that's what I actually have. – TLama Apr 12 '13 at 14:01
  • I'm not so sure what you need. the [example](http://www.delphidabbler.com/articles?article=22&part=1) you linked to worked perfectly for me (as oposed to the *lame* EmbeddedWB implementation). Look at the sources: `TExternalContainer = class(TNulWBContainer, IDocHostUIHandler, IOleClientSite)` and in the main project `fContainer := TExternalContainer.Create(WebBrowser1);`. It's kinda tricky but actually a very good idea to avoid an interposer class like you did : `TMapBrowser = class(TWebBrowser, IDocHostUIHandler)`. – kobik Apr 12 '13 at 14:04
  • And yes, IE will dispose of FExtObj interface object when it is no longer needed. this is by design. – kobik Apr 12 '13 at 14:07
  • 1
    I once did something like the opposite: getting to the container from an ActiveX loaded in a web-page: http://yoy.be/item.asp?i98 that's where IOleClientSite comes in. I should find some time though to dig into the code you post here, looks very much like things I've done as well. – Stijn Sanders Apr 12 '13 at 14:14
  • @kobik, there's no problem with my implementation if I declare `FExtObj` as `IDispatch`. But then I can't use `OnZoomChange` event. On the other hand, if I declare `FExtObj` as `TOpenLayersExt`, I can use events, but the reference is released. The question might be interpreted also as why the `FExtObj` reference is not released if it's declared as `IDispatch` and is released when it's `TAutoIntfObject` (sorry, I'm quite newbie in this). And yes, you're right that *interposer* class is not the best for this. – TLama Apr 12 '13 at 14:25
  • Have you tried declaring `TOpenLayersExt = class(TAutoIntfObject, IOpenLayers, IDispatch)`? – kobik Apr 12 '13 at 14:26

1 Answers1

6

Use reference counting, ie. keep FExtObj as a reference to an interface, rather than object:

  private
    // the extension object
    FExtObj: IDispatch;

...

constructor TMapBrowser.Create(AOwner: TComponent);
var
  AExtObj: TOpenLayersExt;
begin
  inherited Create(AOwner);
  // create extension object
  AExtObj := TOpenLayersExt.Create;
  AExtObj.OnZoomChange := OnZoomChange;
  FExtObj := AExtObj as IDispatch;
end;

destructor TMapBrowser.Destroy;
begin
  FExtObj := nil;
  inherited Destroy;
end;
Ondrej Kelle
  • 36,941
  • 2
  • 65
  • 128
  • 2
    I confirm, fixing the ref-counting resolves the issue. If you're in for a minimalist fix, insert `FExtObj._AddRef;` at line 140 – Stijn Sanders Apr 12 '13 at 19:36
  • @Stijn, just like MS shown in [`their example`](http://support.microsoft.com/kb/188015/en-us). TOndrej, thanks! This works fine as well. – TLama Apr 13 '13 at 16:03
  • @Stijn, I've finally used this solution since it seems that IE release the references irregularly and if I tried to use `_AddRef` their count was never decreased. Thanks anyway! – TLama Apr 14 '13 at 15:36