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...)
Related
I'm experimenting with COM objects and created a simple COM service that acts as a calculator with add, subtract, multiply, divide (details not important).
I then wrote some code to register it dynamically with a C# application
Assembly asm = Assembly.LoadFile("C:\\...COMCalc.dll");
RegistrationServices regAsm = new RegistrationServices();
bool bResult = regAsm.RegisterAssembly(asm, AssemblyRegistrationFlags.SetCodeBase);
After registering it I've been able to use the service from an IE browser in javascript.
var num1 = 2
var num2 = 2
var objTest = new ActiveXObject("COMCalc.COMCalc")
alert(num1 + " - " + num2 + " = " + objTest.Subtract(num1,num2))
I'd like to now be able to test it from my C# Application so I can have a register, unregister, and test method for my COM object. I've struggled to find the documentation for how to do this. Any Ideas?
Bonus: I also would like to access it with the GUID defined in the COM object as opposed to the COMCalc.
regAsm.RegisterAssembly(asm, AssemblyRegistrationFlags.SetCodeBase)
By writing your own custom registration method, you are missing out on the normal way that COM client programs or unit testers will exercise your code. They'll use the type library of your COM component, a machine-readable file that describes the types that you expose from your component. It is the COM equivalent of .NET metadata.
You get a type library by using the normal way to register, either by using your project's "Register for COM Interop" setting or by running Regasm.exe with the /tlb option. Or by running Tlbexp.exe to generate it manually.
This however does not let you test your component with a C# unit test, you'd normally use Project > Add Reference > Browse and pick the .tlb file. But the IDE refuses to accept it, it can see that the type library was created from a .NET assembly. It insists that you use a normal assembly reference instead, picking the DLL instead.
There's a very good reason for that. You can fool the IDE by using late binding but that does not fool the CLR. In other words, you are not actually testing the COM interop at all. You might as well use the normal way to add a .NET assembly reference. Truly testing the component requires using a COM client written in a non-.NET language. Not that many practical ones around anymore, you could use a scripting language like Javascript or VBScript. The closer it is to the actual language and runtime environment that is going to use your component, the better. If you are going to use it in a browser then something like Selenium or HtmlAgilityPack would be wise choice.
Nobody ever likes to hear advice like that. You fool the IDE by late binding, very similar to what you did in the browser:
Type type = Type.GetTypeFromProgID("COMCalc.COMCalc");
dynamic obj = Activator.CreateInstance(type);
dynamic result = obj.Subtract(2, 1);
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.
Although this is a long question the coding and testing part should be really easy to reproduce.
I have created two separate Class Libraries in C# and I think I am running into a name collision problem caused by existing registry keys from my previous projects and trials.
Here are my two classes:
using System;
using System.Runtime.InteropServices;
namespace Test
{
[InterfaceType(ComInterfaceType.InterfaceIsDual),
Guid("ED5D264B-1D80-4A5D-9C14-8297D90B7037")]
public interface ITest
{
// body
}
[ClassInterface(ClassInterfaceType.None)]
[Guid("8B261B92-8EC5-4CDC-A551-67DEB42137FF")]
[ProgId("Test.TestClass")]
public class TestClass : ITest
{
// body
}
}
and
using System;
using System.Runtime.InteropServices;
using ADODB;
namespace Test
{
[InterfaceType(ComInterfaceType.InterfaceIsDual),
Guid("ED5D264B-1D80-4A5D-9C14-8297D90B7037")]
public interface IConnection
{
// body
}
[ClassInterface(ClassInterfaceType.None)]
[Guid("8B261B92-8EC5-4CDC-A551-67DEB42137FF")]
[ProgId("Test.Connection")]
public class Connection : IConnection
{
// body
}
}
I have Exposed .Net Components to COM like this:
In order to access the assemblies from Excel I have added the ADODB references to the assembly, ticked make assembly COM visible and register for com interop. Also, I've added references to each *.tlb file(2 files for two projects) so I can access them using an early binding and use VBA Intellisense.
I have followed the same procedure on another machine and I can use early binding using the Connection as class.
I am thinking there are some old registry keys I haven't deleted on my original machine which will not allow me to use Connection as the class name in VBE. I've manually scanned my registry and deleted everything I could think of related to my project.
I have also deleted the project entirely and used a 3rd party software to scan registry for missing dlls however that didn't help:/
Removed all previously registered GUIDs and applied new ones each time I created a new Project (just in case)
Created new projects using different namespaces and class names (using ADODB;) I haven't been able to use early binding yet like this Test.Connection therefore I am assuming I have a name collision problem. I am suspecting the name class Connection to be causing it although I am not 100% sure.
The Test.TestClass namespace in VBA:
I can declare and use instances of the TestClass type in two ways using early binding:
Dim x as Test.TestClass
Dim x as TestClass
Now going into VBE Object Explorer F2 the TestClass is properly displayed in comparison to other libraries and general idea of using COMs.
However, when I want to use the Test.Connection library I am unable to use early binding following the same pattern as TestClass because the generated *.tlb file automatically changes(renames) the ProgId's. So, instead I have to bind it like this
Dim x As Test.Test_Connection
Dim x As Test_Connection
and the Object Explorer displays the names using _ (underscores) and not . (dots), which is easy to explain why this happens - keep reading :)
As it stands I am sure it is not the VBE environment that changes the names to avoid collisions. It is the VS' *.tlb generator.
I went to the assembly folder and opened both *.tlb files in Notepad++. I can clearly see that the *.tlb for the Test.Connection library already includes the names with the _s unlike the Test.TestClass which has .s
I have tried to manually edit the *.tlb file but as its a mixed binary file it takes some effect but also causes Excel to stop responding in some weird ways so I have to avoid this method.
I think I have explained well what the problem is and where it comes from. Now my question is: Are there any attributes to use in C# code to tell the *.tlb generator not to override my ProdIds? Are there any alternative ways of manipulating *.tlb files? Is this issue a name collision and is it avoidable without changing the name of Connection class?
I'm sorry for such long question but I have been digging and digging for almost a week now and I still cant solve this.
Note: In VBA ( or VBE Object Explorer ) using IntelliSense ctrl+space it does not seem that either Connection or Recordset have been used. Since they are not already reserved in the VBE environment I recon it has to do with my library itself.
As a reference to why this issue has been raised here, please see VBA equivalent to C# using or VB.NET imports creating aliases
Thank you very much for your time!
Do avoid focusing on the ProgId. You are not actually using it, the dialogs that you made a screenshot of show the actual class names, not the ProgId.
Getting the class name renamed to "Test_Connection" is normal behavior for the type library exporter. It will do so whenever it detects a conflict with another interface or class name that has the same name. You are certainly increasing the likelihood of this happening by also having a dependency on ADODB, it also has a Connection class. A very trivial solution is to simply rename your own type.
Your code snippet cannot reproduce this problem. But of course it is incomplete, we can't see what you are really doing in the code. You'll bring in the dependency on ADODB if any of your public methods use a type from this type library. Also note that there are non-zero odds that this will happen by accident. You might have written a method that intended to use your own Connection type but the compiler resolved it to the ADODB type.
An essential tool to debug this is Oleview.exe, run it from the Visual Studio Command Prompt. First create the type library for your C# assembly with Tlbexp.exe. Then use File + View Typelib, you'll see the content of your type library expressed in the IDL syntax. You'll have little trouble recognizing the mapping of your C# types to the IDL declarations.
Pay attention to the importlib directives at the top of the file. They should look like this:
// TLib : // TLib : mscorlib.dll : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
There should only be those two. The first one imports the .NET types, defining _Object. The second one imports standard COM types, like IDispatch. If you see additional ones here then you increase the odds of a name collision.
This IDL also gives you a way to solve the problem, in case it is unsolvable, you can edit it to name the types the way you want them. Save it to a .idl file. And compile it with midl.exe /tlb to generate a type library with your preferred names. Do note that this is not something you want to have to do often.
I have a C# application which interfaces with some hardware (USB device) as follows:
C# application -> intermediate DLL -> hardware DLL -> hardware. The intermediate DLL and hardware DLL are supplied with the USB device so I have no control over these.
The intermediate DLL is the only which which I need to include in the VS project as this is what I call. The hardware DLL is then in the same directory so must be found automatically.
A new version of hardware device is now released with a different hardware DLL. The old DLL is not compatible with the new hardware and the new DLL is not compatible with the old hardware.
How can I make my application work with both pieces of hardware? I guess that I need to load and unload each DLL as required?
Here's what I do for a similar problem. I have a chunk of code that I want to work with, but I have to load the dll at runtime. So I refer to it in my project, but I don't put it in the same directory as the rest of my assemblies. Instead, in the consuming code, I have some code that looks like this:
// constructor called from a static constructor elsewhere
MyDllLoader(string hardwareFolder) {
_hardwareFolder = hardwareFolder;
AppDomain.CurrentDomain.AssemblyResolve += new ResolveEventHandler(CurrentDomain_AssemblyResolve);
SeeIfAlreadyLoaded();
}
private void SeeIfAlreadyLoaded() {
// if the assembly is still in the current app domain then the AssemblyResolve event will
// never fire.
// Since we need to know where the assembly is, we have to look for it
// here.
Assembly[] assems = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly am in assems)
{
// if it matches, just mark the local _loaded as true and get as much
// other information as you need
}
}
System.Reflection.Assembly CurrentDomain_AssemblyResolve(object sender, ResolveEventArgs args) {
string name = args.Name;
if (name.StartsWith("Intermediate.dll,"))
{
string candidatePath = Path.Combine(_hardwareFolder, "Intermediate.dll");
try {
Assembly assem = Assembly.LoadFrom(candidatePath);
if (assem != null) {
_location = candidateFolder;
_fullPath = candidatePath;
_loaded = true;
return assem;
}
}
catch (Exception err) {
sb.Append(err.Message);
}
}
return null;
}
There's another solution too - it's complicated, but I've done it and done the work for you. You declare an abstract class, say MyHardwareAbstraction, that has the signatures of the methods that you want and you code against that interface. Then you write some code which given a path to an assembly, loads it and dynamically defines a new class that matches MyHardwareAbstraction and makes it map onto an instance of the actual object that you want. I wrote a blog several years ago on how to do this.
The nice thing about doing this is that you use the abstract type in your code and work against that and then the adapter compiler will, at run time, compile a new class that will complete that abstract type using some other type as the target type. It's fairly efficient too.
I you want both dll to coexist in the program, you'll have to use AppDomains, as explained here.
Else, you can simply use LoadLibrary after the user has made a clear choice about what version he needs ?
Edit:
If the intermediate DLL is a .Net Assembly, you can use the method mentioned here to specify where to look for your intermediate DLL before you call any method that uses the intermediate DLL, without having to change your existing code.
then you must not directly reference the DLL in your C# project, because .Net Assemblies are discovered and loaded before your Main method is even called. Instead, you must dynamically load the intermediate DLL using AppDomain or other methods, then use the library via reflection, or by using dynamic objects.
Apparently, this would make programming very cumbersome. However, there is an alternative method. You can write a launcher program, that loads your original application (you can load .exe files as libraries), and invokes the Main method of your original program reflectively. To make sure that the correct intermediate DLL is loaded, you can use the method mentioned here, while your launcher program is loading your original application.
The following discussion still applies to the hardware DLL.
The following is valid if:
You need only one version of the dll at a time (during the entire period your application runs), and
The two versions of the intermediate DLLs have exactly the same API.
According to MSDN, the DLL Search Path includes directories specified under the PATH environment variable. (http://msdn.microsoft.com/en-us/library/7d83bc18%28v=vs.80%29.aspx). Hence, you may put the two versions of the intermediate DLLs under seperate sub-directories under your application directory, but with exactly the same name under each directory, for example:
bin\
hardware-intermediate-v1\
intermediate.dll
hardware-intermediate-v2\
intermediate.dll
Then, at start up, after your application has determined which version to use, you may add one of the above directories to your PATH environment variable,
using System;
using System.Reflection;
using System.IO;
...
Environment.SetEnvironmentVariable(
"PATH",
Environment.GetEnvironmentVariable("PATH") + ";" +
Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) +
"\\hardware-intermediate-v1"
);
Then, calls to P-Invoke methods (DLLImport) will result in the corresponding version of the DLL to be loaded. To immediately load all DLLs, you may refer to DllImport, how to check if the DLL is loaded?.
However, if you wish to use the two version of the DLLs together without restarting your application, or if there are any API difference between the two DLLs on the level of method name and/or parameter count/type, you must create two seperate sets of P-Invoke methods, each binding to its corresponding version of the intermediate DLL.
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?