I have a question about naming conventions and using the same variable name in different subs. It's been bugging me since I started with VBA.
Is it good practice to use the same variable name in different modules? The subs don't interact with each other. For example, I cycle through the sheets in my workbook in two different modules and so far have used the same name for the counter variable (count_ws):
Module 1:
Sub Test()
Dim count_ws As Long
For count_ws = 2 To ThisWorkbook.Worksheets.Count
Debug.Print "Module1"
Next count_ws
End Sub
Module 2:
Sub Test2()
Dim count_ws As Long
For count_ws = 2 To ThisWorkbook.Worksheets.Count
Debug.Print "Module2"
Next count_ws
End Sub
If this is not ok, what's the best alternative? The reason I repeat the name is that I didn't want to make the variable name too long, like count_ws_module1 and count_ws_module2
Passing a variable into another sub: Same question, is it advisable to keep the same name? I feel like it could be confusing if I call the variable one name in the first sub, and then something else in the other sub.
Sub Test3()
Dim wsLoans As Worksheet
Dim wsBS As Worksheet
Set wsLoans = ThisWorkbook.Sheets(2)
Set wsBS = ThisWorkbook.Sheets(3)
Call Test4(wsLoans)
End Sub
Sub Test4(ByVal wsLoans As Worksheet)
wsLoans.Range("A1").Value = "Module 4"
End Sub
So, for me this approach seems the most readable and avoids confusion, but I'm happy to hear other opinions. In Sub Test4 I could simply name the Sheet ws. Or wsLoans_Test4, but does this actually help?
I just want to make sure I get this right and build good habits.
This is a question that can lead to endless discussions, and the more you think about the whole subject of naming conventions, the deeper it goes. And as this is strongly opinion bases, those kind of questions are usually closed on Stack Overflow.
I will quickly list 3 aspects:
(1) A subroutine (or function) can be seen as an closed object, often seen as a black box. It should do a defined task, however, how this is done shouldn't matter. It could be stored in a different module and could be written by a different person. You shouldn't have to ask someone "Have you already used the variable name count_ws - if not, I want to reserve it for me. Every routine should use whatever name it likes.
(2) You as a programmer should have some naming conventions. They don't need to be written down, but you should have a specific consistency. Do you name a sheet variable wsData or dataWs, do you use camelCase, PascalCase or snake_case, use Hungarion Notation or not... As a consequence, you will probably name variables identically in different routines when they serve a identical or similar purpose - and why not. Again, you shouldn't have to look into your code if you used the same name already. Exception is if you are dealing within the same routine, don't use the same variable for different purposes, and be careful when naming iteration variables in nested loops.
(3) Function parameter names serves as a documentation. The parameters are the interface between two routines and if you give them good names, it gets easier to figure out what's the purpose of it. If you want to call a routine Copy that receives 2 parameters which are named p1 and p2, you first have to figure out what is source and what is destination, while pFrom and pTo makes it obvious. That said, if you are happy with your naming, there is no reason not to name a variable of a calling routine like the parameter name of a subroutine.
Related
folks. I'm a programming newbie, trying to write a macro to extract some rows of data from multiple workbooks and compile them into a new workbook, then graph them. I have figured out how to loop through the source spreadsheets, and a few other things, but right now I'm stuck on a couple of places. (I'll ask my different questions in different threads for clarity.)
This question is about an error when using WorksheetFunction.CountA to get the number of a list of items I'm searching for. I want to know the number so that I can know when I have found all of my search strings. If I can't find them all, I want to inform the user and quit.
Following advice on this question, I wrote a snippet to test the CountA function. I get an "object required" error. I have reviewed several threads on that topic, but I'm still not getting it. Here is my code snippet:
Sub a_test_kpi_count()
Dim kpi_list_count As Integer
Set kpi_list_count = Application.WorksheetFunction.CountA("kpi_list")
MsgBox "There are " & kpi_list_count & "kpis in the list."
End Sub
When I run the code, the editor stops with either "kpi_list_count" OR "CountA" highlighted. Well, THAT'S helpful!
I should add that "kpi_list" is a named range on the worksheet where the macro lives. However, I get the same error when I specify the range in this manner:
Set kpi_list_count = Application.WorksheetFunction.CountA("K3:K7")
Ergo, I don't think the named range is my problem.
A nudge in the right direction would be much appreciated!
OR, feel free to call me an idiot and point out my obvious error! ;-)
EDIT:
Thanks, Ben.
This code is working:
Sub a_test_KPI_count()
Dim KPIListCount As Long
KPIListCount = WorksheetFunction.CountA(Sheet1.Range("KPI_list"))
MsgBox "There are " & KPIListCount & "KPIs in the list."
End Sub
Thank you! Oddly, it does NOT work when I use the sheet name, buttons. Are sheet names case sensitive? I ask because the editor insists on "Buttons" rather than, "buttons".
A couple more questions for my learning, if I may, about some of your tips:
3, long vs. integer. In this case, I'm counting a short list (<6) items. Is integer acceptable here, or would it be best practice to stick with Long?
Camelcase vs snakecase. Noted. Is this simply a preference of more experienced programmers? Or is there a functional difference?
Your answers have all been of the kind I hope for. A fishing lesson, rather than a fish! Thank you!
Set is for Object variables - remove it.
You need a Range call when specifying a named range.
kpi_list_count = Application.WorksheetFunction.CountA(Range("kpi_list"))
99.9999% of the time you want a Long, not an Integer; see this question:
Dim kpi_list_count as Long
Best practice is to specify which sheet the Range is on (change as necessary):
kpi_list_count = Application.WorksheetFunction.CountA(Sheet1.Range("kpi_list"))
You can drop the Application if you want.
Consider using camelCase instead of snake_case.
Would like to define reference variables (calls a value from a cell in the sheet using ActiveSheet.Cells[row, col]) in one location in a module, for use across multiple subs in an MS Excel file. The file is an action tracker, the subs automate some of the emailing (each subs opens emails under given conditions). All reference variables are the same for each sub - defining in one place will make maintaining the spreadsheet much simpler.
Tried to define variables above first sub, error message appears on first value (as detailed below). I've searched (a) Global Variables and (b) how to define above the subs. However (a) variables all in the same module (b) error message as detailed below. I haven't located a guide on defining variables using ActiveSheet.Cells() references.
Option Explicit
'Defines variables for all macros:
'Defining Reference Variables
Today = ActiveSheet.Cells(2, 4)
ActionLogTitle = ActiveSheet.Cells(3, 3)
IPT_Leader = ActiveSheet.Cells(7, 7)
(On Today = ActiveSheet.Cells(2,4) error highlights on "2")
Compile error:
Invalid outside procedure
As the compiler is hinting, you cannot write assignments outside of a Sub/Function.
You can declare a function for each variable:
Function MyValue()
MyValue = ActiveSheet.Cells(2, 4).Value
End Function
Ideally you don't use ActiveSheet unless that's really what you want though.
There are a lot of ways to define today, but using a word, which is used by Excel English formula =TODAY(), is probably a discussable idea (although it will work!). In general, consider declaring the variable like this somewhere in the modules:
Public myToday as Date
Then, you may reset it everytime the worksheet is openned:
Private Sub Workbook_Open()
myToday = Date
'or
myToday = Worksheets("Name").Range("D2").Value
End Sub
Anyway, working with Public variables is in general discouraged in any programming language, thus it is probably a better idea to come up with a dedicated function or class for it.
I have been developing a VBA, UserForm1 program for the past few weeks and I have performed numerous test and never once had this problem.
The code, shown below, is part of the Initialization routine. I made some enhancements to the program, far deeper in the code than the code under discussion. Now, I can't get past this point in the program. I replaced "Activate" with "Select", but this did not affect the outcome, i.e., an aborted run.
Please, can someone suggest what I am doing wrong? Or, how can a program that been tested dozens of times, suddenly develop a fault out of thin air?
'======================================================================
Dim WPA() As Variant 'Workbook path for Category Class
Dim WBA() As Variant 'Workbook name for Category Class
Dim WSA() As Variant 'Worksheet name for Category Class
'======================================================================
Dim WBK() As Workbook
..........
Set WBK(S) = Workbooks.Open(WPA(S))
Workbooks(WBA(S)).Worksheets(WSA(S)).Activate 'Activate Worksheet
NWS(S) = CheckSheetExists(WSA(S)) 'Check for Worksheet
..........
Function CheckSheetExists(SheetName) As Boolean
CheckSheetExists = Evaluate("ISREF('" & SheetName & "'!A1)")
If CheckSheetExists = False Then
MsgBox "Worksheet " & SheetName & " does not exist. Run aborted."
End '<==================Run aborts
End If
End Function
It looks like you are using global variables?
If so, you should be aware that they are not as permanent as you might expect.
There are a number of things which cause global variables to reset and become undefined.
See this answer for more info: https://stackoverflow.com/a/7043901/1473412
Using "End"
An unhandled runtime error
Editing code
Closing the workbook containing the VB project
Is it possible that the "enhancements to the program, far deeper in the code than the code under discussion", causes one of these things to happen, and so resets the global variables?
If you have to use global variables, you could store them safely in a worksheet. Or, everytime you try and use one, you could check that it is defined, and if not, redefine it?
That is a good question, Storaxs' answer should have fixed this though to my knowledge.
However when something like this happens I try to create a whole new workbook and paste all of this code into it (just for the sheet in question) and then copy all the cells data into the new sheets.
This has saved me once before and it may help you in this case as well.
I also recommend not deleting your old workbook in case you don't port everything over.
If this still doesn't work let us know and we will continue to look into this.
I have a userform that is supposed to insert a row on Sheet3 and populate some cells in that row with some values. it works great as long as I have sheet3 displayed. (The form is shown modeless to give me access to the sheets).
Anyway, I happened to have another sheet active and ran it again and was surprised to see it inserted the row not in sheet3, but in the one I had displayed... (Thank God I saved first!)
In the code, I specified a range object as follows to find the insertion point: (I'll truncate the code a bit to keep it simple)
Dim RecordRange As Range
Set RecordRange = Sheet3.Cells(RowVariable,ColumnVariable)
RecordRange.Offset(1,0).EntireRow.Insert
blah blah blah.
A workaround is to activate the sheet first:
Sheet3.Activate
That at least inserts into the correct sheet, but I'd rather not have sheet3 be displayed when I add the record, so I even surrounded that line with:
Application.ScreenUpdating = False
Sheet3.Activate
Application.ScreenUpdating = True
unfortunately, ScreenUpdating doesn't work from within a Userform Code module, so that bites...
I still don't understand why it chooses to insert the row into whatever sheet is active, when I've already specified sheet3 in the code. I have another Macro very similar that doesn't have this problem.
any ideas?
...so you figure your workaround needs a workaround? Hmm... I think that qualifies as a double-negative since a workaround is supposed to, by definition, fix problems rather than cause them...
A workaround is a bypass of a recognized problem in a system. A
workaround is typically a temporary fix that implies that a genuine
solution to the problem is needed. But workarounds are frequently as
creative as true solutions, involving outside the box thinking in
their creation.
Typically they are considered brittle in that they will not respond
well to further pressure from a system beyond the original design. In
implementing a workaround it is important to flag the change so as to
later implement a proper solution.
Placing pressure on a workaround may result in later system failures.
For example, in computer programming workarounds are often used to
address a problem or anti-pattern in a library, such as an incorrect
return value. When the library is changed, the workaround may break
the overall program functionality, effectively becoming an
anti-pattern, since it may expect the older, wrong behaviour from the
library. (Wikipedia)
Just sayin'... :-)
So instead of fixing the workaround, your original issue is (thankfully) straightforward.
You're using Sheet3 as an object, and I suspect you haven't assigned anything (like Worksheets("Sheet3") to an object called Sheet3.
Try this instead:
Dim RecordRange As Range
Set RecordRange = Worksheets("Sheet3").Cells(RowVariable,ColumnVariable)
RecordRange.Offset(1,0).EntireRow.Insert
If you indeed intended to use Sheet3 as an object then make sure it's declared as set, so for example you could instead use:
Dim RecordRange As Range
Dim Sheet3 As Worksheet
Set Sheet3 = Worksheets("Sheet3")
Set RecordRange = Sheet3.Cells(RowVariable,ColumnVariable)
RecordRange.Offset(1, 0).EntireRow.Insert
Also, just to point out: If either of these solves your issue then the problem would've "made itself known", by including one line at the top of every module (at least during development & troubleshooting):
Option Explicit
This link has a short explanation, but basically it forces you to properly declare all variables (thus helping to prevent future workarounds!)
My code is:
Sub zaa()
'code to find another open file that has the defined name "data"
' - useful where the name changes each month or week
For Each wb In Workbooks
On Error GoTo abcd
x = wb.Name
Workbooks(x).Activate
If Range("Data").Address <> "" Then y = wb.Name
Exit Sub
abcd:
Next wb
End Sub
Basic goal to find an Excel file with a specific named range when I know it exists but don't know the file name as it changes each week or month. Aim is to find the file and exit the sub at that point (or then do other code on that file and exit rather than going to other files.)
I find it works okay if I only have two files open but not if have more (unless the target one is second in line). Whilst I can run with what I have I thought others may benefit from what I have and I can have a more robust solution.
UPDATE:
Thanks to all those who have responded & to Mitch for putting original post in readable format! (I have since learnt how to correct that issue and to be able to copy code directly - which I have indicated in some comments below that I was having trouble with.)
I have had varying degrees of success - code from PaulStock & Reafidy both worked for me originally but PaulStock code has stopped working for me. Responses & code from Jean-François Corbett and Chris Neilsen have been helpful to me but presently when I run the code (as is) it appears to do nothing - ie doesn't leave the sheet I run it from (and that's not where the range name data appears). I was originally running the code in Excel 2007 in a sheet with other macros but to try and get different results have moved them to a stand alone sheet with no other macro files open. Have also tried running them from a file saved as '97-'03 format. Neither yielded different results. Others with more experience than I (see the errors I made evidenced in comments discussion with Reafidy & bear in mind that my original posted code was result of material found through google and modified by me for specific task & application and implication that I am not savvy enough to have come up with it on my own) may find other solutions better for them but for now:
I am more than happy as Reafidy's code works for me.
As I am not registered (I did try but it didn't work) and don't have enough reputation points I can not vote but I have put a check mark next to Reafidy's solution.
FURTHER UPDATE:
I have now discovered that PaulStock & Jean-François Corbett's code wasn't working for me due to a very minor discrepancy on my part - the code contained "Data" whilst my named range was "data". Making this change (to allow for case sensitivity) in their code means that both of their solutions now work for me and hence I have attempted to add a tick to their solutions! Unfortunately I have found that only one solution can have a tick.
I have also learnt that I took Chris' code too literally. In an effort to test each code 'as is' that is what I did. A simple additon of 'wb.activate' in the section where he has 'do stuff' makes the code do what I want.
THANKS again for all four contributions.
You're doing a few convoluted things in your code, and I think that's what's confusing the issue.
Having you error handler (abcd:) start in the middle of a For...Next loop is incredibly bad practice and can only lead to confusion. I can't think of a reason why this should ever be done.
Anyhow, there's no need to use error handling for this task. I understand that there are exceptional cases where bad VBA design forces us to use error handling instead of what should be built-in VBA functionality (e.g. testing whether a non-Variant array has been allocated), but this is not one of those cases. There's a standard way to do it (.Names collection of workbook object), and using error handling instead of this is messy and convoluted and asking for trouble.
Also, why say
x = wb.Name
Workbooks(x).Activate
when you can just say wb.Activate? You're not using x for anything. Or y, for that matter.
The following works and is both simplified and optimised, relative to your original code as well as to the other answers that have been given up to now:
Sub zaa2()
Dim wb As Workbook
Dim nm As Name
For Each wb In Workbooks
For Each nm In wb.Names
If nm.Name = "Data" Then
wb.Activate
Exit Sub
End If
Next
Next wb
End Sub
' A workbook containing a range named "Data" is now activated
' (if one is found amongst the open workbooks).
' Note that there may be more than one, but only the first found is activated...
EDIT: In your comment, you mention you had trouble due to confusion between uppercase "Data" and lowercase "data". To guard against this in the future, one possibility is to ignore case. This could be done as follows:
If StrComp(nm.Name, "data", vbTextCompare) = 0 Then
or
If LCase(nm.Name) = "data" Then
Both will return True if nm.Name is "Data", "data", "dATa", etc.
You can't use error handling like that. Either move the error handling out of the loop or reset the error handler each time it occurs, so use error handling like this:
A much preferred alternative would be:
Sub Test()
For Each wb In Workbooks
x = wb.Name
Workbooks(x).Activate
If RangeExists("Data") Then
y = wb.Name
Exit Sub
End If
Next wb
End Sub
Function RangeExists(s As String) As Boolean
On Error Resume Next
RangeExists = Range(s).Count > 0
End Function
EDIT:
#Jean-François Corbett, I have to say you a very quick to jump on the down vote button. The first solution I posted was because I made the assumption that the OP was not posting his entire code hence why I did not attempt to simplify it or "clean it up" like I usually do. I agree I did not word my answer well, but with regard to the first solution I was trying to demonstrate that he needed to reset the error handler. Unfortunately I should have said a "prefered alternative would be".
#Derek, Sorry I was unable to answer your further questions in time. Obviously you are free to choose whatever method you like. In my opinion the multiple loop solution provided by others which digs into the workbook name collection is unnecessary and long winded. Now more importantly the name collection can contain names which refer to a constant, formula, or a range. I presume you want to only check if the defined name is specifically a named range which means the looping method provided by others would need to be adjusted to be reliable.
I agree with comments made by others that error handling should be avoided BUT unnecessary looping in excel can be as much of an evil as using error handling and personally I avoid it like the plague.
The function above can be placed in its own module and be reused as much as you like. It is quick, reliable, avoids unnecessary looping, checks SPECIFICALLY for a named range in a workbook and is the most widely accepted/used method for checking if a named range exists within the excel vba community (by this I mean using a function and error handling over looping through the name collection). Do a google search for "Check If Named Range Exists" if you don't believe me. Or ask at www.ozgrid.com/forum if you want other excel vba experts opinion's.
Now that I know you have posted your entire code and that you did not intend to activate every workbook, you could use this code which will activate the first workbook found with the named range "data":
Sub Test3()
Dim wbLoop As Workbook
For Each wbLoop In Workbooks
If RangeExists("data", wbLoop) Then
wbLoop.Activate
Exit Sub
End If
Next wbLoop
End Sub
Function RangeExists(s As String, wb As Workbook) As Boolean
On Error Resume Next
RangeExists = wb.Names(s).RefersToRange.Count > 0
End Function
I completely understand the need for positive Criticism and I believe in the down vote system if it is used correctly. However, with two down votes for what I believe was a reasonable solution and along with my help with the ops formatting issues - unfortunately I cant help but feel like I want to distance myself from this forum.
Heres an alternative method without getting fancy with the error handler
Sub zaa()
Dim wb As Workbook
Dim CheckForNamedRange As Boolean
Dim nm As Name
On Error GoTo EH
For Each wb In Workbooks
CheckForNamedRange = True
Set nm = wb.Names("data")
If CheckForNamedRange Then
' Name found
' do stuff
Exit For
End If
Next
Exit Sub
EH:
If CheckForNamedRange Then
' Name not found
Err.Clear
CheckForNamedRange = False
Resume Next
Else
' Some other error occured, so handle it
'...
End If
End Sub
Try this code. Shouldn't have to worry about getting errors.
Sub zaa()
For Each wb In Workbooks
x = wb.Name
Workbooks(x).Activate
For Each n In Workbooks(x).Names
If n.Name = "Data" Then
y = wb.Name
Exit Sub
End If
Next
Next wb
End Sub