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.
Related
I create an application with C# that can read item data from SQL Server and push it to the scale system named "SLP-V Ishida Retail Scales". They have an interface "SLP-V Automation Interface" that allows user programs to interact with their systems. This is the note from help page in SLP-V :
The automation interface (also known as the "COM (common object model) interface") provides a method for user programs to access SLP-V functions. The most common application for this is the use of VB Script to automate SLP-V operations such as importing host files. However, the automation interface can be used from any programming environment that supports automation (COM), and this is the preferred method for incorporating SLP-V functions into end-user applications.
This topic provides a reference for the methods and properties of the SLP-V automation object and includes some sample programs.
SLP-V Automation Object
The SLP-V automation object name is "Ishida.Slp.Scripting.CommonApi" and the type library file is "SlpScripting.tlb".
My question is, does the C# language allow us to interact other programs using OLE Automation? And if the answer is yes, how do I interact with my program?
I mean like calling their method. Because I can't add SlpScripting.tlb as a reference. It says
A reference to 'SLP Scripting Interface ' could not be added
The ActiveX type library 'SlpScripting.tlb' was exported from a .NET assembly and cannot be added as reference. Add a reference to the .NET assembly instead
And I have searched Google about this, but I didn't find the answer.
Finally found a solution
I don't have to add reference in c#, instead of just using :
System.Type objType = System.Type.GetTypeFromProgID("The name of progID that you want to call");
dynamic comObject = System.Activator.CreateInstance(objType);
Example ProgID = "Ishida.Slp.Scripting.CommonApi".
And then just call the function / method that exist in that object, for example :
comObject.LoginToHost("localhost", 8085, username, pass);
OLE automation is old wording for what we now call COM. And yes, .NET can access COM very easily (starting with .NET 1.0).
You have these options:
Method 1
First "register" the COM library on your development system. Look in the documentation of the SLP system, probably this was done already during setup. If not, normally a COM DLL can be registered manually with regsvr32 XXX.DLL. Be aware of 32/64 Bit issues (if you want to register a 32 bit COM DLL in 64 bit Windows, use C:\Windows\SysWOW64\regsvr32.exe).
Then your COM DLL should be listed in Visual Studio if you go to
Add Reference ==> COM
as "SlpScripting Type Library 1.0" or similar.
Then, add a "using SLPxxxx" or similar (Intellisense should show the real name).
After this, you should be able to create a new instance of your COM object.
For additional help, search for "C# COM interop", you will find lots of informations.
Method 2
Open a Visual Studio command prompt, and enter:
tlbimp SlpScripting.tlb
A DLL will be created, which you can add as a reference.
Let me elaborate, before someone is thundering down on me with "Never reuse a COM interface!!!"
We implemented an COM interface on a program which is discontinued. We now actually buy a similar piece of software from a third party (so I can't change that!)
The COM interface is still used by many (third party) programs. Now I have to integrate the new piece of software with these programs (which I can't change because they are third party).
So I think I need a proxy: This proxy will reuse the COM interface so none of the third party programs will be able to tell they're not talking to the discontinued software. Inside the COM object, I'll 'translate' the commands and forward them to the new piece of software.
I'm using C# to build my proxy and I've used the tlbimp.exe to generate a DLL from the type library of the old program.
I'm referencing this generated DLL in my COM project where I provide a implementation for the given interface.
Next, I register the output DLL from my project and use a old client to call the proxy.
It returns the error: Unable to cast object of type 'Lib.ApiClass' to type 'Lib.ApiClass'.
Why is that?
Should I use a different approach?
I need to automate office documents (Word & Excel) from my .Net4 app.
Since I can't really force my users to use a specific Office version I don't use interop assemblies or tlbimp, so my project doesn't contain any additional references nor will the whole app fail if Office is not installed (feature just won't be available).
Instead I ask the system which COM server can handle "Word.Application" or "Excel.Application":
dynamic app = Activator.CreateInstance(Type.GetTypeFromProgID("Word.Application"));
app.Foo();
I'm concerned how to properly dispose of the "app" object when I'm done with it, since now I have two internal management systems to worry about (COM reference counting and .Net reference tracking). Ideally I should be able to wrap the dynamic app object into a disposable wrapper class and to be sure that underlying COM object is unreferenced when the wrapper is disposed of.
EDIT: Also I'd like to know how to properly make the COM object "live on" when I'm done with it, since Word is in a separate process. I should be able to instantiate Word app, automate it and then free all my references but the Word app should stay open.
I would definitely implement IDisposable and call Marshal.ReleaseComObject within it. Make sure you correctly implement the disposable pattern to avoid releasing the COM object more than once and reducing the reference count further than you should, although this may not matter if you are only going to create one instance of the COM object. I have implemented this pattern successfully in the past so you are definitely heading in the right direction.
I think Word will remain open even when the last COM object is reference counted to 0 in your app, unless you explicitly ask it to exit. ( Word.Application.Close() ?)
I have existing managed and unmanaged software using an ActiveX component supplied by a third party to perform some communications, but it is now required that this communication be routed through my application.
Ideally I'd be able to install a .NET component which will expose exactly the same interface, and will be usable as a drop-in replacement.
However, I am running into the limits of my understanding of COM, which admittedly is quite minimal.
How best to ensure that my implementation of the interface is 100% binary compatible with the existing object?
How do I ensure that applications use my implementation of the interface instead of the legacy implementation? Is it simply a matter of registering my implementation, and unregistering the legacy one?
How do I ensure it's a "drop-in" replacement, and requires no changes to existing software?
How do I ensure unmanaged code can use it without issue?
Note: I am able to require that .NET 4.0 be used, if that will make things simpler.
Edit: Bounty will be moved here How to debug why a VB6 application using my .NET ActiveX control does not register for events? after 2 days.
Use the type library of the ActiveX component. Import it with Tlbimp.exe to get the interop library, you probably already have it if you use this component yourself. Implement your own code by inheriting the interfaces in that type library.
Your implementation must use the exact same GUIDs and ProgIDs as the ActiveX component. Use OleView.exe, File + View Typelib and select the ActiveX DLL to see the GUIDs. The ProgIDs are more difficult, best thing to do is to watch how the registry is modified with the SysInternals' ProcMon utility when you register the ActiveX DLL with Regsvr32.exe. Ultimately, the exact same changes need to be made by Regasm.exe when you register your replacement.
As point 2.
Same, the registration gets unmanaged code to use yours instead.
To make this work out well, you really have to know what the interfaces do. You cannot make this work if the ActiveX component is actually an out-of-process server (an EXE).
Well, I've gotten a lot further along with this, but I seem to have encountered an intractable problem.
The object I am replacing uses COM events. When one of the client applications (VB6 I believe, as depends.exe tells me it uses msvbvm60.dll) instantiates and uses my replacement, it does not register for any of the events, and unfortunately, the way it works is that after a particular method call has completed, the client application does nothing until an event fires.
Note: My replacement ActiveX control inherits from System.Windows.Forms.Control, and sets MiscOptions of 131457 on the coclass registry entries as suggested by http://ondotnet.com/pub/a/dotnet/2003/01/20/winformshosting.html, the reason being that the thing I am replacing was an honest to goodness ActiveX control, and I could not get these existing clients to instantiate my object successfully without any code changes at all until I inherited from WinForms control.
I have tried the approach where my coclass declares public events with the same name as the interface specified by ComSourceInterfaces, this works 100% from a C# app that uses AxHost, events are triggered.
I have also tried instead to implement IConnectionPointContainer and all supporting interfaces on my replacement control, and this works 100% from a C# app, but in the VB app, it never actually attempts to Advise() the connection point of the client sink interface to call, it only calls Unadvise() with an invalid cookie value of 0.
One issue with the typelib that I have noticed is that I cannot get tlbexp.exe to export one of the properties on the coclass interface as OLE_HANDLE, it just ends up being a long in the TLB generated from the assembly (this TLB is referenced by the TypeLib entry in the registry). Could this cause issues with eventing?
Any ideas how to debug this?
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