Debug.Assert behavior in MS Access runtime - vba

In many compiled languages, calls to Debug.Assert, or their equivalent, are left out of the compiled production code for performance reasons. However, calls to Debug.Assert still appear to execute in the /runtime versions of MS Access applications.
To test this, I added the following to my startup form:
Private Sub Form_Load()
Debug.Assert UserOK()
End Sub
Function UserOK() As Boolean
UserOK = MsgBox("Is everything OK?", vbYesNo, "Test Debug.Assert") = vbYes
End Function
When I run this in a development environment and click [No] on the MsgBox, execution breaks at the Debug.Assert line (as I would expect).
When I run the same code with the /runtime switch (using a full version of MS Access 2002) I still see the MsgBox, but clicking on [No] does not halt program execution. It seems VBA executes the line but ignores the result. This is not surprising, but it is unfortunate.
I was hoping that Access would skip the Debug.Assert line completely. This means that one must take care not to use Debug.Assert lines that would hurt performance, for example:
Debug.Assert DCount("*", "SomeHugeTable", "NonIndexedField='prepare to wait!'") = 0
Is this behavior documented somewhere? The official documentation in Access appears to be pulled verbatim from VB6:
Assert invocations work only within the development environment. When the module is compiled into an executable, the method calls on the Debug object are omitted.
Obviously, MS Access apps cannot be compiled into an executable. Is there a better alternative than the following workaround?
Private Sub Form_Load()
If Not SysCmd(acSysCmdRuntime) Then Debug.Assert UserOK() 'check if Runtime
End Sub
Function UserOK() As Boolean
UserOK = MsgBox("Is everything OK?", vbYesNo, "Test Debug.Assert") = vbYes
End Function

I don't know if it's any better for your particular use case, but there is an alternative that is better, if you're looking to do this in application agnostic VBA code. VBA has Conditional Compilation. Conditional Complication constants can be declared at the module level, but in this case, it will be better to declare it at the project level.
On the menu bar, click Tools>>Project Properties and type a DEBUGMODE = 1 into the "Conditional Compilation Arguments:" field. (Note: DEBUG won't work as it's a keyword.)
Now you can wrap all of your Debug.Assert() statements in code like this.
#If DEBUGMODE Then
Debug.Assert False
#End If
When you're ready to deploy your project, just go back into the Project Properties dialog and change the argument to DEBUGMODE = 0.
Additional Information:
Utter Access has a fairly comprehensive article on Conditional Compilation.
Directives - MSDN VB 6.0 Language Reference.

Related

Can RubberDuck test if a program ends?

I'm developing tests with RubberDuck, and would like to test MsgBox outputs from a program. The catch is that the program ends right after outputting the MsgBox - there's literally an "End" Statement.
When running a RubberDuck test and using Fakes.MsgBox.Returns, there's an inconclusive yellow result with message "Unexpected COM exception while running tests"
I've tried placing an "Assert.Fail" at the end of the test; however, it seems like the program ending throws things off.
Is it possible for a test in RubberDuck to detect if the program ends?
tldr; No
Rubberduck unit tests are executed in the context of the VBA runtime - that is, the VBA unit test code is being run from inside the host application. Testing results are reported back to Rubberduck via its API. If you look at the VBA code generated when you insert a test module, it gives a basic idea of the architecture of how the tests are run. Take for example this unit test from our integration test suite:
'HERE BE DRAGONS. Save your work in ALL open windows.
'#TestModule
'#Folder("Tests")
Private Assert As New Rubberduck.AssertClass
Private Fakes As New Rubberduck.FakesProvider
'#TestMethod
Public Sub InputBoxFakeWorks()
On Error GoTo TestFail
Dim userInput As String
With Fakes.InputBox
.Returns vbNullString, 1
.ReturnsWhen "Prompt", "Second", "User entry 2", 2
userInput = InputBox("First")
Assert.IsTrue userInput = vbNullString
userInput = InputBox("Second")
Assert.IsTrue userInput = "User entry 2"
End With
TestExit:
Exit Sub
TestFail:
Assert.Fail "Test raised an error: #" & Err.Number & " - " & Err.Description
End Sub
Broken down:
This creates a managed class that "listens" for Asserts in the code being tested and evaluates the condition for passing or failing the test.
Private Assert As New Rubberduck.AssertClass
The FakesProvider is a utility object for setting the hooks in the VB runtime to "ignore" or "spoof" calls from inside the VB runtime to, say, the InputBox function.
Since the Fakes object is declared As New, the With block instantiates a FakesProvider for the test. The InputBox method of Fakes This sets a hook on the rtcInputBox function in vbe7.dll which redirects all traffic from VBA to that function to the Rubberduck implementation. This is now counting calls, tracking parameters passed, providing return values, etc.
With Fakes.InputBox
The Returns and ReturnsWhen calls are using the VBA held COM object to communicate the test setup of the faked calls to InputBox. In this example, it configures the InputBox object to return a vbNullString for call one, and "User entry 2" when passed a Prompt parameter of "Second" for call number two.
.Returns vbNullString, 1
.ReturnsWhen "Prompt", "Second", "User entry 2", 2
This is where the AssertClass comes in. When you run unit tests from the Rubberduck UI, it determines the COM interface for the user's code. Then, it calls invokes the test method via that interface. Rubberduck then uses the AssertClass to test runtime conditions. The IsTrue method takes a Boolean as a parameter (with an optional output message). So on the line of code below, VB evaluates the expression userInput = vbNullString and passes the result as the parameter to IsTrue. The Rubberduck IsTrue implementation then sets the state of the unit test based on whether or not the parameter passed from VBA meets the condition of the AssertClass method called.
Assert.IsTrue userInput = vbNullString
What this means in relation to your question:
Note that in the breakdown of how the code above executes, everything is executing in the VBA environment. Rubberduck is providing VBA a "window" to report the results back via the AssertClass object, and simply (for some values of "simply") providing hook service through the FakesProvider object. VBA "owns" both of those objects - they are just provided through Rubberduck's COM provider.
When you use the End statement in VBA, it forcibly terminates execution at that point. The Rubberduck COM objects are no longer actively referenced by the client (your test procedure), and it's undefined as to whether or not that decrements the reference count on the COM object. It's like yanking the plug from the wall. The only thing that Rubberduck can determine at this point is that the COM client has disconnected. In your case that manifests as a COM exception caught inside Rubberduck. Since Rubberduck has no way of knowing why the object it is providing has lost communication, it reports the result of the test as "Inconclusive" - it did not run to completion.
That said, the solution is to refactor your code to not use End. Ever. Quoting the documentation linked above End...
Terminates execution immediately. Never required by itself but may be placed anywhere in a procedure to end code execution, close files opened with the Open statement, and to clear variables.
This is nowhere near graceful, and if you have references to other COM objects (other than Rubberduck) there is no guarantee that they will be terminated reliably.
Full disclosure, I contribute to the Rubberduck project and authored some of the code described above. If you want to get a better understanding of how unit testing functions (and can read c#), the implementations of the COM providers can be found at this link.

how to access dialog fields in visual basic word macros

I try to save a Word 2010 document to PDF and then want to do something with the file(name) that the user selected for this purpose in the corresponding dialog.
Also, I would like to make a few preconfigurations of dialog settings.
However, the following macro (in Microsoft Visual basic for Aplications 7.0, i.e. the stuff you get when simply recording and then modifying macros) does not work:
Sub MyMacro()
dim retval as long
dim DidTheExportToPdfActuallyTakePlaceSuccessfully as Boolean
dim WhatWasThePdfFilenameTheUserChoseFinally as String
With Dialogs(wdDialogExportAsFixedFormat)
.ExportFormat = wdExportFormatPDF
.OpenAfterExport = True
.OptimizeFor = wdExportOptimizeForPrint
.Range = wdExportAllDocument
.Item = wdExportDocumentContent
.IncludeDocProps = True
.KeepIRM = True
.CreateBookmarks = wdExportCreateNoBookmarks
.DocStructureTags = True
.BitmapMissingFonts = True
.UseISO19005_1 = False
retval = .Show()
' DidTheExportToPdfActuallyTakePlaceSuccessfully = ???
' WhatWasThePdfFilenameTheUserChoseFinally = ???
end with
end sub
First of all, I get errors in all the lines .ExportFormat etc.
It appears these problems are caused by late binding.
To circumvent this problem I tried to add option strict off on top, but that immediately produces a compilation error ("Base or Compare or Explicit or Private expected").
I also read about reflection, but it seems that things necessary for that according to online findings (such as dim x as System.Type or y.gettype()) don't compile either ...
If I simply remove the offending lines, the dialog shows successfully and the pdf export takes places successfully. However, it seems that retval is always 0, no matter if the file is actually exported or the user hit cancel. Not to mention that extracting the actual pdf filename fails in the same way as does pre-filling the dialog options.
I'm a bit at a loss because all googling and searching through MS online help tends to take me only to almost compatible situations (especially, nothing found compiles, see above). What is the right way to achieve my goal?
Broadly speaking, only the older builtin dialogs in Word support "late bound" named parameters. Since around Word 2000, most (if not all) of them only recognise the early bound members of the Dialog class.
Possible workarounds include
try to use Sendkeys (IMO always difficult to get right at the best of
times)
try to control the Dialog window via WIN32+Windows messages (no idea
on feasibility there)
adapt one of the standard Application FileDialogs as best you can (in
this case, probably Application.FileDialog(msoFileDialogSaveAs) ),
then follow up with your own UserDialog that prompts for any other
options that you need, then call ExportAsFixedFormat to do the work.
(+variations on (c), e.g. do some of it in .NET with a WIndows form)

VBA User form with ThemeColorScheme and late binding

I'd like to run a user form with status bar.
I show my form with code bolow.
How should I declare variables and assigning new values to those variables?
Very important: I have to use late binding in my project.
Sub RunMyUserForm()
With MyUserForm
.LabelProgress.Width = 0
.TextBox1 = 1
'to make the progress bar color match the workbook's current theme:
.LabelProgress.BackColor = ActiveWorkbook.Theme.ThemeColorScheme.Colors(msoThemeAccent1)
.Show vbModeless
End With
End Sub
Thank you in advance for your help!
Updated information:
When I try to run my macro with "Option Explicit", it doesn't work (Compile error: Variable not defined - part of code msoThemeAccent1 is marked as yellow color). That's why I asked for help in defining the variables.
When I try to run my macro without "Option Explicit", it dosen't work (Err.Description: "Value is out of range", Err.Number: -2147024809)
When I try to run my macro with early binding (reference to "MS Office Object Library" via Tools/References in VBE) everything works perfect with (and without) "Option Explicit".
Your compiler is seeing msoThemeAccent1 as a variable, and it is undeclared. This is why your code won't run with Option Explicit and also why your code raises an error when you disable Option Explicit. .Colors is a 1-based collection (?) so when you call:
ActiveWorkbook.Theme.ThemeColorScheme.Colors(msoThemeAccent1)
It is compiling to:
ActiveWorkbook.Theme.ThemeColorScheme.Colors(0)
Which raises an expected, albeit cryptic error.
I used some code to check the value of this constant, but in hindsight I should have just referred to the documentation:
http://office.microsoft.com/en-us/excel-help/HV080559557.aspx
This should fix it for you
ActiveWorkbook.Theme.ThemeColorScheme.Colors(5)
Alternatively, if you need to rely on this value in several places in your code, you could declare a variable (public or private, scope depends on what you need it for).
Public Const myAccentColor1 as Long = 5
And then, in your code you could:
ActiveWorkbook.Theme.ThemeColorScheme.Colors(myAccentColor1)
Revised
I understand now, without reference to MS Office Object Library this makes sense. This is one of the few libraries that I maintain a reference to in my XLSB file, so my confusion was a result of the fact that I thought I was using an appropriate late-binding, but I was really using early-binding.

How to call another module without returning to the first one after completion?

This is probably the dumbest question I've ever asked here, but it's hard to find answers to things like this.
I have a program with a bunch of modules/subs that each calculate a different variable. They're pretty complex, so I like to keep them separate. Now I want an earlier module to skip to another module based on user input. I thought I could use the call (sub name) method for this, but then the program returns to where the call line was and continues on that module from where it left off.
Example:
Module 1:
Sub NewPracticeSub()
Call otherpracticesub
MsgBox ("We've gone back to this sub... :(")
End Sub
Module 2:
Sub otherpracticesub()
MsgBox ("We're in the other practice sub!")
End Sub
I don't want it to return to Module 1. What can I do to have it switch control to Module 2 without it then returning to complete Module 1 upon completion of Module 2?
I feel like I just used the most confusing language possible to explain all of this, but thank you for your help anyways!!
Edit: I know I used the words module and sub interchangeably, and I know they're different. I like to keep each sub (which are each very large in my program) in their own modules because it's easier to keep track of them, and easier to explain/demonstrate the application flow to other people.
I think all you're looking for is the command Exit Sub which will make the program leave the subroutine without continuing any further, But the way you usually want to do this is, rather than calling a Sub, rather call a Function that returns a boolean value.
So, for example:
Public Function MyFunc() as Boolean
....
If [good] MyFunc = True
Else MyFunc = False
End Function
Then you could do something along the lines of:
Sub MyCallingSub()
...
If MyFunc = True then Exit Sub
Else ...
End Sub
It just adds in A LOT more felxibility and ability to choose whether you want to continue further in your sub or not.
Hope that makes sense.
Other than using the ugly End statement which I will describe below (and strongly recommend you to avoid), I'm not aware of any way to circumvent the call stack. Even John's response necessarily returns to the calling procedure, and evaluates another statement to determine whether to proceed or end.
This may yield undesirable outcomes, which is why I hesitate to recommend it, in favor of properly structuring your code, loops, etc., with respect to the call stack.
In any case, here is how you can use the End statement within your child subroutines, without needing any sort of public/global variables. This still allows you the flexibility to decide when & where to invoke the End statement, so it need not always be invoked.
Sub NewPracticeSub()
Call otherpracticesub, True
MsgBox ("We've gone back to this sub... :(")
End Sub
Sub otherpracticesub(Optional endAll as Boolean=False)
MsgBox ("We're in the other practice sub!")
If endAll then End '## Only invoke End when True is passed to this subroutine
End Sub
Why I say this method should be avoided, via MSDN:
"Note The End statement stops code execution abruptly, without
invoking the Unload, QueryUnload, or Terminate event, or any other
Visual Basic code. Code you have placed in the Unload, QueryUnload,
and Terminate events of forms and class modules is not executed.
Objects created from class modules are destroyed, files opened using
the Open statement are closed, and memory used by your program is
freed. Object references held by other programs are invalidated.
The End statement provides a way to force your program to halt. For
normal termination of a Visual Basic program, you should unload all
forms. Your program closes as soon as there are no other programs
holding references to objects created from your public class modules
and no code executing."
It will always return but that doesn't mean its a problem. I suggest you use Exit Sub as follows:
Sub NewPracticeSub()
Call otherpracticesub
**Exit Sub**
'Nothing more can execute here so its no longer a worry
End Sub
Module 2:
Sub otherpracticesub()
MsgBox ("We're in the other practice sub!")
End Sub

How can I tell what module my code is executing in?

For a very long time, when I have an error handler I make it report what Project, Module, and Procedure the error was thrown in. I have always accomplished this by simply storing their name via constants. I know that in a Class you get the name programmatically with TypeName(Me), but obviously that only gets me one out of three pieces of information and only when I'm not in a "Standard" module.
I don't have a really huge problem with using constants, it's just that people don't always keep them up to date, or worse they copy and paste and then you have the wrong name being reported, etc. So what I would like to do is figure out a way to get rid of the Constants shown in the example, without losing the information.
Option Compare Binary
Option Explicit
Option Base 0
Option Private Module
Private Const m_strModuleName_c As String = "MyModule"
Private Sub Example()
Const strProcedureName_c As String = "Example"
On Error GoTo Err_Hnd
Exit_Proc:
On Error Resume Next
Exit Sub
Err_Hnd:
ErrorHandler.FormattedErrorMessage strProcedureName_c, m_strModuleName_c, _
Err.Description, Err.Source, Err.Number, Erl
Resume Exit_Proc
End Sub
Does anyone know ways to for the code to tell where it is? If you can conclusively show it can't be done, that's an answer too:)
Edit:I am also aware that the project name is in Err.Source. I was hoping to be able to get it without an exception for other purposes. If you know great, if not we can define that as outside the scope of the question.
I am also aware of how to get the error line, but that information is of course only somewhat helpful without knowing Module.Procedure.
For the project name, the only way I can think of doing this is by deliberately throwing an error somewhere in Sub Main(), and in the error handling code, save the resulting Err.Source into an global variable g_sProjectName. Otherwise, I seem to remember that there was a free 3rd party DLL called TLBINF32.DLL which did COM reflection - but that seems way over the top for what you want to do, and in any case there is probably a difference between public and private classes. And finally, you could use a binary editor to search for the project name string in your EXE, and then try to read the string from the position. Whilst it is frustrating that the names of every project and code module is embedded in the EXE, there seems to be no predictable way of doing this, so it is NOT recommended.
There are several questions here.
You can get the Project Name by calling App.Name
You cannot get the name of the method you are in. I recommend using the automated procedure templates from MZ Tools, which will automatically put in all the constants you need and your headache will be over.
The last piece is possibly having to know the name of the EXE (or lib) that invoked your ActiveX DLL. To figure this out, try the following:
'API Declarations'
Private Declare Function GetModuleFileName Lib _
"kernel32" Alias "GetModuleFileNameA" (ByVal _
hModule As Long, ByVal lpFileName As String, _
ByVal nSize As Long) As Long
Private Function WhosYourDaddy() As String
Dim AppPath As String
Const MAX_PATH = 260
On Error Resume Next
'allocate space for the string'
AppPath = Space$(MAX_PATH)
If GetModuleFileName(0, AppPath, Len(AppPath)) Then
'Remove NULLs from the result'
AppPath = Left$(AppPath, InStr(AppPath, vbNullChar) - 1)
WhosYourDaddy = AppPath
Else
WhosYourDaddy = "Not Found"
End If
End Function
Unfortunately, you'll need to have individual On Error GoTo X statements for individual modules and procedures. The project is always stored in Err.Source. The VBA error handling isn't all that great in this area -- after all, how much good does the project name as the source of the error, as opposed to procedure/module, do you.
If you manually or programatically number your lines (like old-school BASIC), you can use ERL to list the line number the error occurred on. Be warned, though, that an error that occurs on a line without a number will make ERL throw its own error, instead of returning a zero. More information can be found at this blog post.
If you're using Access 2007 (not sure about other Office apps/versions), try this code snippet dug out of the help documentation:
Sub PrintOpenModuleNames()
Dim i As Integer
Dim modOpenModules As Modules
Set modOpenModules = Application.Modules
For i = 0 To modOpenModules.Count - 1
Debug.Print modOpenModules(i).Name
Next
End Sub
And Microsoft includes these remarks:
All open modules are included in the
Modules collection, whether they are
uncompiled, are compiled, are in
break mode, or contain the code
that's running.
To determine whether an individual
Module object represents a standard
module or a class module, check the
Module object's Type property.
The Modules collection belongs to the
Microsoft Access Application object.
Individual Module objects in the
Modules collection are indexed
beginning with zero.
So far, I haven't been able to find anything on referencing the current Project or Procedure. but this should point you in the right direction.
I suggest you take a look at CodeSMART for VB6, This VB6 addin has a customizable Error Handling Schemes Manager, with macros that will insert strings for module name, method name, etc., into your error handling code with a single context menu selection.
Has some other very nice features as well - a Find In Files search that's superior to anything I'd seen till ReSharper, a Tab Order designer, and much more.
At my previous employer, we used this tool for many years, starting with the 2005 version. Once you get used to it,it's really hard to do without it.