What's wrong with my Shared Add-in Constructor? - c#

Good morning, fellows developers:
I'm currently trying to fix several performance issues of an Excel Shared Add-in inherited from a previous developer, basically I'm trying to find how the add-in stuff works internally in Excel, meaning I had searched the net for information an my understanding is:
In the registry the LoadBehaviour should be set to 3
The Excel Workbook during the open event should previously load all add-ins referenced in the VBA Project
Once the document is open my add-in should be available to be used by the VBA code.
Now I add Log4Net to the add-in and curiously enough I had seen the following behaviour
During the Open Event in the Excel Workbook there is a global variable
Public myAddin As Object
Set myAddin = New TradingAddin.TradingAddin
Thus the Contructor of the C# Class is called.
After a couple of seconds the constructor is called one more time and all the IDTExtensibility2 methods OnConnection, OnDisconnection, etc.. are called as expected.
I thought that once Excel loads the Add-in it should be available to the VBE Code and i could write something like
Set myAddin = Application.COMAddins.Item("Trading").Object
But it returns Nothing and calling the Constructor of the Class twice destroy any state saved inside the C# Object that should be available in memory during the life of the Excel Workbook.
Update:
The Platform is Visual Studio 2005 Team Edition and the Target Application is Excel 2003 and the Add-in is a Shared Add-in. I'm not using VSTO.
The actual code I was trying to invoke in VBA is
Set addIn = Application.COMAddIns.Item("K2Trading.K2Trading").Connect
Set managedObject3 = addIn.Object <--- This value that I thought was an Instance of the Add-in is equal to Nothing (NULL)
Set addIn = Application.COMAddIns("K2Trading.K2Trading").Connect
Also changing the LoadBehaviour in the registry to 3 from 2 loads the Add-in the first time correctly firing all Extensibility Event OnConnection, OnDisconecction and the Object Class Constructor, now I need to find a way for the invocation part from VBA of the Add-in, meaning how to connect the Add-in instance to a reference in VBA and from there call all the methods exposed through the Interface of the COM Object????
Also I double checked using ProcMon that the Add-in was found and loaded by Excel as per this link (very useful) http://blogs.msdn.com/dvespa/archive/2008/10/15/troubleshooting-outlook-com-addins-using-procmon.aspx .
Any ideas our perhaps pointing into the right direction?
How could I find out how many instances of the COM Object are loaded? or put into another words could be possible to have a single instance of a COM Object?
TIA,
Pedro
To Mike:
I tried your solution but I'v been facing Unspecified error (Exception from HRESULT: 0x80004005 (E_FAIL)) when executing this code
public void OnConnection(object application, Extensibility.ext_ConnectMode connectMode,
object addInInst, ref System.Array custom)
{
object missing = System.Reflection.Missing.Value;
try
{
if (debug)
{
log.Debug("Connection Mode :" + connectMode);
}
this.excelApp = (Excel.Application)application;
this.addInInstance = addInInst;
Office.COMAddIn addIn = this.excelApp.COMAddIns.Item(ref addInInst);
//addIn.Object = this;
// We connect our Instance of the Add-in to the Arrya of COMAddins of Excel
VBA.Interaction.CallByName(addIn, "Object", VBA.CallType.Let, this);
^
|
The Exception occurs here.
You will noticed this looks a little different from you posted a while ago
public void OnConnection(
object application,
Extensibility.ext_ConnectMode connectMode,
object addInInst,
ref System.Array custom)
{
// Direct call fails b/c ".Object" is a late-bound call:
//
// addInInst.Object = this;
// Reflection fails I believe b/c .Object is a 'let' assigned
// property for reference type, which is very unusual even for
// COM (although legal) and is a foreign concept to .NET. Use
// of the right BindingFlags here *might* work, but I'm not sure:
//
// PropertyInfo propInfo;
// propInfo = addInInst.GetType().GetProperty("Object");
// propInfo.SetValue(addInInst, this, null);
// This works!:
VBA.Interaction.CallByName(addInInst, "Object", VBA.CallType.Let, this);
}
Because addInInst is not pass as a Office.COMAddin but a instance of my class, so
trying to assign to the Object Property is incorrect since it doesn't exists in that class
Also I'm curious about how Excel loads the Add-ins, what I mean by that is based on my observation when I just loaded Excel the OnConnection method didn't execute right away, but until I hit the =AvgCost() function ??
Any ideas?

How could I find out how many
instances of the COM Object are
loaded? or put into another words
could be possible to have a single
instance of a COM Object?
Do you mean COM add-in here? You cannot have the same COM add-in loaded into Excel more than once.
Now I need to find a way for the
invocation part from VBA of the
Add-in, meaning how to connect the
Add-in instance to a reference in VBA
and fro there call all the methods
exposed throught the Interface of the
COM Object????
VBA is all late-bound, it is not pre-compiled into a DLL like a VB6 DLL or a .NET assembly. Therefore, all of your VBA calls will have to be late bound as well. Easiest is to call Excel.Appliction.Run("NameOfYourVbaMacro") to call any macro stored within a standard module within your workbook. (You can access your workbook by name using Excel.Application.Workbooks["NameOfYourAddin.xla"]. You don't need to treat it as an add-in specifically, other than when you force it to load.) You can also use reflection code to access the workbook and worksheet members, if you have code behind either the ThisWorkbook class module or any of the Worksheet class modules.
I thought that once Excel loads the
Add-in it should be available to the
VBE Code and i could write something
like
Set myAddin =
Application.COMAddins.Item("Trading").Object
So, if I understand correctly, you want not only to have your C# managed COM add-in call VBA code (as discussed above), but now you also want to have VBA code to be able to call our C# managed COM add-in? I think this is a ton of complexity that probably needs some re-thinking... but it can be done.
Exposing your managed COM add-in to a COM caller via the ComAddin.Object property is tricky when done from C#, but doable. See the following discussion:
Calling Managed Add-In method from Automation client.
I hope this helps get you going...
Mike
Edit: Response to Pedro's Reply
Pedro,
You will noticed this looks alittle
different from you posted a while
ago...
Yes, your code is different and is why it does not work!
At a minimum, your last line looks incorrect:
VBA.Interaction.CallByName(addIn, "Object", VBA.CallType.Let, this);
Instead, your code should be passing in your class to the 'addInInst':
VBA.Interaction.CallByName(addInInst, "Object", VBA.CallType.Let, this);
And I'm not sure what you are trying to do by this line:
Office.COMAddIn addIn = this.excelApp.COMAddIns.Item(ref addInInst);
That line looks like it should throw an exception. I am very surprised that it does not. But code similar to that -- where you pass in the progId for the COM add-in you wish to access -- is typically used by an external caller that wishes to access your COM Add-in via the .Object property. This is not code that should be used from within the add-in itself.
Because addInInst is not pass as a
Office.COMAddin but a instance of my
class, so trying to assign to the
Object Property is incorrect since it
doesn't exists in that class
I don't understand what you are trying to do here. I don't know if you can pass in any other object other than the instance of the class that implements IDTExtensibility2. At a minimum, whatever class you do pass back must be be a COM visible class by using the correct class and interface attributes. But I think that it is far easier to stick to the standard practice of passing in the class that implements IDTExtensibility2 from within the OnConnection method. That is, pass in your 'this' object reference.
If you want to try fancy things that go beyond the standard approach, that's ok, but I would get a simple example working first. Try to replicate the code that I show in the example Calling Managed Add-In method from Automation client. Once you have that working, you can try to move onto more sophisticated operations. But once you have the simple version working, I think you'll find that this is all that you need.
I hope this helps Pedro,
Mike

Related

How to call a COM object from C#

First, I realize that there are many posts here that discuss this topic. I must have read 20+ of them, easily. But none of them give the answer that I seek.
I have written a tiny C# test COM DLL with a single method in it that prints "I am alive!" in a message box. Using VStudio as admin, I can build and register the COM object. I have successfully called the object from VBA and run the method. And I can see the name of the COM Interface in the VStudio Add Reference / COM dialog box. This all makes me think the object is properly constructed, registered, and usable.
Now I'm trying to call it from a console C# app. Like many others, I'm trying to figure out the equivalent of the VBA "CreateObject("DLLName.ClassName")" code to get hold of the object in C#.
One way is to just add a reference to the DLL to my console app project. I point to the assembly through the Projects section of the Add Reference dialog, not through the COM section of the dialog. Then I can simply say var o = new MyComImplementationClass(); and treat it like any other class. That works, but it means my console app is cheating and not using the COM object through the usual COM GAC interface.
Another way (that doesn't work, but I wish it did), is to add the reference through the COM tab on the Add Reference dialog. I can see it but VS protests that "the XXX.tlb file was exported from a .NET assembly. Add a reference to the assembly instead." Which brings me back to the solution above, which I think means that my app is cheating. (I didn't have to add references to my VBA test app, for example.)
Another way is to use Type.GetTypeFromProgId as shown by this code fragment below. But I can't get that to work either. I must be passing in the incorrect ProgID string - I get the sense it has something to do with registry info and is not the same "DLLName.ClassName" string that I feed CreateObject() in VBA.
public static dynamic ComObjectGet () {
const string progID = "ComExampleDLLName.ComImplementationClassName";
Type foo = Type.GetTypeFromProgID (progID);
dynamic COMobject = Activator.CreateInstance (foo);
return COMobject;
}
Worse yet, on this MSDN example page it says "This method is provided for COM support. Program IDs are not used in Microsoft .NET Framework because they have been superseded by the concept of namespace." So probably I should not be using the GetTypeFromProgID at all.
If it helps any, I can use VSTO in C# to call the MSOffice primary interop assemblies. But they load from the COM tab of the add reference dialogs (which is where I want my COM library to load from).
For clarity, my COM DLL name is ComExampleLibrary.dll. The default namespace is ComExampleNamespace. The interface name is IComInterface, and the implementation classname is ComImplementation. The internal method name is Run.
Could someone give me instructions or a code snippet that does the "right, approved" thing for calling COM objects (not just the ones I write) from C#? Thank you.
Thanks to the people who helped me out, here is the answer. Both GetTypeFromProgID and GetTypeFromCLSID work as shown below. My problem was that I was using "AssemblyName.ClassName" instead of "Namespace.ClassName" in the call to GetTypeFromProgID.
public static dynamic ComObjectGet () {
const string progID = "ComExampleNamespace.ComImplementation";
Type foo = Type.GetTypeFromProgID (progID);
//var bar = Guid.Parse ("99929AA7-0334-4B2D-AC74-5E282A12D06C");
//Type foo = Type.GetTypeFromCLSID (bar);
dynamic COMobject = Activator.CreateInstance (foo);
return COMobject;
}
So my original code was correct, but I was passing in the wrong argument. This snippet is the equivalent of the VBA CreateObject("Namespace.ClassName") call.
I still don't know why I cannot add a reference to the COM item in the COM tab of the Add Reference dialog like I would for any other COM object. I suppose that's a different question.

Preventing multiple instances of a singleton object with excel plugins

I've got two pieces of technology that plug into excel.
One is a COM Addin that implements the IDTExtensibility2 interface.
Another is an RTD server that is implemented via Excel-DNA.
Both of these objects are instantiated by excel. Each of them needs to access a third object at runtime to get data from and push it to excel.
Since I can't hand this object to the excel plugins I've made it a singleton with the hope that each of them could share the same instance.
Unfortunately when running the code, each of them don't see the instance of the singleton object that the other has created.
How can I get both addins to reference the same object?
Let's work under the assumption that both addins need to remain and I'd rather not go to an interprocess communication setup.
TL/DR
Two excel plugins need to share a 3rd object, but making the third object a singleton doesn't work as each excel plugin doesn't see the instance of the third object that the other plugin created.
What is the solution to such a problem?
You add-ins are loading in separate AppDomains. One option is to integrate the COM Add-In into the Excel-DNA add-in. This might be as easy as:
Add your COM add-in code to your Excel-DNA project,
Change your add-in class to derive from ExcelComAddIn (instead of IDTExtensibility2),
Load that class in your AutoOpen via ExcelComAddInHelper.LoadComAddIn(...).
Otherwise you will need some sort of inter-AppDomain communication. You can set up a Marshal-By-Reference object that you set to the Object property of the COMAddIn object corresponding to your loaded add-in, and retrieve that using the COM interfaces from the Excel-DNA add-in.

Marshaling unmanaged dll in C# through ComInterop without registering DLL

I have an unmanaged DLL which I'm currently calling from C# using a COM Class Wrapper.
[ComImport(), Guid("75E81043-CAD5-11D3-800D-00105A5E2FA0")]
public class MyObject { }
[ComImport(), Guid("75E81042-CAD5-11D3-800D-00105A5E2FA0"),
InterfaceType(ComInterfaceType.InterfaceIsDual)]
public interface MyInterface
{
string EncryptString([In, MarshalAs(UnmanagedType.BStr)] string bstrOrginal);
}
Then to call:
MyInterface obj = (MyInterface)new MyObject();
string crypt = obj.EncryptString("something");
This works, the return value is as I expect. However, it requires that the dll is registered with regsvr32.
I'm looking for a way to do this without the requirement of needing to regsvr32. Preferably, by just having a copy of the dll available. It is worth noting, I have the source for the unmanaged dll, and the ability to modify it if necessary.
A shove in the right direction would be greatly appreciated.
I have wanted to do the same thing myself. As Jim mentions it is possible to get the address of DllRegisterServer and call it, but that still modifies your registry with the various entries. If you don't want to do this (for example, you might not have the necessary privileges to write to the Registry), then there is another way.
Any DLL that houses one or more in-process COM objects must expose the DllGetClassObject function. This function is used to acquire an instance of the COM class factory that is used to create a COM object. What you need to do is:
Load the library (DLL) that houses the desired COM object
Locate the DllGetClassObject function
Call DllGetClassObject, passing it the CLSID of the desired COM object, this will return an IClassFactory instance.
Call the CreateInstance method on the class factory to get an instance of the COM object.
Cast the returned object to the interface you wish to use.
Note that there be dragons with this approach -- it is fairly low level. If you get anything wrong you will experience access violation exceptions or worse. (For instance, your interface declaration has to exactly match the COM interface).
I have included some sample code at gist which you might like to use if you want to go this way.
Using this code would look something like this:
// Load the library. You dispose this after you are finished with
// all of your COM objects.
var library = new LibraryModule();
library.Load("mylibrary.dll"); or whatever your dll is called
var clsid = new Guid("75E81043-CAD5-11D3-800D-00105A5E2FA0");
var myObject = (MyInterface)ComHelper.CreateInstance(library, clsid);
Just note that if you dispose of the LibraryModule object, then this will unload the DLL. Depending on your needs, you might just assign the value to a static field so that it exists for the lifetime of the program.
You need to set up your unmanaged DLL for registration-free COM. There's a pretty complete walkthrough here, with plenty of examples. Among other things, it involves using manifests to point to the file, side-by-side assemblies, and some interop.
An important note in the walkthrough is that registration is required for the initial setup of the client, but not later.
If your DLL has to be a COM object, then it must be registered. This registration does not need to be performed by regsvr32. All regsvr32 does is load the DLL, get the address of DllRegisterServer and call it it. DllRegisterServer adds the necessary entries to the registry for an application to use the object.
If your DLL does not need to be a COM object, you can modify it to simply export the function(s) you need and p-invoke it(them).

C# COM Class - DISP_E_UNKNOWNNAME

I have declared a COM visible class in C#. The code is as follows:
[ComVisible(true)]
public class AComVisibleClass : TheParentClass
{
public bool SomeFunc(string id)
{
return true;
}
}
This class is instantiated by a factory class, also COM accessible.
But if I try to access in a VB script file, a DISP_E_UNKNOWNNAME exception is thrown.
This is a new class on a pre-existent library we have here at work. All other classes are accessible through COM. The whole library is compiled into a single assembly file. I have registered the new assembly using regasm, but I still get this exception.
I've tried to debug the COM call using VS2008. The factory class seems to be able to instantiate the AComVisibleClass. The aforementioned exception is thrown only when the factory tries to execute SomeFunc.
I know this may sound a little(?) bit vague, but I cannot expose the real code here. If someone need more information, please ask me.
I can think of three possible reasons for this problem:
Reason 1: Wrong name used in CreateObject:
I suppose that your VBScript code calls
something like
this:
Set obj = CreateObject("MyLibrary.AComVisibleClass")
If this is correct, then please open the registry editor and check whether the HKEY_CLASSES_ROOT key contains a subkey called MyLibrary.AComVisibleClass.
If it does not, then your library name possibly is different than you expected. Search the registry for AComVisibleClass to find the correct library name.
Reason 2: 64-bit issues:
If the problem happens on a 64-bit operating system, the reason could be that your VBScript runs as a 32-bit process and the C# COM DLL is 64-bit or vice versa.
Reason 3: Wrong function name:
You might be using the wrong function name in the script, e.g. obj.SomeFnc(1) instead of obj.SomeFunc(1), or the function name you have chosen is a reserved keyword in VBScript or it contains unusual characters.
Sounds like you need to support IDispatch.
Check out Does C# .NET support IDispatch late binding?
edit
This answer is likely wrong, and I may yet wind up deleting it. For now, it seems to add value, so I'll let it stay.

Side-By-Side COM Interop with C# and VBA

I'm not talking about calling a VBA COM from C#... the other way around!
What I would like to do is call a C# library using VBA in MS Access without registering the DLL. I've been playing around with side-by-side interop for a while without success and it has finally occurred to me that a mdb.manifest is probably not an acceptable replacement for an exe.manifest (probably obvious, I know, but I was trying to be optimistic).
My question: Is it possible to get VBA to load a side-by-side COM component?
Or, is there another way to use an unregistered C# library in Access?
(Before you ask, my reasons are: there is absolutely no way I will be granted access to my client's Windows registry -- that's why it was written in Access in the first place. And, I will need to implement the same functionality in a C# application soon and rather not do it twice).
To add to the already existing answers: with .NET 4.0, it's actually quite simple to consume a C# dll in your VBA project without registering the COM.
EDIT: I just tried this with the mscorlib.tlb and mscoree.tlb that are in C:\windows\Microsoft.NET\Framework\v2.0.50727-- loading an assembly compiled in 3.5-- and it worked just fine. So apparently you don't need .NET 4.0.
The below is an example of how to use a C# dll in your VBA project. It is slightly modified from this answer.
1) Add references to the following type libs your VBA project (Tools->References):
C:\windows\Microsoft.NET\Framework\v4.0.30319\mscorlib.tlb
C:\windows\Microsoft.NET\Framework\v4.0.30319\mscoree.tlb
(use Framework64 folder if you are running 64-bit Office)
2) In your C# project, make sure you add the [ComVisible(true)] attribute to your class:
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace VB6FuncLib
{
[ComVisible(true)]
public class VB6FuncLib
{
public VB6FuncLib()
{ }
public void test()
{
MessageBox.Show("Test Successful");
}
}
}
You don't need to check the option "Register for COM Interop". That's only for building a standard COM object. You don't have to check "Make Assembly COM Visible" either, unless you want the whole assembly to be visible (that would also eliminate the need for the COMVisible attribute).
3) In your VBA code, add a new module with this code:
Sub Test()
Dim Host As mscoree.CorRuntimeHost
Set Host = New CorRuntimeHost
Host.Start
Dim Unk As IUnknown
Host.GetDefaultDomain Unk
Dim AppDomain As AppDomain
Set AppDomain = Unk
Dim ObjHandle As ObjectHandle
Set FS = CreateObject("Scripting.FileSystemObject")
Path = FS.GetParentFolderName(CurrentDb().Name)
Set ObjHandle = AppDomain.CreateInstanceFrom(Path & "\VB6 Function Library.dll", "VB6FuncLib.VB6FuncLib")
Dim ObjInstance As Object
Set ObjInstance = ObjHandle.Unwrap
ObjInstance.test
Host.Stop
End Sub
4) Copy the DLL into the same folder as your Office project and run the Test() sub in VBA.
Notes:
It should be noted that one of the limitations of this technique is that it won't work if the .DLL is stored on a remote network share. One simple solution would be to copy it into the same local folder on each PC where it is being used. Another solution would be to include the binaries in your Access app/VBA project, and have MS-Access export them. One way that could be accomplished would be by storing them in Base64 in a table or spreadsheet, then converting them and exporting them as binary.
I was able to get early binding (and therefore Microsoft IntelliSense) to work by creating a type library to go with the DLL (by using tlbexp), and adding a reference to the TLB in my VBA project, but it does complicate matters a bit because it requires your VBA app to know where both the DLL and the TLB files are (and also requires someone to make sure they are there).
You don't have to own the exe to use SxS, SxS is another word for Activation Context.
If you can import the relevant win32 calls into vba (and you can), then you can use the activation context api to load your manifest file.
More on the subject and some examples can be found here.
The problem is that to use SxS, you need to own the exe to set up the config to load the SxS assembly. You don't "own" Access, and while you could drop the right config in to cause it to load your .NET COM stuff sans registration, it wouldn't be a "good citizen" move.
If you get tricky with shimming, you can set up an unmanaged DLL (or a hacked C# class library with a dllexport, see this, for example) with an export that will load the .NET framework, create an instance of a COMVisible DispInterface managed type and return it (the method should return IDispatch). Then write a VBA declare to your DLL export function (declared as returning Object). If this doesn't make sense, you probably oughtn't try it... :) I've done this before in a similar situation, and it does work, but I don't have a sample to point you at.
C# libraries are not regular DLLs. They're more similar to COM libraries which need to be registered (just like ActiveX controls) before being used; especially when called from non-.NET code.
(Unless, of course, things have changed...)

Categories