2

Taking the HTMLElementEvents2 interface in MSHTML as an example, each of the EventMethods is passed a pEvtObj parameter, as in

HTMLElementEvents2 = dispinterface
  [...]
  function  onclick(const pEvtObj: IHTMLEventObj): WordBool; dispid -600;

to identify which element called the event. So, if you define a class descended from TInterfacedObject which implements the HTMLElementEvents2 interface, in the implemented event methods, you could identify which particular HTML element called the event handler, and so access its members.

Being able to identify the caller object which invoked the event is pretty much essential where your handler instance is receiving event calls from multiple callers (e.g. when the handler is attached to multiple HTML elements in the MSHTML example).

This method of implementing a COM Events handler works fine, but is a bit verbose in source terms because it requires defining methods which implement each of the events in the event interface.

There is an alternative, much more concise, method for implementing an event handler, based I assume on the TEventDispatch class in OleCtrls.Pas, which allows you to attach a handler to a single event - see my answer to the q Detect when the active element in a TWebBrowser document changes.

The problem with the technique in that answer is that I can't see a way inside the Invoke implementation to identify the caller object, and my questions is, can this be done and if so, how?

I've tried observing the untyped Params argument passed to the answer's Invoke, by code like this:

function TEventObject.Invoke(DispID: Integer; const IID: TGUID;
  LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo,
  ArgErr: Pointer): HResult;
var
  vPDispParams : PDispParams;
begin
  vPDispParams := PDispParams(@Params);
  [...]

but the rgvarg member of vPDispParams^ (which I was hoping to contain the pEvtObj parameter) contains no elements, and its cArgs is zero.

The code in my answer to the linked q is an MCVE of what I'm asking.

Community
  • 1
  • 1
MartynA
  • 30,454
  • 4
  • 32
  • 73
  • 3
    Typically, you would have a separate event sink for each source; that sink would then, of course, know where events are coming from. COM connection points are designed for this use case; the source is not in any way represented in the parameters. HTML events are exceptions to the rule because they bubble: it is common to handle an event on an element other than the one that triggered it, but somewhere up the hierarchy. So designers of these events provided an application-specific mechanism for obtaining the original source. – Igor Tandetnik Jan 31 '16 at 13:46
  • @IgorTandetnik: Thanks, I follow that, I think. But, in the case of .Invoke in my example, why isn't the pEvtObj of the methods in the HTMLElementEvents2 present amongst the vPDispParams passed to it? – MartynA Jan 31 '16 at 14:48
  • Show how you attach your sink; in particular, what you pass to `FindConnectionPoint`. Make sure you use an interface with `2` at the end (e.g. `HTMLElementEvents2` and not `HTMLElementEvents`). The difference between the two is precisely that the former passes a parameter to the sink and the latter doesn't. – Igor Tandetnik Jan 31 '16 at 15:23
  • @IgorTandetnik: Thanks, I think you're onto something with the difference. I'll see it I can sort out a problem with using FindConnectionPoint and then maybe take this question down and maybe replace it with a new one, depending ... – MartynA Jan 31 '16 at 16:12

1 Answers1

3

With the assistance of comments from @IgorTandetnik, to whom I'm greatly obliged, I've found the solution to this problem.

In the linked answer, I was using a flawed assignment of the EventObject to the HTML Input element(s), like this

var
  V : OleVariant;
  E : IHtmlElement;


  V := Doc.getElementById('input1');
  E := IDispatch(V) as IHtmlElement;

  //  Create an EventObject as per the linked answer
  DocEvent := TEventObject.Create(Self.AnEvent, True) as IDispatch;

  E.onclick := DocEvent;

In doing that, I'd overlooked the fact that an IHTMLElement has an OnClick property, and it was that which I was assigning the DocEvent to, not some imagined Events interface related to it.

When I replaced E.onclick := DocEvent by using a ConnectionPoint, like this

  ITE := IDispatch(V) as IHtmlInputTextElement;
  Assert(ITE <> Nil);
  CPC := ITE as IConnectionPointContainer;
  Assert(CPC <> Nil); 

  OleCheck(CPC.FindConnectionPoint(HTMLInputTextElementEvents2, CP));
  OleCheck((CP as IConnectionPoint).Advise(DocEvent, Cookie));

, then the DocEvent's .Invoke method works fine. In particular, I can access the IEvtObj of the HTMLInputTextElementEvents2.OnClick method using code like the following:

function TEventObject.Invoke(DispID: Integer; const IID: TGUID;
  LocaleID: Integer; Flags: Word; var Params; VarResult, ExcepInfo,
  ArgErr: Pointer): HResult;

[...]
begin
  vPDispParams := PDispParams(@Params);
  V := OleVariant(vPDispParams^.rgvArg^[0]);
  IDisp := IDispatch(V);
  if Supports(IDisp, IHTMLEventObj, IEvtObj) then begin
    IHE := IEvtObj.srcElement;
    IHE.QueryInterface(IHtmlInputTextElement, ITE);
  end;

So, mystery solved. My question was a bit of an XY problem - I thought I needed to identify the caller of the event, whereas if I'd set the event handling up using FindConnectionPoint for the events interface I was intending to use, that need wouldn't have arisen.

MartynA
  • 30,454
  • 4
  • 32
  • 73