Wednesday, January 30, 2008

Fun with COM Part 1 - Interfaces

Early in our development process we encountered a problem with our COM objects; namely, they are not serializable and therefore cannot be (safely) cached. Fortunately, all of the objects used within VB6 implement a VB6 a result, the decision was made to create .Net classes can implement these VB6 interfaces, so they can (among other things) be made cacheable. It was not an easy task, however, thanks to some oversights in the VB6 code and a generally stupid decision on the part of Microsoft: Visual Basic 6 defaults all parameters to being passed by reference if not explicitly specified. As a result, we have many, many places in our VB6 interface which look like this:
Private Property Get IField_ControlIdx() As Integer
IField_ControlIdx = m_intControlIdx
End Property

Private Property Let IField_ControlIdx(p_intControlIdx As Integer)
m_intControlIdx = p_intControlIdx
End Property

When implemented in VB.Net, that (should) look like this:
Public Property ControlIdx() As Short Implements ...
returns m_intControlIdx
End Get
Set(ByRef value As Short)
m_controlIdx = value
End Set
End Property

Unfortunately, that doesn't compile, instead giving the following error: 'Set' parameter cannot be declared 'ByRef'. Changing 'ByRef' to 'ByVal' makes the property valid, but not the class, which gives the following compile error: [PropertyName] cannot implement [Propertyname] because there is no matching property on interface [Interface]. Stuck.

Microsoft has the following article on this issue:

Their solution? "To resolve this bug, rebuild the COM library so that it passes arguments to property procedures by value instead of by reference." Well sure, of course THAT would work, but that comes with its own our case, the fact that it would mean roughly 50 VB6 classes that would require updating and testing. Our interface is shared with our windows application, which is well past the point of a core change, so this wasn't an option.

Fortunately, there is another solution: C#.

Because the parameters are passed by reference, the C# interpreter sees the set properties as methods:
public void set_ControlIdx(ref short value)

Alternatively, you can implement the interface explicitly:
void [InterfaceName].set_ControlIdx(ref short value)

This is how we handled it. By implementing the interface explicitly, we were able to create our own, valid public properties, while hiding the ugly ByRef versions. To keep things consistent, we had the explicit methods reference the cleaner, public methods, keeping the logic in the same place.
public short ControlIdx
get { return m_controlIdx; }
set { m_controlIdx = value; }

short [Interface].get_controlIdx()
return this.ControlIdx;

void [Interface].set_controlIdx(ref short value)
this.ControlIdx = value;

Hope that helps!

Additional note for VB developers:

We actually took an additional step in our C# classes. In order to keep the bulk of our logic in Visual Basic (I personally prefer C#, but this is a VB shop), we actually made our C# classes abstract, and our public properties are abstract as well.

Then, we created matching VB.Net classes which inherit from our C# classes and put the logic there. The result is an extra file, and an external class library, but we were able to implement our VB6 interface without changing it, and while still keeping the actual logic in VB.Net.

No comments: