I'm trying to use VBA code to invoke a protected API which need authentication with OAuth2. Once I try to open a URL, I'm redirected to the ADFS page for authentication and than I come back.
Now for some applications using CreateObject("InternetExplorer.Application") and the .Navigate URL works fine, for other web app I need to use New InternetExplorerMedium in order to have the code working.
Can you tell me the differences between these objects and why some websites work with one and some work with the other?
Thank you
This way of referring to objects is called "Early" and "Late Binding". From MSDN:
The Visual Basic compiler performs a process called binding when an object is assigned to an object variable.
An object is early bound when it is assigned to a variable declared to be of a specific object type. Early bound objects allow the compiler to allocate memory and perform other optimizations before an application executes.
By contrast, an object is late bound when it is assigned to a variable declared to be of type Object. Objects of this type can hold references to any object, but lack many of the advantages of early-bound objects.
You should use early-bound objects whenever possible, because they allow the compiler to make important optimizations that yield more efficient applications. Early-bound objects are significantly faster than late-bound objects and make your code easier to read and maintain by stating exactly what kind of objects are being used.
TL DR:
The difference is that in early binding you get intellisense and compiling time bonus, but you should make sure that you have added the corresponding library.
Example usage of late binding:
Sub MyLateBinding()
Dim objExcelApp As Object
Dim strName As String
'Definition of variables and assigning object:
strName = "somename"
Set objExcelApp = GetObject(, "Excel.Application")
'A Is Nothing check:
If objExcelApp Is Nothing Then Set objExcelApp = CreateObject("Excel.Application")
'Doing something with the Excel Object
objExcelApp.Caption = strName
MsgBox strName
End Sub
Example usage of early binding:
First make sure that you add the MS Excel 16.0 Object Library from VBE>Extras>Libraries:
Sub MyEarlyBinding()
Dim objExcelApp As New Excel.Application
Dim strName As String
'Definition of variables and assigning object:
strName = "somename"
'A IsNothing check:
If objExcelApp Is Nothing Then Set objExcelApp = CreateObject("Excel.Application")
'Doing something with the Excel Object
objExcelApp.Caption = strName
MsgBox strName
End Sub
Related articles:
Early and Late Binding (my site)
Early Binding vs Late Binding (ex-SO Documentation)
Related
I am using a Function that seems not compatible with VB strict option
It shoots a warning
Late bound resolution; runtime errors could occur.
Public Function RegRead(ByVal Path As String) As String
Dim ws As Object
On Error GoTo ErrHandler
ws = CreateObject("WScript.Shell")
RegRead = ws.RegRead(Path)
Return RegRead
Exit Function
ErrHandler:
RegRead = ""
End Function
it sugests something related to "ws.RegRead"
Can someone tell me how to modify this to not have the warning ?
Your problem is that you have
Dim ws As Object
This means that the compiler doesn't know what methods are available on ws, so all method calls will be late bound. Since the compiler can't check whether that method call is correct (name, parameter types are correct), this check happens at run-time. If anything is wrong then you will have a run-time error. With Option Strict it will give you a warning on that.
Instead you should use APIs designed for .NET, such as the Registry class.
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
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.
The TypeName function is supposed to return a string representing the object name. After testing this with other objects it seems to work. However, for the InternetExplorer object it is returning the IWebBroser2 interface instead. This will prove it but please ensure Microsoft Internet Controls is referenced before running
Sub anotheroverflowquestion()
'Turn on References to Microsoft Internet Controls
Dim IE As New InternetExplorer
MsgBox TypeName(IE)
End Sub
I have a COM object (built with C#.NET) I'm using in VBA (Excel) and it would be really nice enumerate the COM object's fields and reference them automatically. In .NET, this could be done with reflection. Is there any way to do this in VBA?
So, instead of
Dim x As MyCOMObject
Set x = New MyCOMObject
x.f1 = 1
x.f2 = 2
x.f3 = 3
Something more like:
Dim x As MyCOMObject
Set x = New MyCOMObject
For i = 0 to COMFieldCount(x) - 1
SetCOMField(x, GetCOMFieldName(i), i+1)
Next i
You will probably need to refine this code a bit, but it does roughly what you are looking for.
First, you need to add reference to "Typelib information", TLBINF32.dll. I'm not sure if this is a part of Windows or came with some of the numerous SDKs I have installed on my machine, but it is in the System32 folder.
I am assuming that you are setting properties of a COM object, so you will be calling "property put" functions to set the values of your object. You may need to check for the datatypes of those properties, I haven't done any datatype conversion in my code.
The code looks like this:
'Define the variables
Dim tliApp As TLI.TLIApplication
Dim typeinfo As TLI.typeinfo
Dim interface As TLI.InterfaceInfo
Dim member As TLI.MemberInfo
'Initialize typelib reflector
Set tliApp = New TLI.TLIApplication
'Get the type information about myObject (the COM object you want to process)
Set typeinfo = tliApp.ClassInfoFromObject(myObject)
'Set all properties of all the object's interfaces
For Each interface In typeinfo.Interfaces
For Each member In interface.Members
'If this is a "property put" function
If member.InvokeKind = INVOKE_PROPERTYPUT Then
'Invoke the mebmer and set someValue to it.
'Note that you'll probably want to check what datatype to use and do some more error checking
CallByName myObject, member.Name, VbLet, someValue
End If
Next
Next