I've got a particularly tricky situation here that I would like to throw out there for a bit more feedback on (I'm the only .NET dev in the company I work for so no-one to bounce off).
I've been tasked with replacing an aging VB6 authored ActiveX component that is consumed by an application that contains both VB6 useage and VB.NET usage of the component I'm replacing.
I have the complete source code for all these components so I can see the use cases.
For the sake of this discussion the components could be called:
MyVb6.dll
MyApp (with VB.NET & VB6 Components)
During the build process for MyApp they make use of TlbImp tool to then produce an interop library:
Interop.MyVb6.dll
Usage
In most instances the usage of this is as expected, making use of the CreateObject() method such as:
Private Property MyProp() As Object
Get
Try
If m_myProp Is Nothing Then
m_myProp = CreateObject("MyVb6.MyVb6Obj")
m_myProp.Initialize()
End If
Catch : End Try
Return m_myProp
End Get
However in one instance I've found they appear to have changed tactic on how to consume this interop dll and they have a static reference to it and a typed property, such as:
Private Property MyProp() As MyVb6.MyVb6ObjClass 'Whilst this is strongly typed, it is from the interop dll ...
Get
If m_myProp Is Nothing Then
m_myProp = CreateObject("MyVb6.MyVb6Obj")
m_myProp .Initialize()
End If
Return m_myProp
End Get
The expense of rebuilding and redeploying the entire application is completely out of the question so I'm left with no option other than to replace just the MyVb6.dll.
What I'm hoping to find out here is if this is a practical solution ...
Replacement
What I have done so far is written the skeleton of the replacement dll and since the object instances are created using a known string value I have added this is the ProgId, such as:
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("xxx")]
[ProgId("MyVb6.MyVb6Obj")]
public class MyNewCSharpVersion : IMyNewCSharpVersion
{
...
}
Obviously for each of the objects I have maintained an identical interface so the calling application will find all the expected methods to call etc.
Then when I deploy this I'll deregister the old COM component and register my new one. Will this attempt at tricking it into thinking it's creating the same object actually work?
The Very Nasty Problem
There are also examples in the source code where they have used the VB6 dll directly, such as:
Dim myObj As MyVb6.MyVb6Obj
Unfortunately, no-one can answer the question if this particular component is still in use which in itself is slightly worrying.
If anyone has done something similar and got it working it would be great to know.
Not a problem, just use the right names:
namespace MyVb6 {
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("xxx")]
public class MyVb6Obj : _IOldVb6Version
{
...
}
}
Note the different interface name. Don't write your own. Add a reference to the old VB6 component so you can be completely sure that you implement the IOldVb6Version interface accurately with an exact match on the IID, the methods, their order and their arguments. If you don't know the old interface name then have a look with Oleview.exe, File + View Typelib.
Related
I'm trying to wrap a COM API so that others can use my wrapper without knowing anything about the wrapped API. However, I've hit a problem trying to use some of the items from the COM API.
Let's say the COM API has a method that returns an object defined within the API.
IComChildObject IComObject.GetChildObject()
If I have a reference to the dll that defines the COM API, I can easily use this as follows...
IComChildObject childObject = myComObjectInstance.GetChildObject();
The problem I have, is that I need to be able to work with the IComChildObject through the wrapper class, without referencing the COM API dll.
I tried to create an interface in my wrapper, that would accomplish this. So inside my wrapper project, I have an interface like this.
public interface ILocalChildObject : IComChildObject{}
Then I added a property to my wrapper, that I thought would allow my external code to use the IComChildObject.
public class ComWrapper
{
IComObject comObject;
public ILocalChildObject ComChildObject { get { return comObject.GetChildObject() as ILocalChildObject;}}
}
When I run the following code from my external code, the ChildObject is null
ILocalChildObject ChildObject = myComWrapper.ComChildObject;
I'm obviously doing something wrong, but I'm in over my head on this and don't even know what to search Google for.
Maybe it's not clear what I'm trying to do, or maybe I'm trying to do something strange here. I want to create a wrapper class library in such a way, that the code that uses it doesn't have to know anything about the wrapped library. I've done okay so far, up until the point where I need to use objects from the wrapped library in the external code. I could easily resolve this by referencing the wrapped library in my external project, but I'd like to avoid doing that.
Basically, I just need a way to use the IComChildObject in my external code, without adding a reference to the COM API dll.
Any help would be greatly appreciated,
If your API is based on IDispatch you could use the dynamic keyword, something like this:
dynamic childObject = GetChildObjectSomehow();
childObject.CallAnyMethod() // compile will always succeed, will be resolved at runtime (and failed if there's like a typo error)
Note dynamic is not available on .NET core for COM objects yet.
If it's not (if it's based on IUnknown), then you'll have to declare this interface either in an external dll or .tlb, or directly in your C# code, so it can be called by the .NET runtime. You don't have to use the original .dll, you can redefine the interface by yourself if needed (maybe a simplfied version). But the runtime has to know the binary layout to be able to call it.
Another way to handle this, as Hans Passant points out, is to wrap the COM API object in a class. Then you can access the properties within the object through the new object. The only downside to this approach is there's a lot of typing, as you have to recreate any properties or methods that you want to access in the COM API object.
In the wrapper project, you'll create a class that will contain the object returned from the API. This class will also have properties and methods, that allow a user to manipulate the API object through the class.
public class LocalChildObject
{
internal IComChildObject ComChildObject;
public string ChildObjectProperty { get { reutrn ComChildObject.ChildObjectProperty; } set { ComChildObject.ChildObjectProperty = value ;}}
public LocalChildObject(IComChildObject ComChildObject)
{
this.ComChildObject = ComChildObject;
}
}
In this example, ComChildObject is the object returned from the API. Then there's a property in the class ChildObjectProperty, which allows you to get or set the ChildObjectProperty of the ComChildObject.
Then in my main wrapper class, I can have a property that returns this new object (which contains the API COM object).
public class Wrapper
{
public LocalChildObject GetLocalChildObject { get { return new LocalChildObject(ComObject.GetChildComObject());}}
}
Then in the external code I can make changes to the object through the new wrapper object
LocalChildObject localObject = myWrapperInstance.GetLocalChildObject;
localObject.ChildObjectProperty = "A new string";
This method requires recreating all the properties and methods that you want to expose through the wrapper, however, it does allow the user using the wrapper to use IntelliSense.
I want to use c# code app in tcl using COM > twapi > tcl path.
By studying "call c# code from tcl" wiki page I understood two things.
We need to compile c# code with com interface register VS option. Then use that namespacename.classname to create object instance. But it is not clear how twapi (or tcom) will use that com (or link). Can you please explain in more details. Thanking you in advance.
C# code
using System;
namespace MyClassLib
{
public class Class1
{
public Class1() {}
public int Double (int val) { return val * 2 ; }
}
}
When the project is registered for COM Interop and built, the class is registered in the system's Registry as a provider for that class name. The tcom package knows how to use that information. When you do:
set myCom [tcom::ref createobject "ClassLibrary1.Class1"]
It goes away and asks the COM service that's built into Windows to make an instance of that object and give a reference to it back to you. That reference is what is stored in the variable. You can then invoke methods of the object; the syntax for that is Tcl-ish rather than C#-ish, but the models line up.
$myCom Double 6
Yes, there's a lot of complexity going on behind the scenes, mostly centred on IDispatch and its related interfaces.
I am trying to see if I can build a COM component in C# (.NET 4) which I can use from a VB5 program (cue derisive remarks) to access a web service. Following all the instructions I have been able to find on MSDN and CodeProject as follows:
Building COM Objects in C#
Example COM Class (C# Programming Guide)
I have written the following:
[Guid("7A715F02-D349-45DC-B0AE-9925FD3B943C")]
public interface ARCOM_Interface
{
[DispId(1)]
string GetServiceResponse();
}
[Guid("5130F041-619E-41F9-84B6-8332642228F6")
, InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface ARCOM_Events
{
}
[Guid("0A77754F-34CF-4E0E-AAC2-85FD686758E0")
, ClassInterface(ClassInterfaceType.None)
, ComSourceInterfaces(typeof(ARCOM_Events))]
[ComVisible(true)]
public class ARCOM_Class : ARCOM_Interface
{
public string GetServiceResponse()
{
string response = string.Empty;
ARWebService.ARWebService svc = new ARWebService.ARWebService();
response = svc.PingMeBack();
return response;
}
}
The Assembly in question is signed with a strong name and the output is registered for COM Interop. After building, I have applied RegAsm to it and generated a type library with tlbexp.exe.
In VB6, when I open the list of references from the Project properties, I can find the assembly in the list, and I can check it. I can even do the following in the VB6 code:
Private Sub HitWebService()
Dim arcom As ARCOMObject.ARCOM_Class
arcom. <== Intellisense doesn't sense anything!
End Sub
The Intellisense sees the ARCOMObject and the class, but nothing that is within the ARCOM_Class itself (except for the usual "GetType", "Equals" and other generic Object methods/properties). Most specifically, it does not seem to see the GetServiceResponse() method, so I can't call it.
What am I leaving out?
I see you've worked out a way to get what you want while I was typing up this reply, but I'll continue anyway because it might tell you some stuff you don't know...
You've applied the ClassInterface(ClassInterfaceType.None) attribute to the class. This tells COM interop not to define an explicit interface for the class, and so your client must use IDispatch (late binding). This means that your client must have a-priori knowledge of the interfaces that the class implements. That is, you (the programmer) know what methods are available but tools like IntelliSense have no way of finding that information.
Just go ahead and call the method:
Dim response As String
response = arcom.GetServiceResponse
OK, I found something that unexpectedly made a difference.
Notice that the code above contains the decoration [ComVisible(true)]. Making the assembly visible to COM is of course essential. I thought that this covered it, but upon further searching I found that there is another place to mark it. If you look in the project properties, in the Application tab you will find the Assembly Information... button. Click on it and at the bottom of the dialog box you'll see a checkbox "Make assembly COM-visible". I checked it, recompiled (and re-ran tlbexp and regasm), and then found that the method GetServiceResponse() was visible.
I don't know why the make ComVisible checkbox works when setting the attribute doesn't.
Edited to add: I think I see what is up with that. The ComVisible(true) makes the class visible to COM, but not the assembly; this is why "Make assembly COM-visible" needs to be checked, because the GetServiceResponse method seems to get its COM-visibility through the ARCOM_Interface, which is not marked as ComVisible unless the assembly is as well.
An existing Visual C++ application makes the following call;
BOOL bRet = pMyClass.CreateDispatch("BlahBlah.MyClass");
if ( !bRet )
{
// Error handling snipped
}
else
{
pMyClass.MyMethod();
pMyClass.ReleaseDispatch();
}
pMyClass is a class which was apparently auto-generated by ClassWizard, and it inherits from COleDispatchDriver.
The actual DLL to which it refers is a VB6 one, and this is being migrated to C# as part of an effort to move away from VB in general.
My question is... is there anything special I need to do to make sure that the C# assembly will work in the same way as the original VB6 module did? Currently, the C# looks like this;
[ComVisible(true)]
[ProgId("BlahBlah.MyClass")]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class MyClass
{
...
public void MyMethod()
{
...
}
}
Is this sufficient? Are there any gotchas to be aware of when setting public string fields (not shown in code) on MyClass?
Note that I'm not the original author of this code - it's from a legacy system and I'm just doing the migration.
The CreateDispatch call uses late binding to talk to the COM server. ClassInterfaceType.AutoDispatch. Using AutoDual is fine, that also includes late binding support. With the significant advantage that you can make it a lot faster some day. Late binding isn't cheap.
Last Updated: 2009-08-11 2:30pm EDT
A few days ago I posted this question about some very strange problems. Well, I figured out what specifically was causing a build on one machine to not run on others and even came up with a work-around, but now it leaves me with a nice, specific question: Why?
To reproduce the problem, I create a new InteropUserControl and do the following:
Add a new public struct MyStruct:
Give it a GUID and ComVisible attributes
Add a GetMyStruct member to the _InteropUserControl interface and implement it in InteropUserControl.
MyStruct:
[Guid("49E803EC-BED9-4a08-B42B-E0499864A169")]
[ComVisible(true)]
public struct MyStruct {
public int mynumber;
}
_InteropUserControl.GetMyStruct():
[DispId(7)]
void getMyStruct( int num, ref MyStruct data );
(I have tried returning MyStruct instead of passing by reference, as well.)
InteropUserControl.GetMyStruct() implementation:
public void getMyStruct( int num, ref MyStruct data ) {
data = new MyStruct();
data.mynumber = num * 2;
}
I also sign the assembly and install it to the GAC and register with Regasm. Upon adding it to a new VB6 project and adding a call to GetMyStruct() and compiling on our build machine, it refuses to run on other machines.
To get around this, I had to expose a class to COM instead of the struct, and basically change GetMyStruct to this:
public void GetMyData( int num, MyClass data ) {
data.mynumber = num * 2;
}
In my actual project, I retrieve the struct internally, and then copy all the field values from the struct to the matching members on the instance of the class passed to the method by the client.
So why did a struct cause this behavior and a class worked fine? Is there some magic to exposing a struct to COM for using in VB6?
I think it may have something to do with OLE Automation.
Note: I also tried returning the struct rather than using a ref parameter, but that did not change the behavior.
Edit to add link to project template:
Interop Forms Toolkit 2.0 is the original VB.NET project template and dll. I don't reference the dll, so you may not need to install this.
C# Translations of templates on CodeProject is what I used to create mine (the project template, not the item template). The VB.NET version generates the __InteropUserControl event interface, the _InteropUserControl interface, and a few relevant attributes automagically. Those are explicitly coded in the C# version, and that's about all that's different between the two.
I think I found a solution to this problem.
I had the same exact problem, vb6 breaks when calling a method of an interop library by passing an structure. This is a project I created for testing a DLL interop, so all I have in my project was a form. But I had another project (the main application) with the same reference and it works fine.
After reading Joel post, I wanted to test his solution and in fact id did work (using a class instead a structure). But I have other interops where I'm using structures, so I was quite worried that at any point my application might fail. Additionally I didn't want to do the extra work of creating and exposing interface and a class to replace the structure.
So, I took the code from my form and move it to a public sub in a module. It Worked immediately. By the way, that's how i had implemented the call in the main application which was working ok.
I hope it might help others.
Is there some magic to exposing a
struct to COM for using in VB6?
The article COM Data Types* on MSDN says that structs are supported. Specifically, the MSDN article says that COM structures are defined as:
ByRef VALUETYPE< MyStruct >
There are also a couple of articles on customing your COM-callable wrappers at the bottom of the page, you may wish to review those.
Edit (2016): Original link was broken, so I fixed it to Version 3.5 of the .Net Framework.