I am just now learning about how to use the Call Stack within the VBE. One question I have that I cannot seem to find an answer for is what it means when, as I am stepping through two subroutines, why there are two instances of [<Non-Basic Code>] appearing in my Call Stack.enter image description here
I am not sure if it is relevant, but I have included my two subroutines I am stepping through if it helps!
Here are my module-level declared variables (with the exception of Option Explicit):
Option Explicit
Dim fso As Scripting.FileSystemObject
Dim NewFolderPath As String
My first subroutine:
Sub UsingTheScriptingRunTimeLibrary()
Dim OldFolderPath As String
NewFolderPath = Environ("userprofile") & "\Documents\Education\Excel\VBA\Test" 'where we I am copying TO
OldFolderPath = Environ("userprofile") & "\Documents\Education\Excel\VBA\Excel VBA Introduction - Wise Owl" 'where we are copying FROM
Set fso = New Scripting.FileSystemObject
If fso.FolderExists(OldFolderPath) Then
If Not fso.FolderExists(NewFolderPath) Then
fso.CreateFolder NewFolderPath
End If
Call CopyExcelFiles(OldFolderPath) 'since we are sending this as an argument to our CopyExcelFiles sub for its StartFolderPath parameter, we do not need to set it in our next sub
End If
Set fso = Nothing
End Sub
Here is my second sub:
Sub CopyExcelFiles(StartFolderPath As String)
Dim Fil As Scripting.File
Dim SubFol As Scripting.Folder
Dim OldFolder As Scripting.Folder
Set OldFolder = fso.GetFolder(StartFolderPath)
For Each Fil In OldFolder.Files
If Left(fso.GetExtensionName(Fil.Path), 2) = "xl" Then
Fil.Copy NewFolderPath & "\" & Fil.Name
End If
Next Fil
For Each SubFol In OldFolder.SubFolders
Call CopyExcelFiles(SubFol.Path)
Next SubFol
End Sub
Anyways, when I am stepping through my first subroutine, I see my first subroutine listed and one instance of (and this is the literal text) [<Non-Basic Code>]. However, when I begin stepping through my second subroutine, I see two instances of [<Non-Basic Code>] as well as both of my subroutines listed.
Any help on this would be greatly appreciated!
As freeflow mentions in comment to you, the call stack shows [Non-Basic Code] whenever an external code is called.
Look at this part:
For Each Fil In OldFolder.Files
...
Next Fil
You are doing a For Each over the OldFolder.Files; who is the one doing the enumerating? It's not obvious when you program VBA, but if you were writing C, something has to implement the enumerator for the For Each statement to work on. In this case, the Scripting.Files object has implemented a hidden enumerator. Thus, when VBA has a For Each, it's calling the hidden enumerator, and getting a item out of it.
This is not the only way. There are other ways you can see a Non-Basic Code -- a common routine is via event handlers. Say you do a MyWorkbook.Save to save the Excel workbook. That will call events on the workbook such as BeforeSave event and so forth. If you have VBA code in the event handlers, you certainly will see a Non-Basic Code between the calls.
Basically, whenever your code has to pass through some external library to execute the code, the call stack will insert Non-Basic Code to let you know that there's a layer between your VBA code and the previous code that triggered it. There are also cases where your VBA code might be accessed by a non-basic Code entirely so it'll be the first one in the call stack, too.
Related
I was wondering how I would call something in VBA to write its code while running? So I mean if I had the text in A1 read:
sub Write()
Call OtherScript
End Sub
So again that is text inside the cell not in a VBA script. And then in a script while its running it Calls "A1" and the code that's in A1 gets run through VBA without having to actually put it in there.
This is not a real code obviously, I am really just trying to find out if this is possible. A friend that helps me learn to code and works me through a lot of VBA's said he does not know how that would work so Im posting it here to see if possible.
Please, try the following code. Before running it, write in a cell:
sub WriteSomething()
Call OtherScript
End Sub
You cannot create a function/sub named Write because this word is illegal, meaning something else in VBA.
and in the next cell (on the same row):
sub OtherScript()
MsgBox "Hello!"
End Sub
I used "K2". Use it too, or adapt the range from the code. You should also have a Module3 standard module. Please, update the module name with the one existing in your vbProject. Anyhow, the code can also create the module...
Copy the next code and run it:
Sub write_Run_Subs()
'It needs a reference to 'Microsoft Visual Basic For Applications Extensibility x.x'
Dim vbProj As VBProject, objMod As VBComponent, mdlName As String
Dim rngStr As Range, strSub1 As String, strSub2 As String
Set rngStr = Range("K2")
strSub1 = rngStr.value
strSub2 = rngStr.Offset(0, 1).value
mdlName = "Module3" 'of course, it have to exist in ThisWorkbook vbProject
Set vbProj = ThisWorkbook.VBProject
Set objMod = vbProj.VBComponents(mdlName)
objMod.CodeModule.AddFromString strSub1
objMod.CodeModule.AddFromString strSub2
Application.Run mdlName & ".WriteSomething"
End Sub
It is only a simple code without too much error handling, but it should work... If you run it twice, it will insert two such subs, if not preliminarily check their existence.
If adding the necessary reference looks complicated, please firstly run the following code, which will add it:
Sub addExtenssibilityReference()
'Add a reference to 'Microsoft Visual Basic for Applications Extensibility 5.3':
ThisWorkbook.VBProject.References.AddFromGuid _
GUID:="{0002E157-0000-0000-C000-000000000046}", _
Major:=5, Minor:=3
End Sub
About
This question is not about when to use a Function or a Sub, or the difference between ByRef and ByVal (although some insights will be unavoidable).
It is about scenarios which are 'commonly' solved with a Function, but can optionally be solved with a Sub using ByRef in the sense of 'modifying'.
The Code
Consider the following function:
' Returns the worksheet (object) with a specified name in a specified workbook (object).
Function getWsF(wb As Workbook, _
ByVal wsName As String) _
As Worksheet
' 'getWsF' is 'Nothing' by default.
' Try to define worksheet.
On Error Resume Next
Set getWsF = wb.Worksheets(wsName)
End Function
You can utilize it like the following:
' Writes the name of a specified worksheet, if it exists, to the `Immediate` window...
Sub testFunction()
Const wsName As String = "Sheet1"
Dim wb As Workbook
Set wb = ThisWorkbook ' The workbook containing this code.
' Define worksheet.
Dim ws As Worksheet
Set ws = getWsF(wb, wsName)
' Test if worksheet exists.
If Not ws Is Nothing Then
Debug.Print "The worksheet name is '" & ws.Name & "'."
Else
Debug.Print "Worksheet '" & wsName & "' doesn't exist in workbook '" _
& wb.Name & "'."
End If
End Sub
But you can also write each of the procedures in the following way:
' Although 'ByRef' is not necessary, I'm using it to indicate that whatever
' its variable is referring to in another procedure (in this case
' a worksheet object), is going to be modified (possibly written to
' for other datatypes).
Sub getWsS(ByRef Sheet As Worksheet, _
wb As Workbook, _
ByVal wsName As String)
' 'Sheet' could be 'Nothing' or an existing worksheet. You could omit
' the following line if you plan to use the procedure immediately
' after declaring the worksheet object, but I would consider it
' as too risky. Therefore:
' 'Reinitialize' worksheet variable.
Set Sheet = Nothing
' Try to define worksheet.
On Error Resume Next
Set Sheet = wb.Worksheets(wsName)
End Sub
' Writes the name of a specified worksheet, if it exists, to the `Immediate` window...
Sub testSub()
Const wsName As String = "Sheet1"
Dim wb As Workbook
Set wb = ThisWorkbook ' The workbook containing this code.
' Define worksheet.
Dim ws As Worksheet
getWsS ws, wb, wsName
' Test if worksheet exists.
If Not ws Is Nothing Then
Debug.Print "The worksheet name is '" & ws.Name & "'."
Else
Debug.Print "Worksheet '" & wsName & "' doesn't exist in workbook '" _
& wb.Name & "'."
End If
End Sub
Side by Side
Procedure
Function getWsF(wb As Workbook, _ Sub getWsS(ByRef Sheet As Worksheet, _
wsName As String) _ wb As Workbook, _
As Worksheet wsName As String)
Set Sheet = Nothing
On Error Resume Next On Error Resume Next
Set getWsF = wb.Worksheets(wsName) Set Sheet = wb.Worksheets(wsName)
End Function End Sub
Usage (relevant)
' Define worksheet. ' Define worksheet.
Dim ws As Worksheet Dim ws As Worksheet
Set ws = getWsF(wb, wsName) getWsS ws, wb, wsName
The Question(s)
Is the second solution viable?
I'm looking for a proper description of what each of the relevant two procedures do and some insights
in terms of common practice, readability, efficiency, pitfalls ...
In your case, I would use the Function approach and here are a few reasons:
I can use the result of the Function without storing the returning variable:
With getWsF(ThisWorkbook, "Sheet1")
'...
End With
Obviously, I would need to be sure it never returns Nothing or have some error handling in place.
or
DoSomething getWsF(ThisWorkbook, "Sheet1")
where DoSomething is a method expecting a Worksheet/Nothing
As #TimWilliams mentioned in the comments, if you don't expect multiple return values then this is the "expected" way to do it. A well-established convention is that if the method has no return value it should be a Sub. If it has a single return value then it should be a Function. If it returns multiple values then it should also be a Function and:
you either use a Class or Type to pack them as one result
or, the Function returns a primary value as the result and the rest of the return values ByRef (see #UnhandledException's answer for an example).
If you ever need to call the method with Application.Run then Function is safe. Using a Sub often leads to Automation Errors or code simply stops running after the method is executed. It doesn't matter if you need to use the result of the Function or not, don't call a Sub with Application.Run if you don't want nasty errors. Of course, to avoid issues with Application.Run, you could have a Function that doesn't get assigned a return value and still return the Worksheet ByRef but this would be too confusing for the reader.
Edit #1
Forgot to mention that the Application.Run automation errors are happening when calling methods from a different document (for Excel - different Workbook)
Edit #2
In this section I will try to address the proper description side of your question, without doing a begginer and an advanced explanation but a combined one.
Difference between a Sub and a Function
A Sub is just a function that does not return a value after the function executes. In lots of languages, such a function is called a Void Function.
The implications is that a Sub is just a stand-alone statement. It cannot be called from inside an expression. You can only call it with one of:
MySub [argsList]
Call MySub([argsList])
On the other hand, a Function can be used inside statements like:
arguments to other methods e.g. DoSomething MyFunction(...); Debug.Print MyFunction(...)
assignment e.g. x = MyFunction(...)
With blocks e.g. With MyFunction(...)
method chaining e.g. MyFunction(...).DoSomething
The convention mentioned above:
A well-established convention is that if the method has no return value it should be a Sub. If it has a single return value then it should be a Function
becomes quite clear when we understand that a Sub does something and a Function returns a single value, by definition.
Similarity between a Sub and a Function
Both value-returning functions (Function in VBA) and void functions (Sub in VBA) are receiving values as parameters. In VBA, it is possible to return results via ByRef parameters. Not all languages support ByRef parameters (e.g. Java - except modifying members of Objects for example).
Note that porting code from a platform that supports ByRef to another one that does not, can be quite time-consuming if the ByRef approach is abused in the source platform.
Difference between ByVal and ByRef parameters
Passing by value (ByVal):
a new memory space is allocated for a new variable which will be of local scope to the method being called
the contents of the original variable are copied in the newly allocated space of the new variable (for Objects the address of the interface Virtual Table is copied instead)
contents of the original variable are NOT changed regardless of what the method does
it is much safer because the programer does not need to keep in mind/care about other parts of the program (specifically the calling method)
Passing by reference (ByRef):
a new variable is created but no new memory space is allocated. Instead the new variable points to the memory space occupied by the original variable being passed from the calling method. Note that for Objects, the original variable is passed entirely (no new variable is created) unless the interface is different (passing a Collection as an Object type parameter - Object stands for IDispatch) but this is a discussion outside of the scope of this answer. Also note that if the parameter is declared as Variant, there are more complex operations happening to facilitate the redirection
the contents of the original variable can now be changed remotely because both the original variable and the newly created one point to the same memory space
it is considered more efficient because no new memory is allocated but this comes with the downside of increasing complexity
Comparison of the presented methods
Now that we have some understanding of the differences, we can look at both the presented methods. Let's start with the Sub:
Sub getWsS(ByRef Sheet As Worksheet, wb As Workbook, ByVal wsName As String)
Set Sheet = Nothing
On Error Resume Next
Set Sheet = wb.Worksheets(wsName)
End Sub
First of all, there should be an On Error GoTo 0 statement before the End Sub because otherwise the Error 9 is propagated up the calling chain (if sheet not found) and can affect logic inside other methods, long after the getWsS method has returned.
The method name starts with the verb "get" which implies that this method returns something but the method declaration is a Sub which is, by definition, more like a Do Something than a Return Something. Readability is certainly affected.
There is a need for an extra ByRef parameter to return the single piece of result. Implications:
it affects readability
it requires a declared variable inside the calling method
the result cannot he chained/used in other expressions within the calling method
it requries the extra line Set Sheet = Nothing to make sure the original variable does not retain previous contents
Now, let's look at the Function approach:
Function getWsF(wb As Workbook, ByVal wsName As String) As Worksheet
On Error Resume Next
Set getWsF = wb.Worksheets(wsName)
End Function
Same as before, there should be an On Error GoTo 0 statement before the End Function because otherwise the Error 9 is propagated up the calling chain. Also, the Workbook can be passed ByVal as best practice.
Obvious differences:
the name getSomething is perfect for a function that returns Something. Readability is far better than the Sub couterpart
the reader/maintainer of the code instantly knows that the function returns a Worksheet just by looking at the return type (as opposed to looking through a list of ByRef parameters and figuring out which one is the return variable)
the result can be chained/used in expressions
no extra lines of code are needed, the default returning value is already Nothing
the most widely accepted convention is used
I've used CTimer and it seems like on my x64 machine, the Sub approach runs faster with about 20ms when running the methods for a million times. Are these minor efficiency gains worth the loss in readability and flexibility of use? That is something that only the maintainer of the code base can decide.
To answer your question directly:
Q: Is it viable?
A: Yes, it will compile and carry out the functionality that you're expecting.
The grey area comes about when you ask should you do this.
There's definitely nothing stopping you (assuming you aren't subject to some company coding standards or anything). Typically however, functions are used to take in parameters, perform some kind of logic and return a value or object at the end of that logic.
Functions are typically non-destructive and don't change the values or properties of the input parameters. This becomes especially important for code readability and maintenance because other developers (and even yourself a few months from now) will read the code expecting functions to behave in a certain way.
Sub routines on the other hand are not expected to return anything, and so they are used to run concise, related sections of code that carry out some kind of logic relevant to the application. Going back to the point of readability and maintenance, it's reasonable to assume that objects and properties will change inside of a sub routine and so again this makes the developer's life a little easier.
Ultimately there's no hard and fast rules - but there are years of experience and best practice which are often good advice to take on in these scenarios :)
A good example for using both, a function and a ByRef parameter is a 'Try' function:
Public Function TryGetMyValue(ByRef outMyValue As Long) As Boolean
On Error Goto Catch
outMyValue = GetMyValue() 'Does anything to retrieve the value and could raise an error...
TryGetMyValue = True
Done:
Exit Function
Catch:
Resume Done
End Function
It could be used like that:
Public Sub TestTryGetMyValue()
Dim myValue As Long
If Not TryGetMyValue(myValue) Then Exit Sub
Debug.? "MyValue is: " & myValue
End Sub
Is it possible to execute code written in specific excel cells?
For example: In Range("A1") I have text -> debug.print("Test"). I need some way to execute this from VBA code.
Is it possible?
First, use this answer to ensure programmatic access to the VBA model. Then, use this method that makes a string parser for a simple command located in a worksheet cell. This should be enough to get you going.
Demo:
Option Explicit
Sub test()
Dim strCode As String
strCode = Sheet1.Range("A1").Text
StringExecute strCode
End Sub
Sub StringExecute(s As String)
' Credits to A.S.H., see https://stackoverflow.com/questions/43216390/how-to-run-a-string-as-a-command-in-vba
Dim vbComp As Object
Set vbComp = ThisWorkbook.VBProject.VBComponents.Add(1)
vbComp.CodeModule.AddFromString "Sub foo()" & vbCrLf & s & vbCrLf & "End Sub"
Application.Run vbComp.Name & ".foo"
ThisWorkbook.VBProject.VBComponents.Remove vbComp
End Sub
No. You can't store code in the worksheet. But you can store the name of a procedure in a worksheet cell and then call that procedure using Application.Run
Short answer: No
You can only run code that is within the VBA editor. Therefore it is not possible to run code from a cell directly.
But …
But what you can do (as a workaround) is writing a procedure that extracts the VBA code from that cell and includes it into a module and then runs that code.
However even if this is possible this would be a task for a pro-user and it is not recommended for beginners (you should know what you do here).
Nevertheless if you want to have a look into how to add procedures/functions into the VBA Editor have a look at Programming The VBA Editor.
The closest you can easily get to your goal is to use VBA User Defined Functions. These can be called from worksheet formulas in the same way as native Excel functions.
But they have limitations compared to a VBA subroutine - the main one being that they can only return information to the cells that they occupy.
It seems that Set Object = Nothing didn't destroy the Fs Object in this code:
Sub Test2()
Dim Fs As New FileSystemObject
Set Fs = Nothing
MsgBox Fs.Drives.Count ' this line works
End Sub
The last line works with no errors!. thats mean Fs Object is still exists, right?.
So how to destroy this Fs Object.
Another way to ensure proper destruction of an object, is to yield its object reference to a With block (i.e. don't declare a local variable):
Sub Test()
With New FileSystemObject
MsgBox .Drives.Count
End With
End Sub
The object only ever exists inside the With block, and when execution reaches the End With token, if you try it with a custom class module you'll notice that the the class' Class_Terminate handler runs, effectively confirming the proper destruction of the object.
As for the As New quirk, as was already explained, if you intend to set the object reference to Nothing inside that scope, don't declare it with As New, because VBA will set the object reference to Nothing, but will also happily ("helpfully") create a new instance for you as soon as you re-reference it again, be it only to verify that the object Is Nothing.
Side note, this (annoying) counter-intuitive behavior is specifically what's behind the reasoning for Rubberduck's Object variable is self-assigned code inspection:
(note: if you've got code you want to inspect, know that it runs much faster in the actual VBE than on the website)
(if it wasn't clear already: I'm heavily involved with the Rubberduck project)
it must have to do with the "declare & instantiate" pattern, most often addressed as a "to be avoided" pattern
if you split them, you get Nothing after setting it to:
Sub Test2()
Dim Fs As FileSystemObject
Set Fs = New FileSystemObject
Set Fs = Nothing
MsgBox Fs.Drives.Count ' this line DOESN'T work
End Sub
This amended code seems to work fine. Seems a nuance with the dim/new on the same line. Hopefully someone else can give a better idea as to the reasoning.
As others have commented, if it is a dim within the sub then it will be collected after the sub completes anyway.
Sub Test2()
Dim Fs As FileSystemObject
Set Fs = New FileSystemObject
Set Fs = Nothing
End Sub
I have two spreadsheets... when one gets modified in a certain way I want to have a macro run that modifies the second in an appropriate manner. I've already isolated the event I need to act on (the modification of any cell in a particular column), I just can't seem to find any concrete information on accessing and modifying another spreadsheet (this spreadsheet is located on a different LAN share also... the user has access to both, though).
Any help would be great. References on how to do this or something similar are just as good as concrete code samples.
In Excel, you would likely just write code to open the other worksheet, modify it and then save the data.
See this tutorial for more info.
I'll have to edit my VBA later, so pretend this is pseudocode, but it should look something like:
Dim xl: Set xl = CreateObject("Excel.Application")
xl.Open "\\the\share\file.xls"
Dim ws: Set ws = xl.Worksheets(1)
ws.Cells(0,1).Value = "New Value"
ws.Save
xl.Quit constSilent
You can open a spreadsheet in a single line:
Workbooks.Open FileName:="\\the\share\file.xls"
and refer to it as the active workbook:
Range("A1").value = "New value"
After playing with this for a while, I found the Michael's pseudo-code was the closest, but here's how I did it:
Dim xl As Excel.Application
Set xl = CreateObject("Excel.Application")
xl.Workbooks.Open "\\owghome1\bennejm$\testing.xls"
xl.Sheets("Sheet1").Select
Then, manipulate the sheet... maybe like this:
xl.Cells(x, y).Value = "Some text"
When you're done, use these lines to finish up:
xl.Workbooks.Close
xl.Quit
If changes were made, the user will be prompted to save the file before it's closed. There might be a way to save automatically, but this way is actually better so I'm leaving it like it is.
Thanks for all the help!
Copy the following in your ThisWorkbook object to watch for specific changes. In this case when you increase a numeric value to another numeric value.
NB: you will have to replace Workbook-SheetChange and Workbook-SheetSelectionChange with an underscore. Ex: Workbook_SheetChange and Workbook_SheetSelectionChange the underscore gets escaped in Markdown code.
Option Explicit
Dim varPreviousValue As Variant ' required for IsThisMyChange() . This should be made more unique since it's in the global space.
Private Sub Workbook-SheetChange(ByVal Sh As Object, ByVal Target As Range)
' required for IsThisMyChange()
IsThisMyChange Sh, Target
End Sub
Private Sub Workbook-SheetSelectionChange(ByVal Sh As Object, ByVal Target As Range)
' This implements and awful way of accessing the previous value via a global.
' not pretty but required for IsThisMyChange()
varPreviousValue = Target.Cells(1, 1).Value ' NB: This is used so that if a Merged set of cells if referenced only the first cell is used
End Sub
Private Sub IsThisMyChange(Sh As Object, Target As Range)
Dim isMyChange As Boolean
Dim dblValue As Double
Dim dblPreviousValue As Double
isMyChange = False
' Simple catch all. If either number cant be expressed as doubles, then exit.
On Error GoTo ErrorHandler
dblValue = CDbl(Target.Value)
dblPreviousValue = CDbl(varPreviousValue)
On Error GoTo 0 ' This turns off "On Error" statements in VBA.
If dblValue > dblPreviousValue Then
isMyChange = True
End If
If isMyChange Then
MsgBox ("You've increased the value of " & Target.Address)
End If
' end of normal execution
Exit Sub
ErrorHandler:
' Do nothing much.
Exit Sub
End Sub
If you are wishing to change another workbook based on this, i'd think about checking to see if the workbook is already open first... or even better design a solution that can batch up all your changes and do them at once. Continuously changing another spreadsheet based on you listening to this one could be painful.