Enabling multiple event listeners for Delphi ActiveX controls

Paul Milenkovic

Copyright 2005

 

The Borland Delphi development environment has a facility for writing ActiveX controls.  While ActiveX controls are the primary components for assembling applications from pre-written modules in Visual Basic up through Version 6, ActiveX controls are usable in a variety of rapid application development software packages under Windows, ranging from Matlab through the .NET languages.

ActiveX controls have property variables for configuring their behavior using rapid-application development software tools, they have functions for performing operations on those controls during runtime, and they have events, which allow them to call back into their containing applications with information about mouse clicks and state changes.  The ability of an application containing ActiveX controls to register listeners to those events is an important feature in developing application programs based on ActiveX controls.

While ActiveX is only supported under Windows, one of the big advantages of ActiveX is the ability to supply software modules to a variety of computer languages and host environments.  The idea behind Java is to support multiple operating systems but to require everyone to use the same computer language.  The idea behind ActiveX is to support only Windows but to allow developers to use whatever computer language suits their requirements.

Certain host environments are not able to support events for ActiveX controls written in Delphi – the Mark Hammond ActiveX module for Python is one instance; Matlab Version 7 is another although Matlab Version 6 didn’t have this problem.  Delphi restricts ActiveX events to only allowing a single listener to be registered while these particular host environments attempt to register multiple listeners and report error messages when these registrations of event listeners fail.

Delphi permits events to have function or reference-variable return values.  The reason for single registration is that return values don’t make sense for multiple event listeners.  On the other hand, many ActiveX containers don’t support event return values anyway – while an ActiveX control does not have to observe this restriction, it is useful to follow this restriction to allow an ActiveX control to be used with more host containers and with more languages.

To the extent that useful ActiveX controls are written in Delphi, it would be useful for the Python ActiveX module and Matlab Version 7 to only register single event listeners.  To the extent that there are host containers that go ahead and register multiple listeners, it would be useful to be able to implement ActiveX controls in Delphi that accept registration of multiple event listeners.  Some cases of multiple listener registration may not be avoidable.  The Python ActiveX module registers default listeners inside an MFC-derived module, and it also registers listeners for the Python event interface (Mark Hammond, personal communication).  The MFC module is enough of a “black box” that it may not be possible to remove its listeners or to connect the Python interface to those listeners, hence the need for two sets of listeners. 

 

How Delphi Implements an ActiveX Control

An ActiveX control is implemented in Delphi by first implementing a VCL (Visual Component Library) control and by generating the ActiveX control using a “wizard.” 

A VCL control is an object wrapper around a Windows-native “window handle” control, and a VCL control is generally developed by extending a VCL base class through inheritance.  No special software tools are provided for this task and knowledge of which methods to override is supplied through documentation and sample controls. 

A Delphi ActiveX control is an object wrapper around the VCL control.  The “wizard” is a software tool for automatically generating that object wrapper; much of the wrapper forwards calls from the ActiveX interface to corresponding methods of the wrapped VCL control.  The ActiveX control is derived from base class TActiveXControl in module AXCtrls.pas to implement behavior common to all ActiveX controls, but relies on forwarding method calls to the VCL control to implement behavior specific to a particular ActiveX control.  The automatic generation of code takes the drudgery out of writing all of the forwarding methods.  The generation wizard generation of the ActiveX control from the VCL control is a one-time occurrence without a “round trip” capability to make further changes – further changes can be made by hand editing the forwarding methods, which is straightforward from using the wizard-generating forwarding calls as examples.

The code to support registration of event listeners along with the Delphi-imposed restriction to single registration is found in module AXCtrls.pas.  This module contains a definition for class TConnectionPoint where method Advise contains

  if (FKind = ckSingle) . . . then

  begin

    Result := CONNECT_E_CANNOTCONNECT;

    Exit;

  end;

 

which rejects multiple registrations of event listeners (a listener is called a “sink” and gets added with the AddSink method) when the FKind flag is set to ckSingle.  That connection point object is created and added to the connection point container (ActiveX jargon for a collection of connection point objects) in the TActiveXControl.Initialize method using the call

  if FControlFactory.EventTypeInfo <> nil then

    FConnectionPoints.CreateConnectionPoint(FControlFactory.EventIID,

      ckSingle, EventConnect);

One way to allow multiple event listeners would be to edit TActiveXControl.Initialize in module AXCtrls.pas to read

  if FControlFactory.EventTypeInfo <> nil then

    FConnectionPoints.CreateConnectionPoint(FControlFactory.EventIID,

      ckMulti, EventConnect);

There is no simple way to recompile the Delphi runtime of which AXCtrls.pas is a part, but one could edit a copy of AXCtrls.pas and placing that edited file in your project source files directory to override that portion of the Delphi runtime.  Doing this means runtime packages need to be disabled.  This method has the disadvantage that AXCtrls.pas has changed between Delphi versions and there are copyright restrictions on distributing AXCtrls.pas with any sources that you give out.

 

Implementing Multiple Event Listeners Using Inheritance

The module AxMvnt.pas implementing class TActiveXControlMultiEvent implements support for multiple event listeners by using inheritance to override behavior of class TActiveXControl – the source listing is commented on how to substitute TActiveXControlMultiEvent for TActiveXControl as the base class of your Delphi ActiveX control.  Your ActiveX control should then obey the restriction of not having function or var-parameter return values, which is a convention you need to follow anyway if you want your control to work in the containers where multiple event listeners is an issue.

The means of enabling multiple event listeners is straightforward – locate the connection point object created by TActiveXControl and remove it from the connection point collection object (called a connection point container), and create a new connection point object using the ckMulti flag and place it into the connection point collection.

For an object class to be modifiable through inheritance and overrides of virtual methods it has to be designed with that objective in mind.  Class TActiveXControl has been so completely locked down through “encapsulation” that getting at the default connection point object is a challenge and removing that object so a multi-listener connection point object can be substituted is an even bigger challenge.  One of the main purposes of encapsulation in object-oriented programming is not to make life difficult for the developer who is saying “Why can’t I do that, I know what I am doing.”  Rather, it is to limit the interfaces between parts of a system to give the developer of the base class the freedom to make changes, to fix bugs or to add features, without breaking other parts of the system.

Retrieving the default connection point is accomplished through the FindConnectionPoint method of the IConnectionPointContainer interface.  Class TActiveXControl has a ConnectionPoints property containing a TConnectionPoints object, a collection object for TConnectionPoint objects, but we cannot call ConnectionPoints.FindConnectionPoint because it is a protected method.  The TActiveXControl object itself implements the IConnectionPointContainer interface, the methods of that interface in turn implemented by delegation to the protected methods of the ConnectionPoints property using the implements keyword in the property definition.  The syntax

      (Self as IConnectionPointContainer).FindConnectionPoint(

inside method InitializeControl of class TActiveXControlMultiEvent has the desired effect.

Once we get the connection point in the form of a variable referencing the IConnectionPoint interface, how do we remove that connection point from the collection so we can substitute a new connection point with the desired properties?  It would be desirable for class TActiveXControl to have a method to do this – actually, if TActiveXControl had an accessible property to select between ckSingle and ckMulti modes of connection point behavior, we wouldn’t need to override TActiveXControl is the first place.  Taking TActiveXControl as a given, some “hacking” is required to make up for its shortcomings.  The process for removing the connection point requires knowledge of the inner workings of TActiveXControl, which we get from reading the source code distributed with Delphi.  This means that we will be more strongly coupled to the inner workings of TActiveXControl than we would like, but this may be an alternative to copying unit AXCtrls.pas in its entirety, which gives even tighter coupling to the Delphi runtime.

 

Deleting the Default Connection Point Object

Deleting the default connection point object by invoking its Free method should not be a problem.  Class TConnectionPoint is a “contained interface” object implemented by inheritance from TContainedObject.  The lifetime of a contained-interface object is managed by the container, and any reference counts are combined with the reference count of the container.  In most cases, invoking Free on an object where the lifetime is managed with a reference count leads to trouble, but that is not the case in this instance.  If we free the connection point object as implemented in AXCtrls.pas, the destructor code removes that object from its container. 

The call to FindConnectionPoint returns an interface to the connection point object, not the connection point object itself.  Casting the interface variable to class TConnectionPoint or to base class TObject and invoking Free results in a runtime error.  This problem along with a solution is discussed by Shenoy who references Hallvard.  The function GetImplementingObject() works at a low enough level to get a reference to the underlying object from a reference to an interface to allow safely calling Free.

 

Discussion

The above solution to enabling registration of multiple listeners to the events of ActiveX controls written in Delphi is an “ugly” workaround.  It is ugly in the sense that it breaks encapsulation, relies heavily on implementation details that require reading the source to AXCtrls.pas, and relies on a novel means of retrieving the underlying object from an object reference that relies on low-level implementation details of Delphi objects.  It is a solution to a problem that should not be there – there is not fundamental reason while ActiveX containers should require multiple listener registrations to allow events to work; there is no reason why Borland shouldn’t put a feature into Delphi to allow multiple listener registrations for ActiveX events.

It is also a question of certain problems “coming up on the radar.”  If a large number of ActiveX controls are written in Delphi, and if a large number of users of Matlab Version 7 regard support for ActiveX controls as an important feature, something will get done at one end or the other.  On the other hand, the difficulty with hosting Delphi ActiveX controls under Python has come up and has been identified as not having a simple solution given the use of the MFC library to implement ActiveX support.

I also regard to proposed solution to the Delphi multiple listeners problem as a form of research into object-oriented programming – what are the capabilities as well as limitations to extending a base class by inheritance to change its behavior?  What are the advantages and disadvantages of encapsulation?