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?