I have a UDF that can be called from within a cell in my excel workbook. I need it to add a worksheet at the end of the workbook. I have used sheets.add multiple times in my VBA script, but never in a function called from inside a cell and this is apparently causing some issue.
The function accepts an optional parameter for file path of the workbook in which to add the sheet, and if the user leaves this blank I want to default to the active workbook.
Below is the relevant code... What am I doing wrong?
Public Function onesheet(Optional filepath As String)
Dim wb As Workbook
Dim ws As Worksheet
If filepath = "" Then
Set wb = ActiveWorkbook
Set target_ws = wb.Sheets.Add(after:=wb.Sheets(wb.Sheets.Count))
End If
The function is being called from the cell with
=onesheet()
A function (UDF) has one role: compute a value and return that value to the cell (or formula/expression) that called it.
This is a function:
Public Function Foo(ByVal bar As String) As String
Foo = "Hello, " & bar
End Function
You can use it in a worksheet cell like this:
=Foo("dsdavidson")
And every time Excel recalculates that cell's value, it calls the UDF, making the cell's value read Hello, dsdavidson.
Functions don't have side-effects. Functions don't modify other cells. Functions take input, process it, and output a result.
What you're doing wrong, is using a UDF as if it were a macro.
Change your Function for a Sub, and don't call it from within a cell. Make a button to call it instead. Or whatever rocks your boat. But you can't have a cell formula that adds a worksheet to the workbook every time it recalculates.
Macros need to be Public and parameterless. So you'll want to take your optional parameter value from a specific cell, or display a form that lets the user pick from a list of available opened workbooks - and then call your procedure and pass the user's selection as a parameter.
Quite possibly the macro code could end up looking something like this (YMMV):
Public Sub AddWorksheet()
With New PromptForm
.Show
If .Cancelled Then Exit Sub
OneSheet .SelectedBook
End With
End Sub
You cannot add sheets through user defined function.
Here are the limitations of User Defined Functions.
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:
1) Insert, delete, or format cells on the spreadsheet.
2) Change another cell's value.
3) Move, rename, delete, or add sheets to a workbook.
4) Change any of the environment options, such as calculation mode or screen views.
5) Add names to a workbook.
6) Set properties or execute most methods.
For more details visit this site...
https://support.microsoft.com/en-in/help/170787/description-of-limitations-of-custom-functions-in-excel
Related
I have a sheet with a function called in a cell in the sheet1.
When I change to the sheet2, edit something and go back to sheet1, I see the value change (because I use ActiveWorkbook and ActiveSheet). If I do something in sheet1 the value come back.
I can have multiple workbook with the same data too ...
Edit: I forgot to specify the name of workbooks and sheets are not static, all is dynamic.
Edit 2: I think Excel do a refresh of all sheet when editing a sheet and VBA code is execute, but the activesheet is not the Sheet1 where the data is ... So, VBA code run in the wrong sheet.
Edit 3: The sheet have "Calculation Options" to "Automatic" and I have a button in the bottom of my Excel page "Calculate" to force refresh of all formulas and VBA code.
Excel cell content:
=IF(BD66;MainFunction(BJ66);"")
Main Function:
Function MainFunction(var)
MainFunction = (var * Test()) / (...)
End Function
Sub Function is use in several functions:
Private Function Test()
Test = ActiveWorkbook.ActiveSheet.Range("BE50")
End Function
How can I do for execute code only on the active sheet and not on all sheet ?
Or what is the best way for do that ?
Thanks for your help.
From what I can see - your Test function causes the problem by always looking at the activesheet rather than the sheet that contains the =IF(BD66;MainFunction(BJ66);"") formula.
To look at this sheet you need to look at the cell that called the function using Application.Caller:
Public Function MainFunction(Target As Range) As Double
MainFunction = Target * Test(Application.Caller)
End Function
Private Function Test(Target As Range) As Double
Test = Target.Parent.Range("BE50")
End Function
I've updated var to Target as that's what I'm used to seeing in worksheet events.
I've find a workaround for my problem ...
I add parameters in my main function for replace all sub function which use "Active..."
My formulas are less simple to build, but it works in the sheet which contains formula ...
Thanks for your helps
I want to create a udf for a formula I have written on excel. The formula is as follows:
=INDEX('Pivot-LH'!$D$5:$D$1650,SMALL(IF(B93='Pivot-LH'!$A$5:'Pivot-LH'!$A$1650,ROW('Pivot-LH'!$A$5:'Pivot-LH'!$A$1650)-ROW('Pivot-LH'!$A$5)+2),1))
Basically the syntax is to look for cell B93 (variable) through some data on Pivot-LH sheet and return the 1st, 2nd and 3rd values.
I want to define a udf for this and tried to do this by recording a macro. It gave the following result which I modified to enter B93 as a variable called newroute. However this always gives the value zero:
Public Function LH(newroute As Range) As Variant
Selection.FormulaArray = "=INDEX(R5C4:R1650C4,SMALL(IF(newroute=R5C1:R1650C1,ROW(R5C1:R1650C1)-ROW(R5C1)+2),1))"
End Function
Why does it not give the same result as the formula?
If you want to call LH from a worksheet formula, your function can only return a value. It cannot update the sheet directly.
See: https://support.microsoft.com/en-us/kb/170787
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.
So you need something like:
Public Function LH(newroute As Range) As Variant
LH = newroute.Parent.Evaluate("=INDEX(R5C4:R1650C4,SMALL(IF(" & _
newroute.Address() & _
"=R5C1:R1650C1,ROW(R5C1:R1650C1)-ROW(R5C1)+2),1))"
End Function
Try this
Public Function LH(newroute As Range) As Variant
Selection.FormulaArray = "=INDEX(R5C4:R1650C4,SMALL(IF(" & newroute.address & "=R5C1:R1650C1,ROW(R5C1:R1650C1)-ROW(R5C1)+2),1))"
End Function
I have defined a function in the Module1 of my workbook that looks like this:
Function Header(r As Range) As String
For i = 1 To r.Row
If r.Offset(-i, -1).Value = "" Then
Header = r.Offset(-i).Value
Exit For
End If
Next
End Function
and I call it with a reference to another workbook's cell like this:
=Header('[OtherWorkbook.xlsx]Sheet1'!C34)
and what I get is #VALUE!... until, I open the other workbook, at which point the value magically appears.
how can I force the spreadsheet to fetch the values from the other workbook, even though it isn't open?
Specifying the full path of the closed Excel workbook should do the trick.
Instead of
=Header('[OtherWorkbook.xlsx]Sheet1'!C34)
Try
=Header('C:\Users\your_username\Desktop\[OtherWorkbook.xlsx]Sheet1'!C34)
Substituting the path of the closed workbook in for the path above.
so the answer is: it can't be done. to get the values, opening the workbook is required.
there is an alternative, supplied by #omegastripes above, though is seems rather complicated:
How can I pick values from an Excel workbook and return them by function on active workbook
I've created a program that creates and populates a custom document property in an Excel 2007 workbook file. However I haven't been able to show the value of this property in a worksheet cell. In Word 2007 you can just select "Insert -> Quick Parts -> Field..." and use the DocProperty field to show the value of the custom field in a document. However I haven't found a similar function in Excel 2007.
Does anybody know how to display the value of a custom document property in an Excel worksheet cell? I would prefer a solution similar to the Word 2007 solution mentioned above. I rather not use a macro/custom code for this.
Unfortunately I believe you need to use an user defined function. Add a new VBA module to your workbook and add this function:
Function DocumentProperty(Property As String)
Application.Volatile
On Error GoTo NoDocumentPropertyDefined
DocumentProperty = ActiveWorkbook.BuiltinDocumentProperties(Property)
Exit Function
NoDocumentPropertyDefined:
DocumentProperty = CVErr(xlErrValue)
End Function
The call to Application.Volatile forces the cell to be updated on each recalculation ensuring that it will pick up changes in the document properties.
The equivalent in Excel would be via formula and I don't think it's possible to extract a document property without code. There are no native functions to pick out document properties. (An alternative could be to store information in workbook/worksheet Names, which ARE accessible via formula)
In VBA you'd have to create a function something like:
Public Function CustomProperty(ByVal prop As String)
CustomProperty = ActiveWorkbook.CustomDocumentProperties(prop)
End Function
and then call it in a formula with =CustomProperties("PropertyName").
There is another subtle point. Formula dependencies only relate to other cells; this formula depends on a custom property. If you update the custom property a pre-existing formula involving CustomProperty will not be updated automatically. The cell will have to be re-evaluated manually or the entire workbook forced through a recalc. Your best chance would be to make the function volatile, which means the formula would be recalc'd on every cell change -- but this still means you only get an update if a cell has been changed.
Select the cell you want to extract
Rename the cell to some useful. From "B1" to "Project_Number".
Open "Advance Properties" click the "Custom" tab. Enter a name for the new property. click "Link to content" the select the cell name from the "Value" pull down list.
I wish i could take cerdit but I found the answer online:
http://pdmadmin.com/2012/03/displaying-custom-property-values-in-excel-using-a-named-range/
You can link a named range to a custom property, but then the custom property reflects the value of the [first cell in the] range. It's effectively read-only; you can change the content of the cell to update the property, but not the other way around.
I know you want to avoid it, but if you want to use the property value in a formula, you'll have to create a custom worksheet function to do so.
I have experienced the same issues other people have. So I will try to comprehensively cover how I addressed it.
First of all, you have no other option than writing a function meant to get whatever you put in a custom or built-in property and make the "problem" cell to point at it this way:
=yourPropertyGettingFunctionName(PropertyName)
PropertyName being a string referring to the name of the custom/built-in property whose value you want to be shown in the cell.
The function could be written (as formerly suggested) as:
Public Function StdProp(ByVal sPropName As String) As String
Application.Volatile
StdProp = ActiveWorkbook.BuiltinDocumentProperties(sPropName).Value
End Function
for a built-in property, or as:
Public Function UsrProp(ByVal sPropName As String) As String
Application.Volatile
On Error GoTo UndefinedProp
UsrProp = ActiveWorkbook.CustomDocumentProperties(sPropName)
GoTo Exit
UndefinedProp:
UsrProp = "n/a"
Exit:
End Function
As already mentioned, including Application.Volatile will allow for a semi-automatic cell contents update.
However, this poses a problem on its own: whenever you open your Excel file, all the cells using such a relationship will get updated and, by the time you exit the file, Excel will ask you for your permission to update it, no matter if you did introduce any change on it or not, because Excel itself did.
In my development group, we use SubVersion as a version control system. In case you inadvertently hit "update" on exit, SVN will notice it and next time you want to commit your changes, the excel file will be included in the pack.
So I decided to use everything at hand to do whatever I needed and avoid, at the same time, this self-update effect I didn't want.
That means using named ranges in combination with property accessing function/s.
Given the fact I can't expect old files to have provision for my new needs, I wrote this function:
Private Function RangeAssign(sRange As String, sValue As String) As Integer
Dim rDest As Range
If RangeCheck(sRange) Then
Set rDest = Range(sRange)
Else
Set rDest = Application.InputBox(sMsg + vbCrLf + vbCrLf + _
"Please, select a cell to get" + vbCrLf + _
"the name " + sRange + " assigned", sCopyRight, Type:=8)
rDest.Name = sRange
End If
rDest.Cells(1, 1).NumberFormat = "#"
rDest.Cells(1, 1).Value = sValue
RangeAssign = True
End Function
It allows for a proper selection of the destination cell. When assigning values to a property (let's say "Author", which happens to be a built-in one), I also update the value stored in the named range, and can write in a cell:
=Author
if I happen to have defined a range named "Author" and filled its "A1" cell with the value for built-in property "Author", which I need to have updated for our own external tracking purposes.
This all didn't happen overnight. I hope it can be of some help.
I used this for extracting the SharePoint properties (based on Martin's answer):
Public Function DocumentProperty(Property As String)
Application.Volatile
On Error GoTo NoDocumentPropertyDefined
DocumentProperty = ActiveWorkbook.ContentTypeProperties(Property).Value
Exit Function
NoDocumentPropertyDefined:
DocumentProperty = CVErr(xlErrValue)
End Function
I want to have/define a unique id for each data row in my Excel data sheet - such that I can use it when passing the data onwards and it stays the same when rows are added/deleted above it.
My thoughts are to use the ID attribute of Range (msdn link)
So, I have a user defined function (UDF) which I place in each row that gets/sets the ID as follows:
Dim gNextUniqueId As Integer
Public Function rbGetId(ticker As String)
On Error GoTo rbGetId_Error
Dim currCell As Range
'tried using Application.Caller direct, but gives same error
Set currCell = Range(Application.Caller.Address)
If currCell.id = "" Then
gNextUniqueId = gNextUniqueId + 1
'this line fails no matter what value I set it to.
currCell.id = Str(gNextUniqueId)
End If
rbGetId = ticker & currCell.id
Exit Function
rbGetId_Error:
rbGetId = "!ERROR:" & Err.Description
End Function
But this fails at the line mentioned with
"Application-defined or object-defined error"
I thought perhaps its one of those limitations of UDFs, but I also get the same error if I try it from code triggered from a ribbon button...
Any other suggestions on how to keep consistent ids - perhaps I should populate the cells via my ribbon button, finding cells without IDs and generating/setting the cell value of those...
EDIT:
As Ant thought, I have the sheet protected, but even in an unlocked cell it still fails. Unprotecting the sheet fixes the problem.... but I have used "Protect UserInterFaceOnly:=True" which should allow me to do this. If I manually allow "Edit Objects" when I protect the sheet it also works, but I don't see a programmatic option for that - and I need to call the Protect function in AutoOpen to enable the UserInterfaceOnly feature...
I guess I need to turn off/on protect around my ID setting - assuming that can be done in a UDF... which it seems it cannot, as that does not work - neither ActiveSheet.unprotect nor ActiveWorkbook.unprotect :(
Thanks in advance.
Chris
Okay...
It does appear that if the sheet is locked, macros do not have write access to low-level information such as ID.
However, I do not think it is possible to unprotect the sheet within a UDF. By design, UDFs are heavily restricted; I think having a cell formula control the sheet protection would break the formula paradigm that a cell formula affects a cell only.
See this page on the Microsoft website for more details.
I think this limits your options. You must either:
give up sheet protection
give up the UDF, use a Worksheet_Change event to capture cell changes and write to ID there
use a UDF that writes the ID into the cell value, rather than save to ID
The UDF approach is fraught with problems as you are trying to use something designed for calculation of a cell to make a permanent mark on the sheet.
Nonetheless, here's an example of a UDF you can use to stamp a "permanent" value onto a cell, which works on unlocked cells of a protected sheet. This one only works for single cells (although it could be adapted for an array formula).
Public Function CellMark()
Dim currCell As Range
Set currCell = Range(Application.Caller.Address)
Dim myId As String
' must be text; using .value will cause the formula to be called again
' and create a circular reference
myId = currCell.Text
If (Trim(myId) = "" Or Trim(myId) = "0") Then
myId = "ID-" & Format(CStr(gNextUniqueId), "00000")
gNextUniqueId = gNextUniqueId + 1
End If
CellMark = myId
End Function
This is quite flawed though. Using copy or the fillbox will, however, retain the previous copied value. Only by explicitly setting cells to be a new formula will it work. But if you enter in the formula into the cell again (just click it, hit ENTER) a new value is calculated - which is standard cell behaviour.
I think the Worksheet_Change event is the way to go, which has much more latitude. Here's a simple example that updates the ID of any cell changes. It could be tailored to your particular scenario. This function would need to be added to every Worksheet the ID setting behaviour is required on.
Private Sub Worksheet_Change(ByVal Target As Range)
Dim currCell As Range
Set currCell = Target.Cells(1, 1)
Dim currId As String
currId = currCell.ID
If Trim(currCell.ID) = "" Then
Target.Parent.Unprotect
currCell.ID = CStr(gNextUniqueId)
Target.Parent.Protect
gNextUniqueId = gNextUniqueId + 1
End If
End Sub
Last note; in all cases, your ID counter will be reset if you re-open the worksheet (at least under the limited details presented in your example).
Hope this helps.
Concur with Ant - your code works fine here on Excel 2003 SP3.
I've also been able to use:
Set currCell = Application.Caller
If Application.Caller.ID = "" Then
gNextUniqueId = gNextUniqueId + 1
'this line fails no matter what value I set it to.
currCell.ID = Str(gNextUniqueId)
End If
Aha! I think I have it.
I think you're calling this from an array formula, and it only gets called ONCE with the full range. You can't obtain an ID for a range - only a single cell. This explains why Application.Caller.ID fails for you, because Range("A1:B9").ID generates an Application-defined or object-defined error.
When you use Range(Application.Caller.Address) to get the "cell" you just defer this error down to the currCell.ID line.
I think we may have a few issues going on here, but I think they are testing issues, not problems with the code itself. First, if you call the function from anything other than a Cell, like the immediate window, other code, etc. Application.Caller will not be set. This is what is generating your object not found errors. Second, if you copy/paste the cell that has the function, they you will by copy/pasting the ID too. So wherever you paste it to, the output will stay the same. But if you just copy the text (instead of the cell), and then paste then this will work fine. (Including your original use of Application.Caller.)
The problem is with Application.Caller.
Since you are calling it from a user defined function it is going to pass you an error description. Here is the remark in the Help file.
Remarks
This property returns information about how Visual Basic was called, as shown in the following table.
Caller - Return value
A custom function entered in a single cell - A Range object specifying that cell
A custom function that is part of an array formula in a range of cells - A Range object specifying that range of cells
An Auto_Open, Auto_Close, Auto_Activate, or Auto_Deactivate macro - The name of the document as text
A macro set by either the OnDoubleClick or OnEntry property - The name of the chart object identifier or cell reference (if applicable) to which the macro applies
The Macro dialog box (Tools menu), or any caller not described above - The #REF! error value
Since you are calling it from a user defined function, what is happening is Application.Caller is returning a String of an error code to your range variable curCell. It is NOT causing an error which your error handler would pick up. What happens after that is you reference curCell, it's not actually a range anymore. On my machine it tries setting curCell = Range("Error 2023"). Whatever that object is, it might not have an ID attribute anymore and when you try to set it, it's throwing you that object error.
Here's what I would try...
Try removing your error handler and see if VBA throws up any exceptions on Range(Application.Caller.Address). This won't fix it, but it could point you in the right direction.
Either through logic or Application.ActiveCell or however you want to do it, reference the cell directly. For example Range("A1") or Cells(1,1). Application.Caller.Address just doesn't seem like a good option to use.
Try using Option Explicit. This might make the line where you set curCell throw up an error since Range(Application.Caller.Address) doesn't look like it's passing a range back, which is curCell's datatype.
I have found that if I protect the sheet with "Protect DrawingObjects:=False", the UDF can set the Id. Strange.
Thanks for all the help with this.