How to view VB6 control-level variables in WinDbg? - com

I have a crash file where I can see that one of my own VB6 user controls is responsible for the crash; i.e. one of its methods is part of the stack trace and I can see the line responsible.
From here, I'd like to inspect the state of its member variables. How do I do this?
Note: I also have the private symbols for my controls. The problem is being able to inspect "Me". The command !object address_of_Me doesn't seem to do the trick and so I'm at a loss.
Thank you.

It's been 10 years since I had to do this in VB6, but I remember a lot of Printer.Print statements in my past life :)
I used to do things like this for debugging (but not for release code)
Sub MySub
On Error Goto ErrorTrap
Dim intX as integer
Dim intY as integer
' do some horrible error here
Exit Sub
ErrorTrap:
Printer.Print "Error"
Printer.Print intX
Printer.Print intY
Printer.Print ...
End Sub

well, codeSMART have one option install global handle on your application first call to SetUnhandledExceptionFilter (win api) is should be installed when load your module main or master form when is closing the program so call to SetUnhandledExceptionFilter.
The code is little long so copy methods names y api calls
Public Sub InstallGlobalHandler()
On Error Resume Next
If Not lnFilterInstalled Then
Call SetUnhandledExceptionFilter(AddressOf GlobalExceptionHandler)
lnFilterInstalled = True
End If
End Sub
Public Sub UninstallGlobalExceptionHandler()
On Error Resume Next
If lnFilterInstalled Then
Call SetUnhandledExceptionFilter(0&)
lnFilterInstalled = False
End If
End Sub
Also here is Record Structure y apis declarations for the module
- CopyMemory
- SetUnhandledExceptionFilter
- RaiseException
' Public enums
-EExceptionType
-EExceptionHandlerReturn
-Private Const EXCEPTION_MAXIMUM_PARAMETERS = 15
' Private record structure
-Private Type CONTEXT
'Structure that describes an exception.
-Private Type EXCEPTION_RECORD
'Structure that contains exception information that can be used by a debugger.
-Private Type EXCEPTION_DEBUG_INFO
-Private Type EXCEPTION_POINTERS
Take a revised that How to route the exe exception back to VB6 app?

Related

Terminate method not called in Access 2021

Anyone else finding that their Terminate() method in Access isn't being called?
Here's my code for my cBusyPointer class with the comments removed for brevity:
Option Compare Database ' Use database order for string comparisons
Option Explicit ' Declare EVERYTHING!
Option Base 1 ' Arrays start at 1
Public Sub show()
DoCmd.hourGlass True
End Sub
Private Sub Class_Terminate()
DoCmd.hourGlass False
End Sub
Typical usage is:
Sub doTehThings()
Dim hourGlass As New cBusyPointer
hourGlass.show()
' Do all teh things
End Sub
In previous versions, this class would restore the hourglass whenever the object went out of scope and was destroyed.
I even tried to manually destroy my hourglass object:
Sub doTehThings()
Dim hourGlass As cBusyPointer
Set hourGlass = New cBusyPointer
hourGlass.show()
' Do all teh things
Set hourGlass = Nothing
End Sub
The only way to fix this is to add a hide() method and call that.
Has anyone else encountered this problem?
I cannot replicate the issue. The Terminate() method is called upon reaching the Set hourGlass = Nothing.
A couple of points:
Dim hourGlass As New cBusyPointer
This will create a new instance every time you call the hourGlass variable even to set it to Nothing. See the answer in the link below for additional info:
What's the difference between Dim As New vs Dim / Set
You should always use Dim/Set when working with objects.
hourGlass.show()
This does not even compile in VBA. Subs do not accept parentheses even when arguments are being expected, unless they are preceded with the Call keyword.
Lastly, the cleanest way to reference an object is to access it using the With statement which ensures the object is terminated when the End With statement is reached.
With New cBusyPointer
.show
End With

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.

Kill Excel on Error

I am hoping you can help me here, in the past you all have been great. I have tried every variation of the kill script for killing excel from vb.net, to no avail.
First I can't post explicit code on here because it is my company's proprietary software, but I can tell you a few things. Also there are over 28,000 lines of code.
I am not using
Imports Excel = Microsoft.Office.Interop.Excel
due to the fact that we have to accommodate different variations of clients software. I am creating the new excel as an object as such
Dim XLObj As Object = CreateObject("Excel.Application")
I have seen this used on several other sites but the kill function they are using is when you save and then close it, which I'm not doing.
The error message I am getting says that "Com object that has been separated from its underlying RCW cannot be used". I'm not sure where this com object is because I have released the sheets, workbook and then the application.
Oh and I don't want to use the excel.kill() because if a client already has the excel open I don't want to kill it without saving it. I only want to kill the newly generated excel process that doesn't have a window open associated with it.
My questions are as follows
I need to be able to close the Excel application when/if the open fails. So say I am click a link and it opens the dialog box to select an Excel template to load but either the data from the database is corrupt or the sql statement is broken. The program throws and error and then Excel should close in the Task Manager. Unfortunately it doesn't close hence the problem.
is there a way to close only the newly created process id? I have tried to use the directions here but it doesn't work either. When I do that it gives me a different error "Value cannot be null Parameter name: o". The line that is throwing the error is on (from the link)
Marshal.FinalReleaseComObject(tempVar)
I only tried this because we are using the With on the XLObj. The With is in reference to the workbook itself so shouldn't it be released when I close the workbook? And being as I'm causing it to error on purpose at the moment it shouldn't reach the With statement anyway.
Is there a way to tell which com object is not closing?
Things I have tried:
This releaseObject that I found on the internet. (don't ask me where I've been through about 75 pages)
Private Sub releaseObject(ByRef obj As Object)
Try
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(obj)
If obj Is Nothing Then
Else
obj = Nothing
End If
Catch ex As Exception
If obj Is Nothing Then
Else
obj = Nothing
End If
Finally
GC.Collect()
GC.WaitForPendingFinalizers()
End Try
End Sub
This is used in conjunction with this function (which was pieced together from the many sites I have been on)
Public Sub CloseExcel(ByRef WorkBook As Object, ByRef Application As Object)
Dim xLSheet As Object = WorkBook.Sheets
For Each xLSheet In WorkBook.Sheets
If xLSheet IsNot Nothing Then
releaseObject(xLSheet)
End If
If xLSheet IsNot Nothing Then
Kill(xLSheet)
End If
Next
If WorkBook IsNot Nothing Then
WorkBook.Close(False)
End If
If WorkBook IsNot Nothing Then
Kill(WorkBook)
End If
releaseObject(WorkBook)
If Application IsNot Nothing Then
Application.Quit()
End If
If Application IsNot Nothing Then
Kill(Application)
End If
releaseObject(Application)
GC.Collect()
GC.WaitForPendingFinalizers()
Application.Quit()
End Sub
and because it is also referenced the Kill function
Public Sub Kill(ByRef obj As Object)
Try
System.Runtime.InteropServices.Marshal.FinalReleaseComObject(obj)
Catch ex As Exception
MessageBox.Show("moduleExcel.Kill " & ex.Message)
Finally
obj = Nothing
End Try
End Sub
any help would be greatly appreciated.
Ok so for those of you having this exact same issue. I do have a solution for you. Yes the above code does work but for a few minor adjustments.
you need to take out all the code in the CloseExcel sub and place it EXACTLY where you want it to close. So if you want it to close if the program errors out, put after the catch statement. You cannot call a Sub and pass in your objects and expect it to kill the process.
you need a few bits above the opening of the new Excel process. and they are as follows.
'declare process for excel
Dim XLProc As Process
'loads the financials excel bookmarks
'this will be where you declare your new excel opbject
Dim XLObj As Object = CreateObject("Excel.Application")
'get window handle
Dim xlHWND As Integer = XLObj.hwnd
Dim ProcIDXL As Integer = 0
'get the process ID
GetWindowThreadProcessId(xlHWND, ProcIDXL)
XLProc = Process.GetProcessById(ProcIDXL)
and of course you will need the GetWindowThreadProcessId which I got from the link I included in the original question. I am posting it here so you don't have to search for it.
<System.Runtime.InteropServices.DllImport("user32.dll", SetLastError:=True)> _
Private Function GetWindowThreadProcessId(ByVal hWnd As IntPtr, ByRef lpdwProcessId As Integer) As Integer
End Function
This code will only close the single process you have it associated with, it will not close other open Excel files. Our clients sometimes will have multiple files open and we don't want to close them without telling them. This KILLS the Excel process that was created at run time when the system Errors out.

Excel VBA - QueryTable AfterRefresh function not being called after Refresh completes

I am developing an Excel (2010+) Application using VBA and have run into an issue where the AfterRefresh event function is not being invoked once the query finishes executing.
I have not been able to find many decent resources or documentation for how to have this event function triggered in a Class Module. I decided to use the Class Module design route instead of putting the event handlers in the worksheet after receiving a response to an earlier question about QueryTables (found here Excel VBA AfterRefresh).
Here is the code for my Class Module called CQtEvents
Option Explicit
Private WithEvents mQryTble As Excel.QueryTable
Private msOldSql As String
' Properties
Public Property Set QryTble(ByVal QryTable As QueryTable): Set mQryTble = QryTable:
End Property
Public Property Get QryTble() As QueryTable: Set QryTble = mQryTble:
End Property
Public Property Let OldSql(ByVal sOldSql As String): msOldSql = sOldSql:
End Property
Public Property Get OldSql() As String: OldSql = msOldSql:
End Property
Private Sub Class_Initialize()
MsgBox "CQtEvents init"
End Sub
' Resets the query sql to the original unmodified sql statement
' This method is invoked when the Refresh thread finishes executing
Private Sub mQryTble_AfterRefresh(ByVal Success As Boolean)
' Problem is here
' This function is never called :( Even if the query successfully runs
Me.QryTble.CommandText = Me.OldSql
End Sub
Here is a quick snapshot of the code the creates an instance of this class, finds a relevant QueryTable, then calls Refresh
Option Explicit
Sub RefreshDataQuery()
'Dependencies: Microsoft Scripting Runtime (Tools->References) for Dictionary (HashTable) object
'From MGLOBALS
cacheSheetName = "Cache"
Set cacheSheet = Worksheets(cacheSheetName)
Dim querySheet As Worksheet
Dim interface As Worksheet
Dim classQtEvents As CQtEvents
Set querySheet = Worksheets("QTable")
Set interface = Worksheets("Interface")
Set classQtEvents = New CQtEvents
Dim qt As QueryTable
Dim qtDict As New Scripting.Dictionary
Set qtDict = UtilFunctions.CollectAllQueryTablesToDict
Set qt = qtDict.Item("Query from fred2")
''' Building SQL Query String '''
Dim sqlQueryString As String
sqlQueryString = qt.CommandText
Set classQtEvents.QryTble = qt
classQtEvents.OldSql = sqlQueryString ' Cache the original query string
QueryBuilder.BuildSQLQueryStringFromInterface interface, sqlQueryString
' Test message
MsgBox sqlQueryString
qt.CommandText = sqlQueryString
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
''' CLEAN UP '''
' Free the dictionary
Set qtDict = Nothing
End Sub
Also here is a screenshot of the Module structure http://imgur.com/8fUcfLV
My first thought on what might be the issue was passing the QueryTable by value. I am not the most experienced VBA developer, but I reasoned this would create a copy and be calling the event on an unrelated table. However, this was not the case and passing by Reference did not fix the problem either.
Also the query is confirmed to run successfully as the data is correctly showing up and being refreshed.
EDIT
I added the BeforeRefresh event function to CQtEvents class Module and confirmed this function is called once Refresh is called
Private Sub mQryTble_BeforeRefresh(Cancel As Boolean)
MsgBox "Start of BeforeRefresh"
End Sub
How might I alter this code get my QueryTable from the QTableModule's RefreshDataQuery() Sub routine to have the AfterRefresh function invoked when the query is successfully ran?
How to catch the AfterRefresh event of QueryTable?
Explanation: in your situation, before event was fired you lost reference of your QueryTable by setting it to nothing when you made cleaning or procedure ended.
General solution: you must be sure that your code is still running and/or you need to keep any references to your QueryTable.
1st solution. When calling QT.Refresh method set the parameter to false in this way:
qt.Refresh false
which will stop further code execution until your qt is refreshed. But I don't consider this solution to be the best one.
2nd solution. Make your classQtEvents variable public and after RefreshDataQuery sub is finished check the status with some other code.
in you CQtEvents class module add the following public variable:
Public Refreshed As Boolean
in your BeforeRefresh event add this:
Refreshed = False
in your AfterRefresh event add this line of code:
Refreshed = True
Make your classQtEvents variable declaration public. Put this before Sub RefreshDataQuery()
Public classQtEvents as CQtEvents
but remove appropriate declaration from within your sub.
Now, even your sub is finished you will be able to check status of refreshment by checking .Refreshed property. You could do it in Immediate or within other Sub. This should work for Immediate:
Debug.Print classQtEvents.Refreshed
3rd solution. (a bit similar to 1st one) Follow steps 1 to 3 from 2nd solution. After you call qt.Refresh method you could add this loop which will stop further code execution until qt is refreshed:
'your code
If Not qt Is Nothing Then
qt.Refresh
Else
' ... Error handling code here...
End If
'checking
Do Until classQtEvents.Refreshed
DoEvents
Loop
Final remark. I hope I didn't mixed up qt variable with classQtEvents variable. I didn't tried and tested any solution using your variables but wrote all above with referenced to code I use.
A github repo that demonstrates the minimum code needed to get this working can be found here.
As mentioned, if your event handler isn't in scope, or your QueryTable reference is lost, you won't catch the event. The key factors to ensuring you catch the event are:
Declare a global variable of your event-handling class module's type outside of any subroutines/methods, at the top of a file (I chose the ThisWorkbook file).
Add a Workbook_Open event handler and instantiate that variable there, so that it is available immediately and will remain in scope (since it's global).
At that point, or at any downstream point when you have a QueryTable you're interested in, pass that QueryTable to the global instance to wire up its events.
(It took me a couple tries to figure this out myself, when someone pointed me in this direction as an answer to this question.)

Problems passing in a UserControl as a parameter in VB6

I have a COM-visible method which looks something like the following:
Public Sub SomeMethod(someControl as Object)
On Error Goto ErrHandler
Dim someSpecificControl as SpecificControl
MsgBox TypeOf someControl is Control
MsgBox TypeOf someControl is SpecificControl
On Error Resume Next
Set someSpecificControl = someControl
On Error Goto ErrHandler
if someSpecificControl is Nothing then
Exit Sub
end if
' do stuff to the control
End Sub
Other components would call this method (i.e. via COM) and pass in a control of type SpecificControl.
My problem is that when run via the debugger, the parameterized control doesn't appear to be of the right type i.e. it exits the sub-routine after the 'cast' fails when I would have expected it not to.
Using TypeOf I have verified that the parameterized object is of type Control (as above) but I cannot work out why it was passed in - apparently - incorrectly. It seems to be behaving correctly when run outside the debugger - but I can't be sure (hence this question).
Can anyone shed any light on this? Could the control have been - somehow - corrupted in the boxing-unboxing process? Is there a better way of doing this?
Edit: I used TypeName as suggested by Kris Erickson and got some interesting results:
MsgBox TypeName(someControl)
MsgBox "someControl is of type SpecificControl: " & TypeOf someControl is SpecificControl
MsgBox "someControl is of type UserControl: " & TypeOf someControl is UserControl
MsgBox "someControl is of type Control: " & TypeOf someControl is Control
I get:
SpecificControl
someControl is of type SpecificControl: False
someControl is of type UserControl: False
someControl is of type Control: True
I guess the only way I have around this is to avoid passing in a UserControl as a parameter.
I'm using VBControlExtender as parameter type
Public Sub SomeMethod(someControl as VBControlExtender)
then I get the references like this
Dim someSpecificControl as SpecificControl
Dim someSpecificControlExt as VBControlExtender
Set someSpecificControl = someControl.object
Set someSpecificControlExt = someControl
Then use someSpecificControlExt to access Left, TabIndex, TabStop, Name, Move, etc. properties of the extender and someSpecificControl to access specific methods/properties of my user control.
FYI, the behaviour of your code depends on whether the user control is implemented in the current project or referenced in an ocx. I'm using Matt Curlands direct user control access hack too, which allows me to do this
Dim someSpecificControl as DirectSpecificControl
so that someSpecificControl props/methods are accessed early-bound.
This is how I get someSpecificControlExt (the extender) from the control:
Public Function GetExtendedControl(oCtl As IUnknown) As VBControlExtender
Dim pOleObject As IOleObject
Dim pOleControlSite As IOleControlSite
On Error Resume Next
Set pOleObject = oCtl
Set pOleControlSite = pOleObject.GetClientSite
Set GetExtendedControl = pOleControlSite.GetExtendedControl
On Error GoTo 0
End Function
This is how I get the internal UserControl of the VB6 user control:
Public Function GetUserControl(oObj As Object) As UserControl
Dim pControl As UserControl
Call CopyMemory(pControl, ObjPtr(oObj), 4)
Set GetUserControl = pControl
Call CopyMemory(pControl, 0&, 4)
End Function
The reference GetUserControl returns has a very weird implementaion of QueryInterface -- it seems UserControl interface is specifically dummied to E_NOTIMPLEMENTED.
I don't know why this happens, but I do know that UserControl's are semi magic in VB6. If you pass a UserControl into a function by its actual type, it loses all of its UserControl base class (I know, VB6 doesn't have inheritance but when creating a UserControl you expect certain features).
So if you
Private Sub AdjustControlProperties(oControl as MyUserControl)
...
End Sub
Once the control leaves your your subroutine, it will behave as a Control, not as a UserControl (you will have no access to UserControl properties anymore, and attempts to access them will cause an error). Very strange bug in VB6, and one that caused much pulling of hair to work it out.
Private Sub AdjustControlProperties(oControl as Object)
...
End Sub
And everything is fine. My guess is that you are correct, and UserControls are boxed, unboxed as Control's not UserControls. The only solution to type checking, is to use
TypeName()
to know what type it is, as that does not get corrupted.