VBA Calling Public Sub in Worksheet Module: Crosstalk between Sheet and ThisWorkbook modules - vba

I was so pleased that this works that I thought I would share it...
It allows me to have generic event handlers in the ThisWorkbook module to initiate worksheet-specific code in an anonymous ActiveSheet.
I use the CalByName function to access user-defined, Public methods in the Sheet objects.
I'm using it to re-start a timer that is killed by auto-recover saves and it works great. I've got it on two sheets.
In ThisWorkbook Module:
Private Sub Workbook_AfterSave(ByVal Success As Boolean)
Dim ws As Worksheet
Set ws = ActiveSheet
On Error GoTo afterSaveFailed
CallByName ws, "afterSave", VbMethod
If debugEvents Then Debug.Print timeStamp & ": " & "AfterSave: afterSave called in sheet: " & ws.Name
On Error GoTo 0
Exit Sub
afterSaveFailed:
If debugEvents Then Debug.Print timeStamp & ": " & "AfterSave: afterSave Failed in sheet: " & ws.Name
Err.Clear
End Sub
then in each applicable Worksheet Module:
Public Sub afterSave()
'sheet specific after save handler
End Sub

CallByName is a very old and well known method. :)
Also in your code
What is debugEvents You might want to use Option Explicit
Why _AfterSave and not _BeforeSave? What if afterSave() makes a change to the workbook? You will have to save again before closing.
Also instead of pasting the code in each applicable worksheet and creating duplicate code why not create a common code in a module?
Public Sub afterSave(oWs as Worksheet)
With oWs
'sheet specific after save handler
End With
End Sub
Worst Case Scenario:
Assuming that the afterSave is different for every worksheet. You could always use a Select Case. This will ensure that you have your code in one place. You don't have to hop around to check the code.
Public Sub afterSave(oWs As Worksheet)
With oWs
Select Case .Name
Case "AAA"
Case "BBB"
Case "CCC", "DDD" '<~~ If two or more worksheet have the same code
End With
End Sub

thanks for the comments. My responses are inserted below:
CallByName is a very old and well known method. :)
Which is why I didn't entitle the post "CallByName".
If you read the title, thats what its about. I couldn't find a solution to this on the web so I had to figure out my own method. My point is not about CallByName, its about cross-talk between the modules and using CallByName to maximum effect to achieve this, I'm not suggesting that CallByName is novel.
I guess I could achieve the same thing using RaiseEvents but I think maybe this is more straight-forward.
I was just happy coz VBA was letting me do what I wanted for a change :q
Also in your code
What is debugEvents You might want to use Option Explicit
It's a global flag to control how noisy the immediate window is. Yes I always use option explicit and do so in this case. I assume that's a given.
Why _AfterSave and not _BeforeSave? What if afterSave() makes a change to the workbook? You will have to save again before closing.
Because the stuff I'm doing needs to be done after the save has completed, not before it starts. The Autorecovery save event kills a timer that I'm running so I need to re-start it after the save.
Also instead of pasting the code in each applicable worksheet and
creating duplicate code why not create a common code in a module?
Public Sub afterSave(oWs as Worksheet)
With oWs
'sheet specific after save handler
End With End Sub
Worst Case Scenario:
Because its "sheet specific"
Assuming that the afterSave is different for every worksheet. You
could always use a Select Case. This will ensure that you have your
code in one place. You don't have to hop around to check the code.
Public Sub afterSave(oWs As Worksheet)
With oWs
Select Case .Name
Case "AAA"
Case "BBB"
Case "CCC", "DDD" '<~~ If two or more worksheet have the same code
End With End Sub
OK, I'll think about that: thanks for the suggestion :)
When I have generic features, I prefer to handle that with Class Modules. To me that's neater: I just instance the object in the sheet to expose the generic features, and as in your suggestion, I only have to check it once. I also use the Class_Initialize and Class_Terminate events to transparently manage life cycle of the generic features. I couldn't do that with generic code in the ThisWorkbook object.
Thats how I handle general functionality, for object-specific features, my preference is to collect the code that is specific to the object, in the actual object: I prefer shy objects. There's really no need to expose that detail to supervisory code. Also, if I'm working on the sheet, I don't have to skip back to a general module and I don't have to try and remember where I put the code or invent some convention, coz it's right there in sheet.
Thank you very much for taking the time to respond in detail!

Related

Referencing Excel Cells in VBA

My main problem is assigning a variable an manipulating it in VBA.
I know that this code works, but I'm not sure if it this is a proper way to assign a variable in VBA like I have with currentcell = Cells(i, 1).Value
Sub C()
Dim i As Integer
For i = 1 To 4
currentcell = Cells(i, 1).Value
MsgBox currentcell
Next i
End Sub
What the macro recorder and a lot of tutorials won't tell you, is that these calls are implicitly referring to the ActiveSheet, which is obviously implicitly in the ActiveWorkbook. When all you ever have to deal with is a single worksheet, it's probably fine, but reality is that it's never the case, and code written with implicit references to the ActiveSheet is incredibly frail, bug-prone, and the underlying cause behind way too many Stack Overflow questions tagged with vba:
Cells
Range
Name
Rows
Columns
Whenever you use any of those, you're making calls against some global-scope Property Get accessor that "conveniently" fetches the ActiveSheet reference for you:
Screenshot of Rubberduck's toolbar, showing the declaration site of the selected code. Disclaimer: I manage that open-source VBIDE add-in project.
So yes, this "works", but what you'll want is to work off an explicit Worksheet object reference instead. There are many ways to get a Worksheet object reference, but the best one is to not care about where the worksheet is coming from, and take it as a parameter:
Sub DoSomething(ByVal ws As Worksheet)
Dim currentCell As Variant
Dim i As Integer
For i = 1 To 4
currentcell = ws.Cells(i, 1).Value
'in-cell error values can't be converted to a string:
If Not IsError(currentCell) Then MsgBox currentcell
Next i
End Sub
This takes the responsibility of knowing exactly what worksheet to work with away from the procedure, and gives it to the calling code, which can look like this:
DoSomething Sheet1 'uses the sheet's CodeName property
Or this:
DoSomething ThisWorkbook.Worksheets("Period" & n)
Or whatever. There are plenty of ways, each with their own gotchas - often you'll see something like this:
DoSomething Sheets("Sheet1") 'where "Sheet1" is in ThisWorkbook
Except Sheets implicitly refers to the ActiveWorkbook which may or may not be ThisWorkbook, and the Sheets collection could return a Chart object, which isn't a Worksheet. Also if "Sheet1" is in ThisWorkbook (the workbook with the code that's running), then referring to a worksheet by name means your code breaks as soon as the user renames the tab for that sheet.
The most robust way to refer to a worksheet that exists at compile/design-time, is by its CodeName. Select the sheet in the Project Explorer (Ctrl+R), then bring up the Properties toolwindow (F4), and set its CodeName by changing the (Name) property to whatever you need: VBA defines a global-scope global object variable by that name, so you can call it MyAwesomeReport and then refer to it as such in code:
DoSomething MyAwesomeReport
And that code will not break if the user changes the worksheet's "name".
Your code (however small that snippet might be) has other issues, notably it's using undeclared variables, which means you're not using Option Explicit, which is another thing that will end up biting you in the rear end and come to Stack Overflow with an embarrassing question that boils down to a typo - because without Option Explicit specified at the top of every module, VBA will happily compile a typo, making your code behave strangely instead of simply not compiling. These bugs can be excruciatingly hard to find, and they're ridiculously easy to prevent:
Use. Option. Explicit. Always.

Run Code In Worksheets Class Code Module In Another Workbook

This code is in a Workbook. ( In a Worksheets Class code Module )
Sub Testie()
Dim FullPathAndName As String
Let FullPathAndName = "'" & ThisWorkbook.Path & "\" & "NeuProAktuelleMakros.xlsm'"
Application.Run Macro:=FullPathAndName & "!FrmProTypeIn", Arg1:=42
End Sub
In another Workbook, named “NeuProAktuelleMakros.xlsm”, which is in the same Folder, I have this code, (in a Normal Code Module):
Sub FrmProTypeIn(MyArg As Long)
MsgBox prompt:="Got Here :). The answer is " & MyArg & " , I forgot the question"
End Sub
If I run the first code, it makes the second code run , ( which tells me I got there and that the answer is 42, I forgot the question )
I would prefer to have the code, FrmProTypeIn() , (which is currently in a Normal Code Module) to be in a Worksheets Code Module. The Worksheet Name is “FoodsLookUpTable”. The Worksheet Code Name is “Tabelle11”
Is this possible and can you give me the syntax?
If that is not possible, what about a simple work around? – The obvious thing I can think of is to have a Call ing code in a normal Module in Workbook “NeuProAktuelleMakros.xlsm” thus:
Sub CallFrmProTypeIn(MyArg As Long)
Application.Run Macro:=Worksheets("FoodsLookUpTable").FrmProTypeIn(MyArg)
End Sub
Then I modify the first code slightly to this:
Sub Testies2()
Dim FullPathAndName As String
Let FullPathAndName = "'" & ThisWorkbook.Path & "\" & "NeuProAktuelleMakros.xlsm'"
Application.Run Macro:=FullPathAndName & "!CallFrmProTypeIn", Arg1:=42
End Sub
That workaround works. But maybe there is a simpler way?
The syntax would be:
Let FullPathAndName = "'" & ThisWorkbook.Path & "\" & "NeuProAktuelleMakros.xlsm'"
Application.Run Macro:=FullPathAndName & "!Sheet1.FrmProTypeIn", Arg1:=42
# Peh
Hi Peh
I had not tried that. That and variations of it do work . Great Thanks. I had been told that this was not possible as I was told that a code cannot be run from a Worksheets code module in another Workbook.
I am wondering if this is telling me that technically I the code has become now a Property of the Worksheets Class module.
So the first three lines here are variations of your suggestion that work.
The last two are typical variation that I had been trying which do not work
Sub PehTesties()
Workbooks("NeuProAktuelleMakros.xlsm").Application.Run Macro:=Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
Application.Run Macro:=Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
Application.Run Macro:=Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
' Application.Run Macro:=Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn Arg1:=42
' Application.Run Macro:="'" & ThisWorkbook.Path & "\" & "NeuProAktuelleMakros.xlsm'!FoodsLookUpTable!CallFrmProTypeIn", Arg1:=42
End Sub
Thanks once again
Alan
Summarising for Prosperity: ( Subject to Edits based on any correcting comments. ;) )
It seems that you can call an Excel code routine ( including those in a Worksheets Class code Module ) from another Workbook. You can pass any optional or required arguments to the called routine.
The documentation is not too clear, or does not explain all, or is wrong..
Using the same example, I can ..
_1) do a simple Call
Call Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn 42
_2) There is an Application.Run Method.
_2a) But I suggest that this code line, although it “works” , probably does not get a chance to use it..
Macro:=Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
I say this as I could just as well do this and it “works” also
Dim vTemp As IBlogPictureExtensibility
Let vTemp = Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
I expect in the latter I did not get a chance to extend the ability to Extensivibly Blog a Picture as the evaluation set off by the = did an auto intensively initiation of the code as I accidently exposed it at the VB Component ( Tabelle11 ) interface, which pseudo made the Object in a pseudo late binding type stylio
This works also
If Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn(42) Then MsgBox prompt:="It wasn't, but it did, so then perhaps was, was but too late - and then .... like did an auto intensively initiation of the routine as I accidently exposed it at the VB Component ( Tabelle11 ) interface, which pseudo made the Object in a pseudo late binding type stylio. It was not Late, it came too late"
Maybe we could refer to the above as a “pseudo” Run Method, way to Run, or Run way
_2b) Aplication.Run “StringReferrenceToMacroName”, MacroArgument1, MacroArgumet2, .....
I think this is using the Application.Run Method correctly; I believe this is wired to take a string reference to the string name of a macro, and also any arguments therefor. The basic syntax therefore would be
Application.Run Macro:="'NeuProAktuelleMakros.xlsm'!Tabelle11.FrmProTypeIn", Arg1:=42
It is important here not to confuse, as I did, a code bit like Tabelle11.FrmProTypeIn with the last string part in that last string, which is the macro name. This macro Name can be seen, for by example, by hitting Alt+F8 to list the literal names of macros
http://imgur.com/KL9pwFq
An interesting advantage of using the Application.Run Method is that the string reference may be extended to include the full path to the Workbook. This has no effect if the Workbook is open. If however, the Workbook is closed, then the workbook is opened by a code line of this form:
Application.Run Macro:="'H:\ALERMK2014Marz2016\NeueBlancoAb27.01.2014\AbJan2016\OutlineGrouping\RoryAppRun\NeuProAktuelleMakros.xlsm'!Tabelle11.FrmProTypeIn", Arg1:=42
_.....
A few last notes:
_A) These two codes are in fact doing the same thing:
Workbooks("NeuProAktuelleMakros.xlsm").Application.Run Macro:=Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
Application.Run Macro:=Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
They only work if Workbooks("NeuProAktuelleMakros.xlsm") is Active. This suggests that Application.Run with unqualified Workbook will go to the Active Workbook. I think that this is one of those occasions showing us that VBA is not really a Object Orientated Programming language. The Application can be called at various levels , but effectively goes “back up” the Hierarchy, so the use of Workbooks("NeuProAktuelleMakros.xlsm") has no effect.
Similarly , if “ProAktuellex8600x2.xlsm” is my workbook with the code in it, ( or any other open workbook for that matter ), then this will still work
Workbooks("ProAktuellex8600x2.xlsm").Application.Run Macro:=Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
The important part is the
Workbooks("NeuProAktuelleMakros.xlsm").Worksheets("FoodsLookUpTable").FrmProTypeIn(42)
_ B) use of Run appears to default to Application.Run
_C ) We do not appear to have a Workbooks or Worksheets Run in such a way as we have , for example, a Worksheets Evaluate in addition to an Application Evaluate. This explains why ...Workbooks("NeuProAktuelleMakros.xlsm").Application.Run Macro:=Worksheets("FoodsLookUpTable").FrmProTypeIn(42) ... is “ignoring” the Workbooks("NeuProAktuelleMakros.xlsm")

I have failed endlessly trying to write the code in VBA to insert this formula into a cell

The formula:
=IFERROR(IF(OR(E10=0,D9=0),0,NETWORKDAYS(D9,E9))," ")
An example of what I've tried in VBA:
Sub inputWorkdays()
Range("h9").Formula = "=IFERROR(IF(OR(E9=0,D9=0),0,NETWORKDAYS(D9,E9)),""Yes"")"
End Sub
I'm trying to add the formula from above into cell H9.
Select the cell with the formula and write the following:
Sub TestMe
debug.print Selection.Formula
debug.print Selection.FormulaR1C1
End sub
In your case it would give:
=IFERROR(IF(OR(E10=0,D9=0),0,NETWORKDAYS(D9,E9)),"YES")
=IFERROR(IF(OR(R[-4]C[-1]=0,R[-5]C[-2]=0),0,NETWORKDAYS(R[-5]C[-2],R[-5]C[-1])),"YES")
Take the first one and use it like this:
Range("h9").Formula = "=IFERROR(IF(OR(E10=0,D9=0),0,NETWORKDAYS(D9,E9)),""YES"")"
I gather from the comments that there is no error, just "nothing happens". I see nothing wrong with your code. Except...
Range("h9").Formula = "..."
When Range is unqualified like this, you implicitly refer to the ActiveSheet; if the active sheet isn't the sheet you're expecting to write to, then it's easy to conclude that "nothing happens" and that the code doesn't work.
If you have Rubberduck installed (full disclosure: I'm heavily involved with the development of this open-source VBE add-in), you will see that Range in this case is a member of Excel._Global, and an inspection result will tell you that you're implicitly referring to the ActiveSheet:
Range("H9").Formula = "..."
Implicit references to the active sheet make the code frail and harder to debug. Consider making these references explicit when they're intended, and prefer working off object references.
http://rubberduckvba.com/Inspections/Details/ImplicitActiveSheetReferenceInspection
To fix this, qualify the Range call with a Worksheet object - now the Range call is a member of the Excel.Worksheet class:
Dim sheet As Worksheet
Set sheet = ThisWorkbook.Worksheets("Sheet1")
sheet.Range("H9") = "..."
By qualifying Range calls with a worksheet object, you make sure that you're always writing to the worksheet you mean to write to - not the worksheet that happens to be the active one when the code runs.

VBA Global variables no longer declared after deleting worksheet

I have some public worksheet variables that are first initialized when the workbook is open. I have a button that does this essentially:
Dim Response As Variant
Response = MsgBox("Are you sure you want to delete this worksheet?", vbYesNo + vbExclamation, "Confirm Action")
If Response = vbNo Then
GoTo exit sub
End If
'Save workbook prior to deletion as a precaution
ThisWorkbook.Save
ActiveSheet.Delete
For some reason after this runs, those worksheet variables are no longer declared and I have to reinitialize them every time. I tried adding my InitVariables macro call after the .Delete and it still doesn't work.
Any reason why this might be happening?
The reason is actually really simple - a Worksheet is a class in VBA, and its code module gets compiled along with the rest of your project even if it's empty. When you delete a worksheet and let code execution stop, the next time you run some code the VBE has to recompile the project because you removed a code module. That causes your custom class extensions to lose their state.
Note that this does not happen unless the code stops running and is recompiled. This works just fine:
Sheet1.foo = 42 'foo is a public variable in Sheet1
Sheet2.Delete
Debug.Print Sheet1.foo 'Prints 42
I just tested it using Comintern foo. It's interesting that the standard module foo losses it value but the public foo variable in a worksheet module does not loses it's value.

VB, excel macro pause and resume working if possible

I cannot figure out the best way to do the following problem. Basically my macro (excel, VB) is checking several (100+) worksheets for correct values, if wrong value is found, I want it to stop, give me a warning, then I could have a look into the reason why the value is wrong, correct it manually and then I want to resume the macro, or remember the last value checked so if I return, it remembers where to continue (resume).
My current problem is that it finds the wrong value, then I can either make it stop so I check the problem, or it goes through all the sheets and then I have to remember which sheets had the wrong value.
What I thought of is make a list where the name of sheet is added every time a wrong value is found. The problem is that usually there is more than 1 wrong value in the same sheet if there is a wrong value at all and this added the same sheet name several times to the list. Another problem with that is that I cannot correct the values straight away.
I'm very inexperienced with programming and so would appreciate your idea on how to best approach this problem (I don't want to spend a long time on coding something which wouldn't be efficient for such a "simple" problem).
When the error is found (I'm assuming you've already been able to identify this), you can use the Application.InputBox function to prompt you for a new value.
For example, if rng is a Range variable that represents the cell being checked, and you have some logic to determine where the error happens, then you can just do:
rng.Value = Application.InputBox("Please update the value in " & rng.Address, "Error!", rng.Value)
The inputbox function effectively halts execution of the procedure, while waiting for input from the user.
If InputBox isn't robust enough, then you can create a custom UserForm to do the same sort of thing. But for modifying single range values, one at a time, the InputBox is probably the easiest to implement.
I believe you can handle this task by using one or two static local variables in your macro. A variable declared with "static" rather than "dim" will remember its value from the last time that procedure was run. This can hold where you left off so you can resume from there.
One thing that could be a problem with this solution would be if the macro gets recompiled. That would probably cause VBA to clear the value that the static variable was holding. Just doing a data edit in Excel should not cause a recompile, but you will want to watch for this case, just to make sure it doesn't come up. It almost certainly will if you edit any code between executions.
Create a public variable that stores the cell address of the last checked cell and use a conditional statement to see if it's "mid-macro" for want of a better phrase. here is a very crude example...
Public lastCellChecked As String
Sub Check_Someting()
Dim cell As Excel.Range
Dim WS As Excel.Worksheet
If Not lastCellChecked = vbNullString Then Set cell = Evaluate(lastCellChecked)
'// Rest of code...
'// Some loop here I'm assuming...
lastCellChecked = "'" & WS.Name & "'!" & cell.Address
If cell.Value > 10 Then Exit Sub '// Lets assume this is classed as an error
'// Rest of loop here...
lastCellChecked = vbNullString
End Sub
The best way to do this is to create a userform and as mentioned by prior users create a public variable. When the program finds an error store the cell and initiate the userform. Your code will stop on the userform. When you're done checking the problem have a button on the userform that you can click to continue checking. Your loop can be something like the below.
public y as integer
sub temp1 ()
rw1= range("a500000").end(xlup).row 'any method to create a range will do
if y = null then y=1
for x = y to rw1
cells(x,1).select
'check for the problem your looking for
if errorX=true then
userform1.show
y = activecell.row
exit sub
end if
next x
end sub
What about inserting a button (on the sheet or in a menubar) for stopping?
Insert the code below:
'This at the top of the module
Public mStop As Boolean
'This in the module
Sub MyBreak()
mStop = True
End Sub
'This is your macro
Sub YourMacro()
'This at the top of your code
mStop = False
'Your code
'...
'This code where you want to break
DoEvents '<<<< This makes possible the stop
If mStop Then
mCont = MsgBox("Do you want to continue?", vbYesNo)
If mCont = vbNo Then
Exit Sub
Else
mStop = False
End If
End If
'Your code
'...
End Sub
Now you need to create a button and link it to the macro called "MyBreak".