Is it possible to pass a custom object (like MyClass[]) from C# to VBA using COM?
If not, which is the best solution to get this working?
I assume you're talking about Excel VBA to C# ...
here's a minimal C# class that does it, in a project w default name ClassLibrary1:
using System;
using System.Runtime.InteropServices;
namespace Tester
{
[ClassInterface(ClassInterfaceType.AutoDual)]
public class TestClass
{
public double D { get; set; } // simple property to get, set a double
public string S { get; set; } // simple property to get, set a string
}
}
and here's VBA to try the class out:
Private Sub foo()
Dim X As New ClassLibrary1.TestClass
X.S = "Hello"
Debug.Print X.S ' prints "hello"
X.D = 12
Debug.Print X.D ' prints a 12
End Sub
and here are the extra things you need to do to make this work:
(1) in C# Project...Properties...Build ==> check "Register for COM interop
(2) in C# Project...Properties...Application...Assembly Information ==>
check "Make assembly COM-visible"
(3) in VBA ... Tools ... References, browse to the C# bin output directory and select the "*.tlb" file
Note: this scheme may fail depending on what you add to the class - I don't think VBA will "see" static classes or classes w other than default constructors. You also cannot map VB collections to .NET collections, but you will be able to pass basic types (double, long) and arrays of the basic types back and forth. Also - the "Autodual" option used is a cheap way to get methods exposed ... easy to get started but less efficient and exposes all public methods. Better practice (but more work) would be to set up your own interfaces. If you expand the members of this TestClass to include instances of other classes you have defined, and if you likewise expose those class methods via AutoDual or via hand-coded interfaces, then those classes and their (non-overloaded) methods will likewise be visible in VBA (with Intellisense).
Hope this helps.
Related
Right now, I am using "string" to enumerate a list of equipment slots on a character.
I am also using "string" to enumerate the class type that the item can be equipped on.
This makes all my methods where I get, remove, generate, etc. items involve having two string parameters that is an equipment slot and class type.
What I really would like is to use 2 classes so that I have 2 strongly typed concepts for the slot and class_type.
The problem is that string is a sealed class and thus I cannot extend it!
A solution I conceived is to use "using SlotType = string;" as alias but I don't know how to have this work GLOBALLY.
How do you define a global alias for a class?
In C# you can create a type alias using "using". Because obviously that term needed a 3rd meanings in C# ;) However 'the scope of a using directive is limited to the file in which it appears.', so it would not apply globally.
There is another option - create a subclass. For example,
public class ListOfIntegers : List<int>{
//Nothing to do here.
}
would give you a sort-off-alias for List<int> that applies everywhere ListOfIntegers is given as Project Reference.
As for not being able to extend something: Just encapsulate it instead.
public class TypedString1 {
public value;
}
public class TypedString2 {
public value;
}
But you may want to set it up so that string overloads are used for stuff like Equality and ToString calls. Also propably a implicit cast to string to make it easier to use.
I'm a bit confused about how I can override constructors requiring arguments when using their classes in a VBA environment.
What works?
I've created several classes in a library, each with an interface to allow full intellisense compatibility when using this library in VBA
With or without constructors, these classes work fine for me e.g.
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("JamHeadArt.ClassEX")]
[Guid("XYZ")]
public partial class ClassEX : IClassEX
{
public ClassEX()
{
// Empty constructor here, some of mine have processes, all work well
}
// Methods/ Properties as outlined by the interface below
}
[ComVisible(true)]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
[Guid("ABC")]
public interface IClassEx
{
// Various methods / fields / properties to be implemented by ClassEX
}
I then add reference to my library and write simple lines of code in VBA to instantiate and access my classes:
Sub Test()
Dim t As JamHeadArt.ClassEX
Set t = New JamHeadArt.ClassEX
' Using t.dot then provides all the methods needed '
End Sub
What goes wrong?
When I create contstructors with arguments (even if optional) in a class, VBA will stop allowing me to create instances of these classes, it tells me the "New" keyword is Invalid and actually won't allow me to choose the class from the intellisense list of objects in my library if I go straight for Dim t As New JamHeadArt.ClassEx even if the parameters are set to optional (therefore not really needed)
The annoying thing here is - I don't actually want my VBA instances to accept parameters via the constructor, they're mainly there for Unit testing and they're optional strings so default to "" ... so I guess my question is something like
Is it possible to override any constructor parameters so when referenced in a VBA environment it will ignore them?
e.g. I really want my constructor to look like this:
public ClassEX(string s = "")
{
}
and in VBA it should work as before Dim t As New JamHeadArt.ClassEX - but it won't with that optional string in there!
You can add an additional constructor, for example:
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[ProgId("JamHeadArt.ClassEX")]
[Guid("XYZ")]
public partial class ClassEX : IClassEX
{
public ClassEX()
{
// Empty constructor here, some of mine have processes, all work well
}
public ClassEX(string foo)
{
// additional constructor, can be used for unit testing etc.
}
// Methods/ Properties as outlined by the interface below
}
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 need to write a c# wrapper for a vb6 application. I always get error 450 ( Wrong number of arguments or property assignment was not valid.) This is my VB Code
Dim DBEngine As New DBEngineNet
Set mDbEProp = DBEngine.Properties("Version") ' <-- ERROR
This code is working, so the problem is the parameter of the property
Dim DBEngine As New DBEngineNet
Set mDbEProps = DBEngine.Properties
Set mDbEProp = mDbEProps("Version") '<-- Working. Results 1.0
Here is my COM-Visible C#-Code. It uses the Interop-Interfaces of the old VB6-MotorApp.
[ComVisible(true)]
public class DBEngineNet : VB6MotorApp.DBEngine
{
public VB6MotorApp.Properties Properties
{
// [return: MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_SAFEARRAY)] Maybe something like this???
get
{
return new PropertiesNet
{
new PropertyNet{Name="Version", Value="1.0"}
};
}
}
Here is the Properties-Object:
[ComVisible(true)]
public class PropertiesNet : VB6MotorApp.Properties, IList<PropertyNet>
{
List<PropertyNet> _properties = new List<PropertyNet>();
public VB6MotorApp.Property this[object Item]
{
get
{
return _properties.FirstOrDefault(p => p.Name == Item.ToString());
}
}
}
Any ideas?
The basic diagnostic tool you need here is OleView.exe, run it from the Visual Studio Command Prompt. Use its File + View typelib command to look at the type libraries and compare them. First on your original VB6 implementation so you have a base-line, next on the type library for your .NET version.
There are inevitably going to be major difference the way you are doing it now, you are exposing too many details of the class implementation. All of the System.Object methods as well as the IList<> implementation methods are going to be visible. Boilerplate is to declare a [ComVisible(true)] interface (VB6 likes their name to start with an _underscore) and hide the class implementation by giving it the [ClassInterface(ClassInterfaceType.None)] attribute. You already have the interface so only the attribute should be necessary.
What you want to look for first in the OleView.exe output is the [dispid] attribute for the DBEngineNet.Properties property. It doesn't act like the default property which is why you have to obtain the property value explicitly in your VB6 code. The default property has dispid(0). You force the value in .NET code by giving it the [DispId(0)] attribute.
You also want to look at the original type library, "VB6MotorApp.Properties" looks wrong. That's a coclass name, not an interface name. Non-zero odds that you should be using VB6MotorApp._Properties. Same for VB6MotorApp._DBEngine.
And look at which interfaces in the coclasses have the [default] attribute. It should be the VB6 interfaces. Probably not an issue if your VB6 snippets work as posted.
I'm having difficulty calling a function from a C# Class in a similar way that I call a function from a VB.NET Module.
I have a C# class General.cs
using System;
namespace XYZ.Classes
{
public static class General
{
//Object Null to Empty Function
public static string NullToEmpty(object obj)
{
var returnString = "";
if (obj != null)
{
returnString = obj.ToString();
}
return returnString;
}
}
}
This type of function can be called in VB.NET from anywhere in the project without declaring the module or prefixing the call - just using
dim x as String = NullToEmpty(obj) - VB.NET
var x = NullToEmpty(obj) - C#
From my googling, it seems a public static class with public static methods may be able to accomplish this in c#.
C# Example:
class foo.cs
namespace XYZ.Classes
{
public class Foo
{
public string doFoo()
{
var obj = null;
var foolish = NullToEmpty(obj);
return foolish;
}
}
}
The function shows up in intellisense (using ReSharper) - but it's not valid(red), so something is not referenced correctly - I'm just guessing...
The point is being able to simply use common 'User Defined' utility functions for null trapping, formatting, etc... - so as not to have to wrap all kinds of stuff in ugly C# code like this:
obj.FooField = dr["Foo"] == null ? "" : dr["Foo"];
Would prefer:
obj.FooField = NullToEmpty(dr["Foo"]);
This becomes even more useful for DateTime applications:
obj.ActivityStartDate = dr["ActivityStartDate"] == null ? "" : Convert.ToDateTime(dr["ActivityStartDate"]).ToString("yyyy-MM-dd HH:mm:ss");
vs:
obj.ActivityStartDate = GetDate(dr["ActivityStartDate"]);
Or Integer Conversion:
cmd.Parameters["#BirthdayDay"].Value = String.IsNullOrEmpty(obj.BirthdayDay) ? 01 : Convert.ToInt32(obj.BirthdayDay);
vs:
cmd.Parameters["#BirthdayDay"].Value = NullToZero(dr["obj.BirthdayDay"]);
Some C# guru must know this :-)
Thanks
Accessing a static method in a class requires you to include the owner class
obj.FooField = General.NullToEmpty(dr["Foo"])
My experience has been that you have a few options:
1) Embed the utility function in a base class that all of your other classes inherit from. This is not really a practical solution because you will probably have classes that inherit from third party or .Net framework classes, which would be very difficult to modify.
2) Use Extension methods to extend your functionality to existing base classes. I think that this would end up being more work than it's worth.
3) Make a very simple-named global method holder class (i.e. Util) and just prefix your methods with this hold name.
We used option 3 to convert a large VB application to C# and it worked quite well. Although it isn't quite a convenient as the VB syntax, once you get used to it, it becomes very easy to work with.
VB.NET adds methods in a Module to the global (unnamed) namespace. This is mostly a back-compat feature, if the team could have removed module support then they would probably have done so. But they couldn't, it would have made it too difficult to port VB6 code to VB.NET. The practice is iffy and doesn't scale at all, you tend to run into trouble when the project becomes large. A problem know as "global namespace pollution".
Programmers tend to work around it by giving method names a prefix. Which works but is pretty awkward since you have to cough up that prefix every time you want to call the method, there is no analogue of the Imports directive for that. IntelliSense suffers greatly as well.
C# just doesn't permit doing this at all. Closest you could get is with extension methods. But don't, get used to the C# Way.