COM/ActiveX

COM and ActiveX support has been added to SQLWindows/Team Developer in two fundamentally different ways: 1.5 and 2.0. Starting from 2.0, SQLWindows/Team Developer started also supporting COM servers, and the entire COM implementation was changed by adding new outline item types. However, Gupta's developers made a conceptual mistake at the time and confused interfaces with classes. For this reason, SQLWindows/Team Developer code that exposes COM servers has the implementation in the Interface item instead of the CoClass item.

Also, SQLWindows/Team Developer COM client support only works for the IDispatch interface (automation objects) and cannot support the faster and more direct dual interfaces. Basically, Team Developer can only call automation objects via the IDispatch interface. Another problem is that when COM support was added, Team Developer's error reporting system was not adapted. As a result, COM client methods are called using a more low level approach where the method call (or property setter/getter) always returns the error code instead of the return value.

For these reasons, Ice Porter generates the same wrappers as the original SAL code, but the calls are dispatched through a real COM interface declared directly in the code. Also, when porting COM servers, Ice Porter fixes the interface mistake and moves the code implementation to the CoClass, with the added advantage of generating real COM dual interfaces.

After the conversion, COM code (both client and server) is much faster, robust and reliable than the original SAL code.

ActiveX Controls

.NET and the PPJ Framework fully support ActiveX controls. What is not supported are OLE objects. Using ActiveX controls in .NET is also more stable and reliable than using them in Team Developer.

ActiveX generation is a complex feature. It uses at the same time .NET Interop, PPJ Multiple Inheritance, Ice Porter COM Client Generation, .NET Events. In order to generate a viable ActiveX control in the ported application, Ice Porter must generate:

  • ActiveX control derived from SalActiveX (which is derived from AxHost)

  • COM Client code to call the control's methods

  • Multiple Inheritance wrappers in the ActiveX control's class

  • Public events declarations

  • COM source interface to hook up the events

  • Event handlers

Everything is inside the ActiveX class, like this:

[ComSourceInterfaces(typeof(AX_MSACAL_Calendar_DCalendarEvents))]
public class AX_MSACAL_Calendar : SalActiveX, AX_MSACAL_Calendar_DCalendarEvents
{
         
 internal MSACAL_Calendar _CoClass = new MSACAL_Calendar();
         
 public AX_MSACAL_Calendar(): base("{8E27C92B-1264-101C-8A2F-040224009C02}")
 {
         InitializeComponent();
 }
 
 public event ClickHandler ClickEvent;
 ...
 
 public SalBoolean NextWeek()
 {
         using (new SalContext(this))
         {
                 return _CoClass.NextWeek();
         }
 }
 ...
         
 void AX_MSACAL_Calendar_DCalendarEvents.Click()
 {
         if (ClickEvent != null)
         {
                 ClickEvent();
         }
 }
 ...
 
 public delegate void ClickHandler();
 ...                
}
 
[ComImport]
[Guid("8E27C92D-1264-101C-8A2F-040224009C02")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface AX_MSACAL_Calendar_DCalendarEvents
{
 [DispId(-600)]
 void Click();
 ...
}

From the top, the ActiveX class implements the source interface exposed by the ActiveX object (AX_MSACAL_Calendar_DCalendarEvents) in order to receive the events notifications, the constructor calls the base constructor in AxHost (.NET's ActiveX support class) passing the GUID of the control, the internal _CoClass member is a reference to ActiveX control object. Then we have the events declarations, the multiple inheritance wrappers that redirect the call to _CoClass, and finally the events implementation code that receives the event notification from the control and dispatches it to the event handler in the application.

Everything is tied together in the generated code, which also means that you also have full control over this part of the code and can adapt it (or fix it) as needed.

The event handlers are generated as individual functions, as shown below:

private void cal_KeyUpEvent(ref short KeyCode, short Shift)
{
 SalNumber KeyCode_ = (SalNumber)KeyCode;
 SalNumber Shift_ = (SalNumber)Shift;
 
 try
 {
         ...
 }
 finally
 {
         KeyCode = (short)KeyCode_;
 }
}

Event handler receive native arguments, just like the COM Server methods, since the call is coming from outside the .NET application. Therefore, Ice Porter generates the necessary code to cast the native types to PPJ types and also to assign back the receive arguments (if any).

Server Objects

COM Server objects are composed of a CoClass item, which is the actual COM object, and one or more Interface items which define the methods and properties exposed by the object. Unfortunately, in Team Developer, the implementation of the methods is also located in the Interface items.

Ice Porter fixes the implementation problem by moving the implementation to the equivalent of the CoClass item and generates real interfaces from the original Interface items. Ice Porter also takes care of generating the code that converts native types to PPJ types and renames all the local variables in the server's functions. The reason for this is that COM servers must expose native types, while SAL code expects PPJ types. Team Developer performed the conversion behind the scenes. In ported code the conversion is visible directly in the code. Additionally, converting native types to PPJ types is not an expensive conversion since PPJ types are based on native types and encapsulate a native type instance.

Structure of a COM Server class

Ice Porter generates a real interface out of the original SAL Interface, looking like this:

 [Guid("312F225C-F03A-48C4-9BEA-A81D94400FAC")]
 public interface ICustomer
 {
         [DispId(0)]
         bool Save(string sFileName);
         
         [DispId(1)]
         string Name { get; }
 }

As you can see, the implementation has been removed and this is now a real interface that can be exposed also as a COM interface supporting both IDispatch and IUnknown.

However, the interface by itself is not a COM server object. The COM server object in SAL is defined in the CoClass item, which aggregates (by deriving from) one or more interfaces. Ice Porter generates the following type of code for CoClass items:

 [ClassInterface(ClassInterfaceType.None)]
 [Guid("8D7DFE12-F301-4DDE-B62A-C5726FB18F3B")]
 public class Customer : ICustomer
 {
         [DispId(0)]
         bool ICustomer.Save(string sFileName)
         {
                 SalString sFileName_ = (SalString)sFileName;
                 ...
                 return false;
         }
         
         [DispId(1)]
         string ICustomer.Name
         {
                 get { return ""; }
         }
 }

As shown in the example above, the implementation of the Save() function has been moved to the explicit implementation of the method in Customer.ICustomer.Save(), fixing the interface implementation problem already mentioned. Customer is not a fully .NET compliant COM object that can be exposed to other systems supporting either automation (IDispatch) or dual interfaces (IUnknown).

Client Objects

COM clients are wrappers that dispatch the calls to the underlying COM object instance. Ice Porter generates the wrapper code following the original SAL application's structure.

Every COM client interface declared in the original SAL application is ported as a class derived from PPJ.Runtime.Com.SalObject. This class contains all the wrapper functions and an inner real COM interface always named COMInterface. All calls are redirected to the internal COMInterface instance, taking advantage of the excellent COM interop support built-in the .NET core system.

Structure of a COM client class

There are always at least two classes for a COM object. The COM object instance and the Interface. The Interface defines the methods and properties implemented and exposed by the COM object instance. In Team Developer, the Interface is a simple Functional Class with wrapper methods that use internal functions to generate and dispatch the COM automation call to the IDispatch interface (Team Developer supports only IDispatch interfaces). For the Interface coming from SAL, Ice Porter generates a real interface using .NET Interop attributes. The COM object instance is created in TD using the COM Proxy item. The COM Proxy contains, hidden in the outline, the GUID of the COM object, while the Interface Functional Class contains the GUID of the interface.

The Interface generated by Ice Porter looks like this:

public class Test__IClient : SalObject
{
 internal COMInterface _Interface = null;
 public SalNumber Test(SalString sArgument)
 {
         try
         {
                 string param1 = (string)sArgument;
                 _Interface.Test(param1);
                 return 0;
         }
         catch (COMException ex)
         {
                 return HandleException(ex);
         }
 }
 
 [Guid("{71ADD5AC-0224-4801-B56D-43AF9C26E916}")]
 [InterfaceType(System.Runtime.InteropServices.ComInterfaceType.InterfaceIsDual)]
 public interface COMInterface
 {
         void Test(string sArgument);
 }
}

Notice that the Test() method is a wrapper that converts PPJ types to native types and then calls the corresponding method on the internal COM interface. The inner COMInterface declaration is the .NET interop declaration necessary to use COM objects. As shown in the sample code, it can also be a dual interface.

The COM Proxy class generated by Ice Porter looks like this:

public class Test_Client : Test__IClient
{
 internal COMObject _CoClass = null;
 
 [ComImport]
 [Guid("{E4FA3700-2993-446a-A259-E6FF0C9CAC47}")]
 public class COMObject
 {
 }
}

The internal COMObject class is the real COM instance created by .NET's interop module. After the object instance is created, the SQLWindows/Team Developer takes care of casting the _CoClass reference to the base classes' COMInterface types and saves a reference into the _Interface members. Which is ultimately the target of the COM calls.

Potential problems and fixes

When using ComInterfaceType.InterfaceIsDual, there is a risk that the original SAL code was incomplete or that the order of the members was changed manually (or generated incorrectly). In this case the dual interface will not work and the call may crash or end up to the wrong member. There are two solutions to this problem: make sure that all the members are correctly declared in the right order; change the interface type to InterfaceIsIDispatch.

Another potential problem is that the correct GUID for the interface is missing in the original SAL code, or it references an outdated interface. There are also to possible fixes for this problem: use the correct GUID; or simply use the universal IDispatch GUID {00020400 0000 0000 c000 000000000046}.

Last updated