I have an issue with C# and COM.
[Guid("f7d936ba-d816-48d2-9bfc-c18be6873b4d")]
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
public class Process : IProcess
{
public Process()
{
}
public int UpdateBalance(string accountNumber, string adminEventDescription, decimal curAmount)
{
return 10;
}
}
[ComVisible(true)]
[Guid("5c640a0f-0dce-47d4-87df-07cee3b9a1f9")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
public interface IProcess
{
int UpdateBalance(string accountNumber, string adminEventDescription, decimal curAmount);
}
And the VB code
Private Sub Command1_Click()
Dim test As Object
Set test = New Forwardslash_PlayerTrackingSystem_Api.Process
End Sub
I get the following,
ActiveX component can't create object?
Any ideas on how to fix the issue?
Have you ticked the "Register for COM interop" box in the project properties?
Do you have the ProgID Forwardslash_PlayerTrackingSystem_Api.Process defined in the C# source as well? Your example code does not seem to include it. (Or are you working with an existing type library and creating the object in VB by GUID somehow?)
And is the C# component registered correctly in the registry on the machine where the VB code runs? See the answer by Paolo for a way to have VisualStudio do this for you when you build and/or register it yourself using the regasm.exe tool. This tool is equivalent to regsrv32.exe for "real" COM objects, but then registers an appropriately built .NET assembly in the registry for use from COM.
Your [InterfaceType] attribute is wrong. VB6 requires an IDispatch interface, it cannot handle an IUnknown interface. It likes ComInterfaceType.InterfaceIsDual best, that produces a full type library, enables IntelliSense in the VB6 editor and is roughly a 1000 times faster than the late-bound IDispatch.
Using regasm's /codebase switch is mandatory if the assembly is not registered in GAC.
Related
Given a specific class:
public class Klass
{
public int value;
public void doSomething(){
return;
}
}
To make said class COM visible, as far as I know, one needs to do a few things:
Import System.Runtime.InteropServices
Create an interface for the class.
Extend the interface created.
Create 2 unique GUIDs, one for the Interface and another for the class.
Add Dispatch IDs to the interface.
Producing something like:
[Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83F")]
public interface IKlass
{
[DispId(0)]
public int value;
[DispId(1)]
public void doSomething();
}
[Guid("0D53A3E8-E51A-49C7-944E-E72A2064F938"),
ClassInterface(ClassInterfaceType.None)]
public class Klass : IKlass
{
public int value;
public void doSomething(){
return;
}
}
The resulting code looks utterly gross in my opinion... The question is, is there a simple cleaner method of creating these COM interfaces? I can imagine modifying the build process myself to give a interop feature. E.G.
public interop class Klass
{
public interop int value;
//...
}
However, this is non-standard, which has it's issues as well. Is there anything built-in to Visual Studio / C# that I can use to make building COM interfaces easier/cleaner?
As suggested by Zohar Peled the best way is to use RegAsm.exe:
Create some C# class library "TestProject":
using System.Windows.Forms;
namespace TestProject
{
// Note. Only public classes are exported to COM!
public class Test
{
// Note. Only public methods are exported to COM!
public void testIt() {
MessageBox.Show("Yellow world");
}
}
}
IMPORTANT:
Only public classes are exported to COM. And only public methods of these classes are available via a COM object instance.
Sign the project.
In AssemblyInfo.cs set [assembly: ComVisible(false)] to [assembly: ComVisible(true)]. Note: You can also use attribute [ComVisible(true)] before each class you want to expose to COM. This just sets the default to true making it easier to work with if building an API
Build the project.
Run regasm. Remember to use the correct version of Regasm (32-bit/64-bit) and the version for your .NET framework:
# .NET v4.5 64-bit
"C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe" -tlb -codebase "C:\Users\sancarn\Desktop\tbd\TestProject\TestProject\bin\Debug\TestProject.dll" -verbose
# .NET v4.5 32-bit
"C:\Windows\Microsoft.NET\Framework\v4.0.30319\RegAsm.exe" -tlb -codebase "C:\Users\sancarn\Desktop\tbd\TestProject\TestProject\bin\Debug\TestProject.dll" -verbose
...
Regasm should output something like this:
Microsoft .NET Framework Assembly Registration Utility version 4.7.3056.0
for Microsoft .NET Framework version 4.7.3056.0
Copyright (C) Microsoft Corporation. All rights reserved.
Types registered successfully
Type 'TestProject.Test' exported.
Assembly exported to 'C:\Users\sancarn\Desktop\tbd\TestProject\TestProject\bin\Debug\TestProject.tlb', and the type library was registered successfully
Now you can test the file in VBScript for example:
Dim o As Object
Set o = CreateObject("TestProject.Test")
Call o.testIt
Sancarn answers your question, but note that this makes ALL COM-compatible classes in your project COM-visible as well, which you might not want (see here and here). If you do not explicitly set the UUIDs you are opening yourself up to problems when you deploy if you access the classes with early-bound clients like VB or VBA (not VBScript, which is late-bound).
Yes it's not "clean" but neither is COM, especially when you want to expose it to late-binding clients live VBScript.
I would also change your public field to a property, which is more standard for public members:
[Guid("EAA4976A-45C3-4BC5-BC0B-E474F4C3C83F")]
public interface IKlass
{
[DispId(0)]
public int value {get; set;}
[DispId(1)]
public void doSomething();
}
I have written a simple COM object in C# with only one method, which is called GetMac. I can't get it to work. I am trying to access it from a legacy Borland C++ Builder 4 (BCB4) application, which I know is old, and not used much anymore, but I am able to access other COM objects from it fine.
The Borland development machine is running Windows XP, so I make the C# COM object target the .NET 4.0 framework. I copied the DLL and PDB file over from the C# Visual Studio machine to the XP machine. I registered it via the following command:
"%WINDIR%\Microsoft.NET\Framework\v4.0.30319\regasm.exe" TRSDotNetCOM.dll /tlb /nologo /codebase
I am able to instantiate the COM object (class) fine via the following line of code:
Variant TDN = CreateOleObject("TRSDotNetCOM.TRSCOM_Class");
If I change the name string, it doesn't work, so I know I have this part correct.
However, when I try to call the method as follows:
MacV = TDN.OleFunction(funcNameV,counterV,macKeyV);
... I get a runtime exception (unfortunately, there's an issue with BCB4's exception handling for OLE calls, so the only info the debugger gives me is "Exception Occurred").
Since I am able to call other COM objects from the same BCB4 application in the same manner, I don't think the problem is with my C++ code. I think it is an issue with either the C#-created COM DLL, or the registration thereof.
To explore this, I used Microsoft OLE/COM Object Viewer to browse my system for the OLE object. I was able to find my object as "TRSDotNetCOM.TRSCOM_Class", as expected.
I'm brand new at using the OLE/COM Object Viewer, so I hope I am looking at the right things below:
When I expand the class, I see the following:
I right-clicked on _Object and chose "View", then "View Type Info". Then, the pane on the right shows:
[ uuid(65074F7F-63C0-304E-AF0A-D51741CB4A8D), hidden, dual, nonextensible,
custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, "System.Object")
] dispinterface _Object {
properties:
methods:
[id(00000000), propget,
custom({54FC8F55-38DE-4703-9C4E-250351302B1C}, "1")]
BSTR ToString();
[id(0x60020001)]
VARIANT_BOOL Equals([in] VARIANT obj);
[id(0x60020002)]
long GetHashCode();
[id(0x60020003)]
_Type* GetType(); };
When I expand the tree on the left, this is what I see:
I do not see my method "GetMac" listed anywhere in there. So, I'm thinking that somehow the method is not visible to COM, or that it's not getting registered via regasm.
Here is the source for the COM object:
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace TRSDotNetCOM
{
[Guid("80ef9acd-3a75-4fcd-b841-11199d827e8f")]
public interface TRSCOM_Interface
{
[DispId(1)]
string GetMac(string counter, string macKey);
}
// Events interface Database_COMObjectEvents
[Guid("67bd8422-9641-4675-acda-3dfc3c911a07"),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface TRSCOM_Events
{
}
[Guid("854dee72-83a7-4902-ab50-5c7a73a7e17d"),
ClassInterface(ClassInterfaceType.None),
ComVisible(true),
ComSourceInterfaces(typeof(TRSCOM_Events))]
public class TRSCOM_Class : TRSCOM_Interface
{
public TRSCOM_Class()
{
}
[ComVisible(true)]
public string GetMac(string counter, string macKey)
{
// convert counter to bytes
var counterBytes = Encoding.UTF8.GetBytes(counter);
// import AES 128 MAC_KEY
byte[] macKeyBytes = Convert.FromBase64String(macKey);
var hmac = new HMACSHA256(macKeyBytes);
var macBytes = hmac.ComputeHash(counterBytes);
var retval = Convert.ToBase64String(macBytes);
return retval;
}
}
}
I did make sure and go into the project properties and check the "Register for COM interop" checkbox. I also generated a Secure Name file with the "sn" utility, and loaded the file in the Signing section of settings.
So...
1) Am I looking in the correct place in the OLE/COM Object Viewer for my method?
2) If so, why would my method not be visible or not get registered?
3) Any ideas of what else could be wrong?
UPDATE: Here is the updated code with Joe W's and Paulo's suggestions. (It still does not work however)
using System;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
namespace TRSDotNetCOM
{
[Guid("80ef9acd-3a75-4fcd-b841-11199d827e8f"),
ComVisible(true)]
public interface TRSCOM_Interface
{
[DispId(1)]
string GetMac(string counter, string macKey);
}
// Events interface Database_COMObjectEvents
[Guid("67bd8422-9641-4675-acda-3dfc3c911a07"),
ComImport,
ComVisible(true),
InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface TRSCOM_Events
{
}
[Guid("854dee72-83a7-4902-ab50-5c7a73a7e17d"),
ClassInterface(ClassInterfaceType.None),
ComDefaultInterface(typeof(TRSCOM_Interface)),
ComVisible(true),
ComSourceInterfaces(typeof(TRSCOM_Events))]
public class TRSCOM_Class : TRSCOM_Interface
{
public TRSCOM_Class()
{
}
public string GetMac(string counter, string macKey)
{
// convert counter to bytes
var counterBytes = Encoding.UTF8.GetBytes(counter);
// import AES 128 MAC_KEY
byte[] macKeyBytes = Convert.FromBase64String(macKey);
var hmac = new HMACSHA256(macKeyBytes);
var macBytes = hmac.ComputeHash(counterBytes);
var retval = Convert.ToBase64String(macBytes);
return retval;
}
}
}
You're missing just a few bits.
Declare your interfaces as ComVisible:
[ComVisible(true)]
public interface TRSCOM_Interface
If your assembly is already COM visible by default (you can check this in the project's properties or typically in AssemblyInfo.cs), you don't need to do this, but it does no harm and it'll keep the interface available for regasm.exe and tlbexp.exe in case you revert this configuration.
Declare the events interface as ComImport:
[ComImport]
public interface TRSCOM_Events
My guess here is that this interface is defined outside your C# project, probably by the BCB4 application or one of its modules.
If my guess is wrong and your C# project is the one defining this interface, then [ComVisible(true)].
If this interface has event methods, you then implement then as events in the class.
Finally, to avoid having another interface exported for your class, you may want to add the ClassInterface attribute:
[ClassInterface(ClassInterfaceType.None)]
public class TRSCOM_Class : TRSCOM_Interface
This way, you're telling that TRSCOM_Interface is your class default interface, as it is the first one you implement, and regasm.exe /tlb won't generate a class interface.
Depending on the order of implemented interfaces is not reassuring, so you can also use the ComDefaultInterface attribute:
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(TRSCOM_Interface))]
public class TRSCOM_Class : TRSCOM_Interface
Now, you can have any order in the implemented interfaces list without worrying about changing the default class interface.
That is the first time I have ever seen a method declared ComVisible. I would forgo that and instead declare the TRSCOM_Interface interface ComVisible.
I've been following a set of tutorials (like this one) to create a simple COM server object.
Say I've got a simple hello world:
[ComVisible(true)]
[Guid("392930B3-9CD0-4247-8C69-83168D1C8F77")]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("nathanr.HellowWorldCom")]
class HelloWorldCom : IHelloWorldCom
{
public int HelloWorld()
{
Console.WriteLine("Hello World!");
return 1;
}
}
With just as simple an interface:
[ComVisible(true)]
[Guid("C08205BE-1393-4070-AE57-FA47F0D653C3")]
interface IHelloWorldCom
{
[DispId(1)]
int HelloWorld();
}
And of course, can't forget the AssemblyInfo.cs file:
...
[assembly: ComVisible(true)]
[assembly: AssemblyKeyFile("HelloWorldCOM.snk")]
...
The problem is when I build the HelloWorldCom.dll and try to register it, regasm just sticks its tongue out at me:
RegAsm : warning RA00000 : No types were registered.
And just to cover my bases I cracked open Regedit and did a search for the ProgID. It wasn't there, which honestly wasn't a surprise.
Am I missing something obvious?
This whole test project is part of a larger (actually useful) set up which is also failing to register.
This will be a very long answer: add public
public class HelloWorldCom : IHelloWorldCom
There might be couple of issues:
You are selecting an incorrect .net framework. In my case, I was trying to register the dll with 2.0 framework, as opposed to 64 bit 4.0 framework
Your DLL is not publicly exposed.
Hope that resolves your issue.
Cheers
I am trying to call a C# dll from QTP (uses vbscript). I have tried a number of things with no success:
Visual Studio 2010
Create C# class libary (st.dll)
code:
using System;
using System.Collections.Generic;
using System.Text;
namespace st
{
public class Class1
{
public static int GetValue()
{
return 34;
}
}
}
regasm /codebase st.dll
fails 'because it is not a valid .NET assembly'
In QTP/vbscript, I have tried
extern.Declare micInteger, "GetValue", "e:\st.dll", "GetValue"
Returns message: 'Invalid procedure call or argument'
Regardless of QTP, I would greatly appreciate any insight on how to call the c# dll from a .vbs file.
I was able to get this working by doing the following:
Create a new C# dll in VS 2010.
namespace st4
{
public class st4_functions
{
public int GetValue()
{
return 34;
}
}
}
In QTP I added the following lines:
Set obj = DotNetFactory.CreateInstance("st4.st4_functions", "c:\\st4.dll")
MsgBox obj.GetValue()
Thanks to all that responded to my problem. Though I did not do the COM solution, it got me thinking that I could stay with .NET and led to this solution. Good job all!
EDIT:
I created a blog post to detail the steps and provide additional information:
http://www.solutionmaniacs.com/blog/2012/5/29/qtp-calling-c-dll-in-vbscript.html
As Marc said, but I think it merits an answer. If you ensure that your dll will be available though the COM mechanics, your script should be able to call into it with things like CreateObject.
How to register .NET assembly for COM interop
Your function is static. Static class members can't be matched up to interface members, and if it can't implement a .NET interface then it certainly won't implement a COM interface.
I've made a simple C# DLL (that's part of a much larger project) using VS2005. I need to use the DLL in Excel via VBA code so I am using COM Interop on the assembly. I am trying to make the build process automatically generate the necessary TLB file so that I don't need to go to the command line and use regasm after every build.
My problem is that although the DLL compiles and builds fine, it does not generate a TLB file. Instead, the error in the title prints out in the output box.
I've gotten other DLLs to build TLB files by going to the project's properties in VS2005 -> Build -> Output -> Check "Register for COM interop". Also I have [assembly: ComVisible(true)] in the AssemblyInfo.cs.
Here's the summary of the source for the problem DLL and the DLL that it references for a return type:
using System;
using System.IO;
using System.Runtime.InteropServices;
using SymbolTable;
namespace ProblemLibrary
{
public class Foo
{
public Foo(string filename)
{
...
}
// method to read a text file into a SymbolTable
public SymbolTable BuildDataSet(string[] selected)
{
...
}
}
}
Here is a summary of SymbolTable.dll. It holds a return type that ProblemLibrary uses.
using System;
using System.Collections.Generic;
namespace SymbolTable
{
public class SymbolTable
{
readonly Dictionary<SymbolInfoStub, string> _symbols = new Dictionary<SymbolInfoStub, string>();
/*methods that interact with Dictionary snipped*/
}
}
You need to have ctor without any params.
You should have GuidAttribute and ProgIdAttribute around the classes.
Its better to mark the assembly as ComVisible(false) and mark explicitly the classes that need export.
Use interfaces for your classes.
Make sure the you have GuidAttribute in the assembly level.
[Guid("<PUT-GUID-HERE-1>")]
[ComVisible(true)]
interface IFoo
{
void DoFoo();
}
[Guid("<PUT-GUID-HERE-2>")]
[ComVisible(true)]
[ProgId("ProgId.Foo")]
class Foo : IFoo
{
public void DoFoo()
{
}
}
In the AssemblyInfo.cs file, make sure you have the following:
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(true)]
UPDATE:
Read: How can I make use of .NET objects from within Excel VBA?
Which links to:
http://richnewman.wordpress.com/2007/04/15/a-beginner%E2%80%99s-guide-to-calling-a-net-library-from-excel/
I saw a similar problem. I got an error like:
warning MSB3391: does not contain any
types that can be unregistered for COM
Interop.
I followed all the rules (ComVisible, etc.) but nothing worked.
Solution: I had to put something in the default constructor so that it would not be optimized away. The moment I had something there, the registration finished with no message and the component was visible in the registry.
Interesting note: a friend of mine managed to register the original DLL with the empty default constructor on his machine (64-bit Windows-7, VS2008-Professional, like mine). However, his REGASM.EXE was:
C:\Windows\Microsoft.NET\Framework64\v2.0.50727\regasm.exe
while mine was:
C:\Windows\Microsoft.NET\Framework64\v4.0.30319\RegAsm.exe
So it could be some difference between versions of the .NET framework - maybe the later version is optimizing too much and the REGASM does not account for that.