I am using Option Explicit in all modules, this one has me scratching my head, perhaps it is in the call method that things get lost.
Reading worksheets and combining particular data into a new workbook, single worksheet, the variable colData should increment the column and it will be updated and passed back in as many times as there are worksheets (ex. copy months 1-12 for a given year in cols 1-12, then the next year copy months 1-12 in cols 13-24, etc.)
Call to Function returns a Boolean (this is an error check directly afterwards):
'Attempt to load Total Revenue for the Import Sheet
TotRevLoaded = Application.Run("modGetDataHelpers.loadTotRev", wsImport, rng, colData)
Here are the parts of the Function that matter:
'Sub to load total revenue from selected file across the worksheets
Private Function loadTotRev(ByRef wsImport As Worksheet, ByRef rng As Range, ByRef colData As Long) As Boolean
'matchRow is called above and works
Dim k As Integer
For k = 1 To 12
'sets headers for the sheet
wsData.Cells(1, colData) = strYear
wsData.Cells(2, colData) = k
'Copy Total Revenue in to row 3
wsData.Cells(3, colData) = wsImport.Cells(matchRow, 2 + k).Value
colData = colData + 1
Next k
loadTotRev = True 'Success in loading the total rev on sheet
End Function
colData updates in the function as one would expect 1-13, but when leaving the function and coming back for the next worksheet import it is always 1, so the value of colData is not changing at its reference address.
This one should be easy. The variable is being converted to ByVal and once you leave the scope of the function the modifications are lost.
I thought this might be because of the arg wrap in (), Nope. Change to a sub and without assignment you can drop these.
I thought it might be due to the assignment =, Nope. Changed to Sub.
Because it is getting passed to a function, Nope. Changed to a Sub Public and Private.
Because the caller and the modifier are in different modules, Nope
Working on the solution, I don't want a global variable or to bring all helper functions into the same module scope (module global variable).
Great feedback from experienced VBA users, leading to good information.
1) #TimWilliams: Avoid the use of Application.Run to call Private Functions/Subs if you intend to pass parameters ByRef, it will caste these as ByVal.
http://www.tushar-mehta.com/publish_train/xl_vba_cases/1022_ByRef_Argument_with_the_Application_Run_method.shtml
2) #MathieuGuindon: Avoid the use of Application.Run altogether, making the Functions/Subs Public, or investigate the implementation of a class to encapsulate and protect, if truly required look at Option Private Module
I did in fact hit upon the make Public solution through trial and error, thereby dropping the Application.Run call, but the reasoning came later.
Related
First, I would like to apologize for my bad language, I hope you'll understand my problem.
I looked after a way to get generic function in Excel and I found the add-in method. So I tried to use it in developping custom functions whitch may help me in my everyday work. I developed a first function which work. So I thought that my add-in programmation and installation was good. But when I try to implement worksheet interractions nothing appened.
My code has to delete rows identified by a special code in a cell of those ones. I get no error message and the code seems to be totally executed. I tried other methods like Cells.delete, Cells.select, worksheet.activate or range.delete but I encounter the same issue.
This is my function's code :
Public Function NotBin1Cleaning(rSCell As Range) As Integer
Dim sht As Worksheet
Dim aLine As New ArrayList
Dim iLine As Integer
Dim iCpt As Integer
Dim iFail As Integer
Dim i As Integer
Dim oRange As Object
Set sht = rSCell.Parent
iLine = sht.Cells.Find("*PID*").Row
For Each rCell In Range(sht.Cells(iLine, 1), sht.Cells(sht.Cells(iLine, 1).End(xlDown).Row, 1))
If sht.Cells(rCell.Row, 2) > 1 Then
iLine = rCell.Row
iCpt = iLine + 1
Do Until sht.Cells(iCpt, 2) = 1
If Not sht.Cells(iCpt, 1) = rCell Then Exit Do
iCpt = iCpt + 1
Loop
If sht.Cells(iCpt, 1) = rCell Then
sht.Range(sht.Cells(iLine, 1), sht.Cells(iCpt - 1, sht.Cells(iCpt, 1).End(xlToRight).Column)).Delete xlUp
iFail = iFail + 1
End If
End If
Next
NotBin1Cleaning = iFail
End Function
it's the line:
sht.Range(sht.Cells(iLine, 1), sht.Cells(iCpt - 1, sht.Cells(iCpt, 1).End(xlToRight).Column)).Delete xlUp
which isn't producing any effect.
I would be really thankful for your help.
This issue is described on the Microsoft support site as part of the intentional design
section below, more detail here (emphasis mine)
A user-defined function called by a formula in a worksheet cell cannot change the environment of Microsoft Excel. This means that such
a function cannot do any of the following:
Insert, delete, or format cells on the spreadsheet.
Change another cell's value.
Move, rename, delete, or add sheets to a workbook.
Change any of the environment options, such as calculation mode or screen views.
Add names to a workbook.
Set properties or execute most methods.
The purpose of user-defined functions is to allow the user to create a
custom function that is not included in the functions that ship with
Microsoft Excel. The functions included in Microsoft Excel also cannot
change the environment. Functions can perform a calculation that
returns either a value or text to the cell that they are entered in.
Any environmental changes should be made through the use of a Visual
Basic subroutine.
Essentially, this means that what you're trying to do won't work in such a concise manner. The limitation, as I understand from further reading, is because Excel runs through cell equation/functions several times to determine dependencies. This would lead to your function being called two or more times. If you could delete rows, there is the potential of accidentally deleting more then twice the numbers of rows intended, due to the excess number of runs.
However, an alternative could be to have the function output a unique string result that shouldn't be found anywhere else in your workbook (maybe something like [#]><).
Then you can have a sub, ran manually, which finds all instances of that unique string, and deletes those rows. (Note: if you included any of the typical wildcard symbols in your string, you will have to precede them with a ~ to find them with the .Find method.) You can even set up the sub/macro with a shortcut key. Caution: if you duplicate a shortcut key Excel already uses, it will run the macro instead of the default. If there will be other users using this workbook, they could experience some unexpected results.
If you decide to go this route, I would recommend using this line:
Public Const dummy_str = "[#]><" ' whatever string you decided on.
in your module with your code. It goes outside any functions or subs, so it'll be global, and then you can refer to the const just as you would any other string variable.
When you write:
sht.Range(sht.Cells(iLine, 1),....
This first parameter should be the row number, but you're refering to a Cell instead. You should change sht.Cells(iLine, 1) for iLine.
BUT
Instead of all this, its easier to use the method Row.Delete:
Rows(iLine).EntireRow.Delete
Sub CopyColumnWidths(FileName1, SheetName1, FileName2, SheetName2)
ColumnNumber = 1
Check = WorksheetFunction.CountA(Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber))
Do While Check > 0
ColumnLetter = LastColumnLetter(ColumnNumber)
Workbooks(FileName2).Sheets(SheetName2).Columns(ColumnNumber).ColumnWidth = Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber).ColumnWidth
ColumnNumber = ColumnNumber + 1
Check = WorksheetFunction.CountA(Workbooks(FileName1).Sheets(SheetName1).Range(ColumnLetter & ":" & ColumnLetter))
Loop
End Sub
Ok, so this is my code. I have verified that all the file names and sheet names are present and accounted for in the same instance of Excel. I have checked for misspellings, extra characters, and 'invisible' characters and none are present.
I tried, for troubleshooting purposes, putting in a Workbooks(FileName1).Activate and it wouldn't work either. In different code that particular file does get hidden, but at the time this code is executed that workbook is visible and present.
For the life of me I cannot figure out why this is breaking and could use a hand.
This is running on Excel 2013, 64-bit if it matters.
------- More Info
FileName1 is "Original Datasheet.xlsx"
FileName2 is "Split Datasheet.xlsx"
SheetName1 (and SheetName2) are "1a. Contents"
When I try to activate FileName2, it works. When I try to activate FileName1, it fails. The sheet names don't matter, it doesn't 'see' FileName1, even though it is present and I can select it in the 'Switch Windows' dropdown.
Repeating again for those who didn't read the title the first time: Yes, all files are loaded in the same instance of Excel. All Files Are Present.
I don't have your answer, but runtime error 9 sounds a lot like one of these values isn't what you think it is - and with the code you have it's hard to tell exactly where it's blowing up.
Start with turning this:
Sub CopyColumnWidths(FileName1, SheetName1, FileName2, SheetName2)
into this (assuming the procedure is called from within the same module - if it's called from another module, make it Public Sub):
Private Sub CopyColumnWidths(ByVal FileName1 As String, ByVal SheetName1 As String, ByVal FileName2 As String, ByVal SheetName2 As String)
Changing your signature to use String parameters passed by value shouldn't break your code in any way, but makes things more explicit and therefore improves readability and makes your intent clearer.
Moving on.
ColumnNumber = 1
Where's that coming from? Declare it. Stick Option Explicit at the top of your module, and then declare every variable until your code compiles again (Option Explicit will make VBA refuse to compile code that uses undeclared variables).
Dim ColumnNumber As Long
ColumnNumber = 1
Now that we know ColumnNumber and Check are local variables declared in the same scope (right?), we move on:
Dim Check As Long
Check = WorksheetFunction.CountA(Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber))
This line is doing too many things: we don't know that Workbooks(FileName1) is succeeding, and we don't know that its Sheets(SheetName1) is succeeding either - yet we call its Columns member regardless, assuming blue skies and sunshine.
Don't assume blue skies and sunshine.
Break it down.
Dim sourceBook As Workbook
Set sourceBook = Workbooks(FileName1)
Dim sourceSheet As Worksheet
Set sourceSheet = sourceBook.Worksheets(SheetName1)
Check = WorksheetFunction.CountA(sourceSheet.Columns(ColumnNumber))
If your code runs up to that point, your problem is half-solved - you have the same issue here, and you're fetching the same Workbook and Worksheet objects again - instead, break it down, assign local object variables, and reuse them:
Workbooks(FileName2).Sheets(SheetName2).Columns(ColumnNumber).ColumnWidth = Workbooks(FileName1).Sheets(SheetName1).Columns(ColumnNumber).ColumnWidth
Dim destinationBook As Workbook
Set destinationBook = Workbooks(FileName2)
Dim destinationSheet As Worksheet
Set destinationSheet = destinationBook(SheetName2)
destinationSheet.Columns(ColumnNumber).ColumnWidth = sourceSheet.Columns(ColumnNumber).ColumnWidth
ColumnNumber = ColumnNumber + 1
Check = WorksheetFunction.CountA(sourceSheet.Range(ColumnLetter & ":" & ColumnLetter))
Step through (F8) this code line by line, then you'll know exactly which instruction is blowing up. Cramming everything into as few code lines as possible and chaining 2, 3, 4 member accesses assuming it will "just work" makes it nearly impossible to know that.
Is it possible that previous code that hides the window for FIleName1 but later unhides it is messing things up? I had to do that because Excel kept putting data on the wrong workbook unless I hid it.
This sounds like you have code (elsewhere?) that works off ActiveSheet, or ActiveWorkbook, explicitly or (more likely?) implicitly.
If you find yourself using unqualified Range, Cells, Rows, Columns or Names calls, you're implicitly referring to the active sheet. Replace them with explicitly qualified member calls using the worksheet object you mean to refer to. There's no magic, Excel (VBA actually) can't guess your intentions; it doesn't "keep putting data on the wrong workbook" - it puts data exactly where you tell it to put it. Hiding the workbook is just a terribly bad work-around: you're still not telling it explicitly where you mean to put it.
I have been looking at this too long.
FileName1 was coming across as "Orignial Datasheet.xlsx" not "Original Datasheet.xlsx"
That is why it wasn't connecting. My mistake, sorry for bothering everyone.
I'm having some issues with an insheet function that I am writing in VBA for Excel. What I eventually am trying to achieve is an excel function which is called from within a cell on your worksheet, that outputs a range of data points underneath the cell from which it is called (like the excel function =BDP() of financial data provider Bloomberg). I cannot specify the output range beforehand because I don't know how many data points it is going to output.
The issue seems to be that excel does not allow you to edit cells on a sheet from within a function, apart from the cell from which the function is called.
I have created a simple program to isolate the problem, for the sake of this question.
The following function, when called from within an excel sheet via =test(10), should produce a list of integers from 1 to 10 underneath the cell from which it is called.
Function test(number As Integer)
For i = 1 To number
Application.Caller.Offset(i, 0) = i
Next i
End Function
The code is very simple, yet nothing happens on the worksheet from which this formula is called (except a #Value error sometimes). I have tried several other specifications of the code, like for instance:
Function test(number As Integer)
Dim tempRange As Range
Set tempRange = Worksheets("Sheet1").Range(Application.Caller.Address)
For i = 1 To number
tempRange.Offset(i, 0) = i
Next i
End Function
Strangely enough, in this last piece of code, the command "debug.print tempRange.address" does print out the address from which the function is called.
The problem seems to be updating values on the worksheet from within an insheet function. Could anybody please give some guidance as to whether it is possible to achieve this via a different method?
Thanks a lot, J
User defined functions are only allowed to alter the values of the cells they are entered into, because Excel's calculation method is built on that assumption.
Methods of bypassing this limitation usually involve scary things like caching the results and locations you want to change and then rewriting them in an after calculate event, whilst taking care of any possible circularity or infinite loops.
The simplest solution is to enter a multi-cell array formula into more cells than you will ever need.
But if you really need to do this I would recommend looking at Govert's Excel DNA which has some array resizer function.
Resizing Excel UDF results
Consider:
Public Function test(number As Integer)
Dim i As Long, ary()
ReDim ary(1 To number, 1 To 1)
For i = 1 To number
ary(i, 1) = i
Next i
test = ary
End Function
Select a block of cells (in this case from C1 through C10), and array enter:
=test(10)
Array formulas must be entered with Ctrl + Shift + Enter rather than just the Enter key.
I've got a large Excel spreadsheet. It includes many data tables that various lookup functions are run on. To make version control easier, I'm currently in the process of pulling these data tables out into separate .csv files, so they can be diffed properly. Unfortunately, they contain a few formulae, which obviously won't work properly when I convert the file to a static .csv.
My current solution is to, wherever a calculation is unavoidable, move the calculation to its own cell in the main workbook and name the cell. Let's call it ExampleCalc. Then, in the cell on the data table where the calculation was, I instead enter ref:ExampleCalc. Then to do the lookup, I wrote the following UDF:
Function RaceLookup(lookupString As Variant, lookupTable As Range, raceID As Range, cleanIt As Boolean) As Variant
' Helper function to make the interface formulae neater
Dim temp As Variant
Dim temp2 As String
Dim inpString As Variant
Dim id As Double
id = raceID.Value
If TypeOf lookupString Is Range Then
inpString = lookupString.Value
Else
inpString = lookupString
End If
With Application.WorksheetFunction
temp = .Index(lookupTable, id, .Match(inpString, .Index(lookupTable, 1, 0), 0))
If Left(temp, 4) = "ref:" Then
temp2 = Right(temp, Len(temp) - 4)
temp = Range(temp2).Value
End If
If cleanIt Then
temp = .clean(temp)
End If
End With
RaceLookup = temp
End Function
This does a standard INDEX lookup on the data sheet. If the entry it finds doesn't start with ref:, it just returns it. If it does start with ref:, it strips the ref: and treats whatever's left as a cell name reference. So if the value the INDEX returns is ref:ExampleCalc, then it will return the contents of the ExampleCalc cell.
The problem is that ExampleCalc doesn't get added to the dependency tree. That means that if the value of ExampleCalc changes, the retrieved value doesn't update. Is there any way to make the function add ExampleCalc to the cell's dependency tree? Alternatively, is there a more sensible way to do this?
The solution i have found is to add the line
Application.Volatile True ' this causes excel to know that if the function changes, it should re calculate. You will still need to click "Calculate Now"
into the function. However, as my code comment indicates, if the only change is in the functional output, you'll have to manually trigger a recalculation.
See https://msdn.microsoft.com/en-us/library/office/ff195441.aspx for more information.
I am trying to use the function: Cells(Rows.Count, 14).End(xlUp).Row to determine the last row with data. This and similar formulas appear in many spots in my VBA code. My spreadsheet is quite complex and I often need to add columns. So rather than referring to Column 14, I'd like to use a value I can easily change in only one place. I am using a named Column of sorts that I only need to update in one place. Items sold appears in column N. I realize that this code is very simple, but I simplified it down for the sake of asking a question. When I try to run the Test sub, I a compile errror: ByRef argument type mismatch and the variable Item_Sold. The reason I am using a letter is that Range() needs a letter, but Cells().End(xlUp).Row needs a number.
It works if I move: Items_Sold = "N" to the sub from the function, but this means this code would have to go in every sub rather than in just one function.
Function:
Function ColNum(ColumnNumber As String) As Integer
Dim Items_Sold As String
Items_Sold = "N"
ColNum = Range(Replace("#:#", "#", ColumnNumber)).Column
End Function
Macro:
Sub Test()
Dim Total_Items_Sold As Integer
Total_Items_Sold = Cells(Rows.Count, ColNum(Items_Sold)).End(xlUp).Row
End Sub
Use a Global constant.
At the top of your module, outside of any Sub/Function:
Const COL_NUM As Long = 14
Usage:
Sub Test()
Dim Total_Items_Sold As Long
Total_Items_Sold = ActiveSheet.Cells(Rows.Count, COL_NUM).End(xlUp).Row
End Sub
Add a global variable at the beginning of the code by writing
public Items_Sold as String
that will solve your ByRef problem. But still you will have problem with undeclared value of Items_Sold when invoking Test() unless you set the value of Items_Sold somewhere before.