C # COM implements enum without reference MSCORLIB
I am creating a COM interface that I need to allow for use For Each
in both Visual Basic scripting and IEnumVariant
C ++. I figured out that I didn't want the C ++ client app to import mscorlib.tlb.
So far my interface is:
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface ICars : System.Runtime.InteropServices.ComTypes.IEnumVARIANT
{
int Count { get; }
}
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class Cars : ICars
{
int ICars.Count => throw new NotImplementedException();
int IEnumVARIANT.Next(int celt, object[] rgVar, IntPtr pceltFetched)
{
throw new NotImplementedException();
}
int IEnumVARIANT.Skip(int celt)
{
throw new NotImplementedException();
}
int IEnumVARIANT.Reset()
{
throw new NotImplementedException();
}
IEnumVARIANT IEnumVARIANT.Clone()
{
throw new NotImplementedException();
}
}
TlbExp spits out this code:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: carsIEnumerator.tlb
[
uuid(3BBCEAA2-9498-48BF-8053-1CEFB3C1C86F),
version(1.0),
custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, "ClassLibraryIEnumerator, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null")
]
library ClassLibraryIEnumerator
{
// TLib : // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface ICars;
[
odl,
uuid(ABD2A9E4-D5C5-3ED9-88AF-4C310BD5792D),
version(1.0),
dual,
oleautomation,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, "ClassLibraryIEnumerator.ICars")
]
interface ICars : IDispatch {
[id(0x60020000), propget]
HRESULT Count([out, retval] long* pRetVal);
};
how can i avoid this?
Even though I have my own interface and one class (without using any .NET type) the link still exists.
source to share
A declaration like IEnumVARIANT must come from somewhere. This is not a standard type int
that every compiler knows about. If you are creating an IDL yourself, you should use #import "oaidl.idl"
to include a definition. But this cannot work in .NET as the type library exporter does not use IDL. So it comes from where the exporter knows about, mscorlib.tlb
A workaround is to just put the interface declaration in your own code instead of using it in mscorlib. Copy / paste it from Reference source or this:
[Guid("00020404-0000-0000-C000-000000000046")]
[InterfaceTypeAttribute(ComInterfaceType.InterfaceIsIUnknown)]
[ComImport]
public interface IEnumVARIANT
{
[PreserveSig]
int Next(int celt, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0), Out] object[] rgVar, IntPtr pceltFetched);
[PreserveSig]
int Skip(int celt);
[PreserveSig]
int Reset();
IEnumVARIANT Clone();
}
And use YourNamespace.IEnumVARIANT in your ICars declaration.
Declaring your own type of enumerator interface is also a solution, IEnumVARIANT doesn't win any prizes. You can give up picky methods that no one ever uses, and you can make it text-safe. A valid alternative if you also have control over the client code or shouldn't be content foreach
with a scripting language. Consider:
[ComVisible(true)]
public interface ICarEnumerator {
ICar Next();
}
And ICarEnumerator GetCars()
in the ICars interface.
Last but not least, consider not executing the iterator at all. Just make it look like an array in your client code:
[ComVisible(true)]
public interface ICars
{
int Count { get; }
ICar this[int index] { get; }
}
source to share
I had the same problem / need and found this good article.
https://limbioliong.wordpress.com/2011/10/28/exposing-an-enumerator-from-managed-code-to-com/
source to share
"The thing is, I don't want the C ++ client application to import mscorlib.tlb."
This is not possible as you are creating your COM class with .NET, which automatically brings mscorlib.tlb and mscoree.dll into play. Try this with a simple object that can only add two integers.
As Hans Passant pointed out, you don't need an interface at all IEnumVARIANT
. Any COM collection must be based on a C # collection, for example List<T>
. This C # collection has a method GetEnumeration()
that spits out an object IEnumeration
that serves as a IEnumVARIANT
COM. All you have to do is include IEnumerator GetEnumerator();
in the interface and delegate the implementation to GetEnumeration()
the C # collection method .
I am showing this in a complete example. Consider a class bank that manages a set of accounts. I need points for the Bank, Account and AllAccounts collection.
I'll start with the key account AllAccounts:
//AllAccounts.cs:
using System;
using System.Collections;
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAllAccounts
{
int Count{ get; }
[DispId(0)]
IAccount Item(int i);
[DispId(-4)]
IEnumerator GetEnumerator();
Account AddAccount();
void RemoveAccount(int i);
void ClearAllAccounts();
}
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class AllAccounts:IAllAccounts
{
private AllAccounts(){ } // private constructor, coclass noncreatable
private List<IAccount> Al = new List<IAccount>();
public static AllAccounts MakeAllAccounts() { return new AllAccounts(); }
//public, but not exposed to COM
public IEnumerator GetEnumerator() { return Al.GetEnumerator(); }
public int Count { get { return Al.Count; } }
public IAccount Item(int i) { return (IAccount)Al[i - 1]; }
public Account AddAccount() { Account acc = Account.MakeAccount();
Al.Add(acc); return acc; }
public void RemoveAccount(int i) { Al.RemoveAt(i - 1); }
public void ClearAllAccounts() { Al.Clear(); }
}
}
Values DispId
equal to 0 and -4 are required for the default Item
method and for the method GetEnumerator()
. Two other files:
Account.cs:
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IAccount
{
double Balance { get; } // A property
void Deposit(double b); // A method
}
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class Account:IAccount
{
private double mBalance = 0;
private Account() { } // private constructor, coclass noncreatable
public static Account MakeAccount() { return new Account(); }
//MakeAccount is not exposed to COM, but can be used by other classes
public double Balance { get { return mBalance; } }
public void Deposit(double b) { mBalance += b; }
}
}
Bank.cs:
using System.Runtime.InteropServices;
namespace BankServerCSharp
{
[ComVisible(true)] // This is mandatory.
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface IBank { IAllAccounts Accounts { get; } }
[ComVisible(true)] // This is mandatory.
[ClassInterface(ClassInterfaceType.None)]
public class Bank:IBank
{
private readonly AllAccounts All;
public Bank() { All = AllAccounts.MakeAllAccounts(); }
public IAllAccounts Accounts { get { return All; } }
}
}
You must register the server with the x64 version of Regasm.
Server test with C ++:
#include "stdafx.h"
#include <string>
#import "D:\Aktuell\CSharpProjects\BankServerCSharp\BankServerCSharp\bin\Release\BankServerCSharp.tlb"
//this is the path of my C# project bin\Release folder
inline void TESTHR(HRESULT x) { if FAILED(x) _com_issue_errorex(x, nullptr, ID_NULL);}
int main()
{
try
{
TESTHR(CoInitialize(0));
BankServerCSharp::IBankPtr BankPtr = nullptr;
TESTHR(BankPtr.CreateInstance("BankServerCSharp.Bank"));
BankServerCSharp::IAllAccountsPtr AllPtr = BankPtr->Accounts;
BankServerCSharp::IAccountPtr FirstAccountPtr = AllPtr->AddAccount();
TESTHR(FirstAccountPtr->Deposit(47.11));
AllPtr->AddAccount();
TESTHR(AllPtr->Item[2]->Deposit(4711));
CStringW out, add;
for (int i = 1; i <= AllPtr->Count; i++)
{
add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
out += add;
}
out += L"\n";
AllPtr->RemoveAccount(1);
for (int i = 1; i <= AllPtr->Count; i++)
{
add.Format(L"Balance of account %d: %.2f.\n", i, AllPtr->Item[i]->Balance);
out += add;
}
AllPtr->ClearAllAccounts();
add.Format(L"Number of accounts: %ld.\n", AllPtr->Count);
out += L"\n" + add;
MessageBoxW(NULL, out, L"Result", MB_OK);
//Raise an exception:
AllPtr->RemoveAccount(1);
}
catch (const _com_error& e)
{
MessageBoxW(NULL, L"Oops! Index out of range!", L"Error", MB_OK);
}
CoUninitialize();// Uninitialize COM
return 0;
}
Note: Item
is a vector in C ++. I have no idea how to change it to its normal functional form, i.e. Item(i)
instead of Item[i]
.
In VBA, you can use your favorite loop For Each
:
Sub CSharpBankTest()
On Error GoTo Oops
Dim Out As String
Dim Bank As New BankServerCSharp.Bank 'New!
Dim AllAccounts As BankServerCSharp.AllAccounts 'No New!
Set AllAccounts = Bank.Accounts
Dim AccountOne As BankServerCSharp.Account 'No New
Set AccountOne = AllAccounts.AddAccount
AccountOne.Deposit 47.11
AllAccounts.AddAccount
AllAccounts(2).Deposit 4711
Dim i As Long
Dim ac As BankServerCSharp.Account
For Each ac In AllAccounts
i = i + 1
Out = Out & "Balance of account " & i & ": " & ac.Balance & vbNewLine
Next
Exit Sub
Oops:
MsgBox "Error Message : " & Err.Description, vbOKOnly, "Error"
End Sub
source to share