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.

+4


source to share


3 answers


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; }
}

      

+2


source


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/

+1


source


"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

      

0


source







All Articles