views:

2445

answers:

5

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

+1  A: 

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.

Joel Goodwin
Its not a range formula, just a single cell.
Chris Kimpton
Following your EDIT above - have you tried using ActiveSheet.Unprotect or ActiveWorkbook.Unprotect? You can always reference the Cell.Parent if you want to avoid being explicit with the sheet (a wise move). I think because the UDF is part of the user interface, being a formula, it has been prevented from making any changes to the sheet, but it's just a hunch.
Joel Goodwin
+1  A: 

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.)

Oorang
Thanks for the heads up on the copy/paste features of ID
Chris Kimpton
+3  A: 

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.

Joel Goodwin
Many Thanks for this detailed analysis, but I have found that if I protect the sheet with "Protect DrawingObjects:=False", the UDF can set the Id. Strange...
Chris Kimpton
Darn, well done on finding that. Are you completely sorted now or are there still malingering issues?
Joel Goodwin
+1  A: 

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...

  1. 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.

  2. 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.

  3. 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.

mandroid
A: 

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.

Chris Kimpton