COM/ActiveX
COM integration varies across Team Developer generations. Ice Porter preserves the SAL-facing wrapper structure while generating .NET interop declarations and separating interface contracts from CoClass implementations.
A dual COM interface exposes both IDispatch and a vtable-based interface derived from IUnknown. The managed declaration must match the component's actual type library; adding InterfaceIsDual cannot create dual-interface support in an existing component. Performance and reliability depend on correct declarations, lifetime, threading, and the COM server itself.
The ActiveX examples below concern Windows desktop hosting. A server-side COM library does not make an ActiveX UI available in a browser. Inventory each dependency before a PPJ Web migration.
ActiveX Controls
PPJ Desktop can host supported ActiveX controls through Windows Forms AxHost. Generic OLE document embedding is not implemented by the same PPJ APIs. Confirm the control's registration, license requirements, process architecture, and apartment model on the deployment machine.
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. In the source outline, method implementations can be located in Interface items.
Ice Porter separates the implementation from the interface contract 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(), separating the contract from its implementation. Customer is now a managed implementation of a COM object that can be exposed to other systems supporting automation through IDispatch or vtable calls through a correctly declared dual interface.
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 runtime.
Structure of a COM client class
The generated client code separates the COM object (CoClass) from the interfaces it exposes. An interface is a contract, not a second COM object. 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 generated initialization code 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.
An incorrect or outdated interface GUID can cause activation or QueryInterface failures. Use the IID from the installed component's type library. Do not substitute the universal IDispatch IID while keeping a dual-interface declaration: the GUID and invocation model must describe the same interface.
Deployment and Validation
Check CLSID and IID values, member ordering, DISPIDs, parameter directions, string marshalling, and HRESULT handling. For in-process components, match the COM DLL and application bitness. Test events and object release as well as successful method calls.
The snippets are structural excerpts; COM exposure also depends on visibility, registration, and the .NET target. See Microsoft's COM interop overview.