I am working on migrating legacy VB6 COM library that registered with regsvr32 and used primarily in vbscript and ASP Classic.
The new .NET 4.8 substitution library is registered with regasm.
All was good until I came across some features that I can not directly migrate because I have not enough knowledge in this field.
Legacy VB6 library has the following Typelibrary code (from OleView, reduced for convenience):
[id(0x00068), propget]
VARIANT_BOOL IncomeOverride([in] short index);
[id(0x00068), propput]
void IncomeOverride(
[in] short index,
[in] VARIANT_BOOL rhs
);
[id(0x00069), propget]
CY IncomeOverrideAmt([in] short index);
[id(0x00069), propput]
void IncomeOverrideAmt(
[in] short index,
[in] CY rhs
);
The caller is using these methods like this:
oTest.IncomeOverride(1) = True
oTest.IncomeOverrideAmt(1) = 12
Firstly I have tried to use properties. But since there is no parameterized properties available in C#, I had to try out the methods like this:
[DispId(0x00068)]
bool IncomeOverride([In] short index);
[DispId(0x00068)]
void IncomeOverride([In] short index, [In] bool v);
[DispId(0x00069)]
decimal IncomeOverrideAmt([In] short index);
[DispId(0x00069)]
void IncomeOverrideAmt([In] short index, [In] decimal v);
That compiles but does not work because regasm makes a warning on registering and refuses generating correct tlb. The warning as follows:
Type library exporter warning processing 'TestLibrary'. Warning: The type had one or more duplicate DISPIDs specified. The duplicate DISPIDs were ignored.
The produced typelibrary is far from what is needed:
[id(0x0001)]
VARIANT_BOOL IncomeOverride([in] short index);
[id(0x0002)]
void IncomeOverride_2(
[in] short index,
[in] VARIANT_BOOL v
);
[id(0x0003)]
CY IncomeOverrideAmt([in] short index);
[id(0x0004)]
void IncomeOverrideAmt_2(
[in] short index,
[in] CY v
);
Next iteration was to use indexers.
[DispId(0x00068)]
[System.Runtime.CompilerServices.IndexerName("IncomeOverride")]
bool this[short index]
{
[return: MarshalAs(UnmanagedType.Currency)]
get;
[param: In, MarshalAs(UnmanagedType.Currency)]
set;
}
[DispId(0x00069)]
[System.Runtime.CompilerServices.IndexerName("IncomeOverrideAmt")]
decimal this[short index]
{
[return: MarshalAs(UnmanagedType.Currency)]
get;
[param: In, MarshalAs(UnmanagedType.Currency)]
set;
}
That is actually a good solution but unfortunately it does not even compile because you can not have multiple indexers with different IndexerName values in a single interface/class.
The last attempt to try to accomodate such feature was to use properties that return arrays/objects with indexers:
[DispId(0x00068)]
bool[] IncomeOverride { get; }
[DispId(0x00069)]
decimal[] IncomeOverrideAmt { get; }
[DispId(0x00068)]
SomeClassWithBoolIndexer IncomeOverride { get; }
[DispId(0x00069)]
SomeClassWithDecimalIndexer IncomeOverrideAmt { get; }
That compiles and registers but since it is generating incorrect typelibrary - it can not be used in vbscript/VB6/ASP Classic.
So the question is - how to implement such feature?
Or just emulate?
Writing custom Typelibrary for C# library?
Is this even possible with the means of C#?
Thank in advance.
Update:
Found a similar question here: How to make C# COM class support parameterized properties from VB6
It actually makes most of the work done but unlike the author of that question - it does not work without using .Value for the parameterized property.
Trying to dig in and find out why VBscript does not recognize default property (Value). Unless this - all works like a champ!
But without default property recognition it is dead because requires rewriting tonns of legacy code... we currently do not need binary compat, just to make the late binding (vbscript) work.
Anyone has any idea why it is not working as supposed?
Update 2
Ok as for now two main solutions (apparently none of them working):
1. using PropertyAccessor class
VBScript:
Dim oTestObject: Set oTestObject = CreateObject("TestSharpLib.TestObject")
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]" 'THIS LINE WORKS
oTestObject.IncomeOverride(1).Value = "True" 'THIS LINE WORKS
oTestObject.IncomeOverride(1) = "True" 'THIS LINE DOES NOT WORK
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]"
C#
using System;
using System.Runtime.InteropServices;
using TestVBNET;
namespace TestSharpLib
{
[ComVisible(true)]
[ProgId("TestSharpLib.TestObject")]
[Guid("49067FAC-A1B3-4469-9032-4E958AA7381C")]
public class TestObject: : ITestObject
{
public object IncomeOverride(short index)
{
return new PropertyAccessor(this, index);
}
public class PropertyAccessor
{
TestObject _owner;
short _index;
public PropertyAccessor(TestObject owner, short index)
{
_owner = owner;
_index = index;
}
[DispId(0)]
public string Value {
get => _index.ToString();
set { }
}
}
}
[ComVisible(true)]
[Guid("4C0FDA39-1027-415A-968C-9A6DF9BEB499")]
public interface ITestObject
{
[DispId(1)]
object IncomeOverride(short index);
}
}
Seems like according to Erik Lippert you can not omit default properties in VBScript. https://learn.microsoft.com/en-us/archive/blogs/ericlippert/vbscript-default-property-semantics
2. Using interface declared in VB.NET
VBScript:
Dim oTestObject: Set oTestObject = CreateObject("TestSharpLib.TestObject")
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]" 'THIS LINE NOT WORKING
oTestObject.IncomeOverride(1) = "True" 'THIS LINE NOT WORKING
wsh.echo "oTestObject.IncomeOverride(1): [" & oTestObject.IncomeOverride(1) & "]"
C#
using System;
using System.Runtime.InteropServices;
using TestVBNET;
namespace TestSharpLib
{
[ComVisible(true)]
[ProgId("TestSharpLib.TestObject")]
[Guid("49067FAC-A1B3-4469-9032-4E958AA7381C")]
public class TestObject: IIncomeOverride
{
public string get_IncomeOverride(int index)
{
return index.ToString();
}
public void set_IncomeOverride(int index, string Value)
{
//throw new NotImplementedException();
}
}
}
VB.NET
Imports System.Runtime.InteropServices
<ComVisible(True)>
<Guid("F40144F0-6BA1-4983-B6EE-D593CA0A2F75")>
Public Interface IIncomeOverride
<DispId(1745027127)>
Property IncomeOverride(ByVal index As Integer) As String
End Interface
This solution does not work at all. Although regasm says the dll registered and OleView actually shows correct typelibrary - VBScript does not see IncomeOverride property at all.
c:\Users\Administrator\Desktop\test.vbs(2, 1) Microsoft VBScript runtime error: Object doesn't support this property or method: 'IncomeOverride'
Typelibrary:
[
uuid(f40144f0-6ba1-4983-b6ee-d593ca0a2f75),
dual,
oleautomation
]
interface IIncomeOverride : IDispatch#i {
[id(0x68030037), propget]
HRESULT IncomeOverride(
[in] long index,
[out, retval] BSTR* pRetVal
);
[id(0x68030037), propput]
HRESULT IncomeOverride(
[in] long index,
[in] BSTR pRetVal
);
};
[
uuid(f40144f0-6ba1-4983-b6ee-d593ca0a2f75),
dual
]
dispinterface IIncomeOverride {
methods:
**removed object methods***
[id(0x68030037), propget]
BSTR IncomeOverride([in] long index);
[id(0x68030037), propput]
void IncomeOverride(
[in] long index,
[in] BSTR rhs
);
};
So for now I am still digging in but out of new ideas. Main disappointment was that VBScript does not see implemented (declared in VB.NET interface) properties/methods.
Update 3 (final solution) After some time spent on testing and researching found out that the easier and probably best way to solve this is to use VB.NET proxy class that simulates all needed interface but proxies the logic to C# class.
Implementing it this way solved all problems. Thanks everyone for discussion and proposed solutions.
I would mark question as solved and picked up one of the comments that suggested this solution but can not because comments were hidden/removed.