I have an object:
public class Shampoo
{
public string Name {get;set;}
public string Id {get;set;}
public string PriceRegion {get;set;}
}
Can I pass this object to a method in C# via Excel DNA:
C# method is:
public static string PassShampoo(Shampoo shampoo)
{
//Some Code...
}
Is this possible from VBA to C# via Excel DNA?
Your .NET library can export and register the Shampoo class as a COM visible type, which you can then instantiate from VBA as Dim sh = New Shampoo(). Similarly other types could be define, like
public class Shopper()
{
public string ReadLabel(Shampoo shampoo)
{
return shampoo.Name;
}
}
and then a New Shopper() could be passed the shampoo:
Dim anne As Shopper = New Shopper()
Dim dove As Shampoo = New Shampoo()
Dim label As String
label = anne.ReadLabel(dove)
The method using Shampoo (Shopper.ReadLabel above) would not be static and would not be available to Excel as a UDF or anything - just as a method on a .NET object onvoked via COM interop from VBA.
So far - no Excel-DNA involved. You could do all of this with a standard .NET assembly that is compiled with the right flags and attributes and registered for COM interop on your machine.
However, Excel-DNA also allows your add-in to be a COM Server. This means that the .xll can host the COM classes you've defined in your library, your add-in can do the COM registration (instead of an installer) without requiring Administrator access, and your COM Objects will live in the same AppDomain as the rest of your add-in. So Excel-DNA helps a bit in gluing things up, but the actual interaction between the VBA code and your .NET assembly is the standard .NET-to-COM interop, which works very well once you've climbed the learning curve a bit.
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.
In visual studio I have an Excel 2010 Add-in project. How can I have that project create the following module:
I know I can save that workbook with that module then use it with my add in. It will be nice if I can have my add-in create that module...
It is possible to create the module. However for this to work the setting to "Trust access to the VB Project model" must be selected in Excel. It throws an error that access is denied if the trust setting is not selected.
using Excel = Microsoft.Office.Interop.Excel;
using VB = Microsoft.Vbe.Interop;
Excel.Application eApp = new Excel.Application();
eApp.Visible = true;
Excel.Workbook eBook = eApp.Workbooks.Add();
VB.VBProject eVBProj = (VB.VBProject)eBook.VBProject;
VB._VBComponent vbModule = eVBProj.VBE.ActiveVBProject.VBComponents.Add(VB.vbext_ComponentType.vbext_ct_StdModule);
String functionText = "Function MyTest()\n";
functionText += "MsgBox \"Hello World\"\n";
functionText += "End Function";
vbModule.CodeModule.AddFromString(functionText);
I dont think that VSTO supports Excel UDF's, the general recommendation is to use Automation Add-in's (as Sid's link suggests).
Another option is to call a managed VSTO function from VBA. Once again this is not recommended but possible.
(Recap of tutorial from link)
Here is any easy way to call Managed functions from VBA.
Create a class with your functions in VSTO
<System.Runtime.InteropServices.ComVisible(True)> _
Public Class MyManagedFunctions
Public Function GetNumber() As Integer
Return 42
End Function
End Class
Wire up your class to VBA in VSTO
Private Sub ThisWorkbook_Open() Handles Me.Open
Me.Application.Run("RegisterCallback", New MyManagedFunctions)
End Sub
Create Hook for managed code and a wrapper for the functions in VBA
In a VBA module in your spreadsheet or document
Dim managedObject As Object
Public Sub RegisterCallback(callback As Object)
Set managedObject = callback
End Sub
Public Function GetNumberFromVSTO() As Integer
GetNumberFromVSTO = managedObject.GetNumber()
End Function
Now you can enter =GetNumberFromVSTO() in a cell, when excel starts the cell value should be 42.
http://blogs.msdn.com/b/pstubbs/archive/2004/12/31/344964.aspx
If what you really want to do is to write .NET UDFs, or a combined .NET application level command and UDF addin then using VSTO is not currently a good solution: I would recommend using either Addin Express (costs) or Excel DNA (free). Both of these allow you to create both .NET XLL UDF addins and Automation UDF addins (XLL UDF addins offer significant performance advantages but with slightly more restricted access to the Excel object model)
A VSTO addin can't create UDF's, so you need to create a separate addin for the functions. Although this addin can be in the same DLL as the VSTO addin, you cannot communicate between the VSTO and the UDF's without special trickery.
I have a blog post about this. It gives you a complete example project that includes VSTO and UDF's.
Here is the basic structure of the UDF itself.
[Guid("3B81B6B7-3AF9-454F-AADF-FAF06E5A98F2")]
[InterfaceType(ComInterfaceType.InterfaceIsDual)]
[ComVisible(true)]
public interface IFunctions
{
int MYINT();
}
[Guid("F58C591D-A22F-49AD-BC21-A086097DC26B")]
[ClassInterface(ClassInterfaceType.None)]
[ComVisible(true)]
public class Functions : IFunctions
{
public int MYINT()
{
return 42;
}
}
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.
I have a simple class library that I use in Excel. Here is a simplification of my class...
using System;
using System.Runtime.InteropServices;
namespace SimpleLibrary
{
[ComVisible(true)]
public interface ISixGenerator
{
int Six();
}
public class SixGenerator : ISixGenerator
{
public int Six()
{
return 6;
}
}
}
In Excel 2007 I would create a macro enabled workbook and add a module with the following code:
Public Function GetSix()
Dim lib As SimpleLibrary.SixGenerator
lib = New SimpleLibrary.SixGenerator
Six = lib.Six
End Function
Then in Excel I could call the function GetSix() and it would return six. This no longer works in Excel 2010 64bit. I get a Run-time error '429': ActiveX component can't create object.
I tried changing the platform target to x64 instead of Any CPU but then my code wouldn't compile unless I unchecked the Register for COM interop option, doing so makes it so my macro enable workbook cannot see SimpleLibrary.dll as it is no longer regsitered.
Any ideas how I can use my library with Excel 2010 64 bit?
You haven't described in detail how your created your .NET assembly. However, there are a certain number of steps required to expose the assembly to COM:
Add the following attributes to your code:
using System;
using System.Runtime.InteropServices;
namespace SimpleLibrary
{
[ComVisible(true)]
[Guid("71F645D0-AA78-4447-BA26-3A2443FDA691")]
public interface ISixGenerator
{
int Six();
}
[ComVisible(true)]
[ProgId("SimpleLibrary.SixGenerator")]
[Guid("8D59E0F6-4AE3-4A6C-A4D9-DFE06EC5A514")]
[ClassInterface(ClassInterfaceType.AutoDispatch)]
public class SixGenerator : ISixGenerator
{
[DispId(1)]
public int Six()
{
return 6;
}
}
}
Your assembly must be signed (Project -> Properties... -> Signing, create a strong key file and check the box to sign the assembly
The following command is necessary to register the assembly (all in one line):
C:\Windows\Microsoft.NET\Framework64\v2.0.50727\RegAsm.exe
SimpleLibrary.dll /tlb SimpleLibrary.tlb /codebase
This creates a .tlb type library file which you will have to reference from your VBA project (Tools -> References -> Browse... in your VBA editor)
Adjust the VBA code:
Public Function GetSix()
Dim lib As SimpleLibrary.SixGenerator
Set lib = New SimpleLibrary.SixGenerator
GetSix = lib.Six
End Function
You will find the steps described in more detail in this article on Microsoft's support database:
How to call a Visual Basic .NET or Visual Basic 2005 assembly from Visual Basic 6.0