How to avoid default property gotchas in VBA? - vba

I only use VBA occasionally, and every time I come back to it I get caught out by some variation of the following:
I have a Range object, currentCell, that I use to keep track of what cell I'm working with in the spreadsheet. When I update this to point to a different cell, I write:
currentCell = currentCell.Offset(ColumnOffset:=1)
The problem is that I've forgotten the Set keyword, so what the above line actually does is use the default property of the Range objects:
currentCell.Value = currentCell.Offset(ColumnOffset:=1).Value
So the contents of the current cell are overwritten by what's in the new cell, and my currentCell variable hasn't changed to point to a new cell, and I get filled with rage as I realize I've made the same mistake for the hundredth time.
There probably isn't a better answer than to put a post-it on my monitor saying "Have you remembered to use Set today?", but if anyone has any suggestions to help me, I'd appreciate hearing them. In particular:
Is there any way to turn on warnings when you implicitly use default properties? I have never used them like this on purpose, I'd always call Range.Value if that's what I meant.
Is there any good practice for marking variables as "this should only be used to read from the spreadsheet"? In most code I write, almost all my variables are for gathering data, and it would be handy to get a warning if something starts inadvertently editing cells like this.

It took me a while to understand how you are running into problem since I do not think I have ever had this problem in spite of using range objects for years. After thinking about things, I realized I do the following 100% of the time when working with cells - I use the offset or cells functions constantly - I rarely use Set to redefine a current variable.
If I have a loop I am iterating through to go through the spreadsheet, I may do something like
Dim startRng as Range
Set startRng = range("A1")
for i = 1 to 100
startRng.offset(i,0).value = i
startRng.offset(i,1).value = startRng.offset(i,0).value
next i
or
for i = 1 to 100
cells(i,0).value = i
cells(i,1).value = cells(i,0).value
next i
Either of these notations means I almost rarely have to use Set with a range object - almost always this happens once (if at all) and indicates the first cell in a range I will iterate over or reference.
It is also really clear what the offset is - since you specify a row/column - which makes it really straightforward what is happening in the code and easier to track since it references a single cell. You don't have to track down and trace backwards to the last 3 places you update a currentCell Range object.
Adopting a style of coding using these sorts of styles should eliminate nearly all these errors you are making. I am quite serious when I say I cannot remember ever having made a similar error in all my years coding VBA - I use the offset and cells functions continuously in my code (loops in these examples, but I use similar methods with all other examples in code) rather than setting ranges to new ranges. The side effect is that when you are setting a range in your code, it is almost ALWAYS immediately following a Dim statement and much more clear.

Whatever you choose to do, you'll need the post-it note, I'm afraid. After all, setting a range object's value to the value in another cell is a perfectly valid and common thing to do. The code has no way of knowing that you want it to do anything other than what you ask it to.
You could try checking your range object's address before and after you update it to make sure it's different, but if you remember to do that, you would be better off simply using the set keyword to update the object the way you intended.
Now that this issue has enraged you to the point of posting your question, I imagine that you'll never forget it again, regardless of how much time goes by before your next visit to VBA. So maybe you won't need the post-it note after all.

Is there any way to turn on warnings when you implicitly use default properties?
No.
Is there any good practice for marking variables as "this should only be used to read from the spreadsheet"?
Well, you could make your own variable naming convention, à la Making Wrong Code Look Wrong, but you'll still have to check your own code visually and the compiler won't help you do that. So I wouldn't rely on this too much.
A better option is to circumvent the need for repeatedly redifining currentCell using .Offset altogether.
Instead, read the entire range of interest to a Variant array, do your work on that array, and then slap it back onto the sheet when you're done modifying it.
Dim i As Long
Dim j As Long
Dim v As Variant
Dim r As Range
Set r = Range("A1:D5") 'or whatever
v = r.Value 'pull from sheet
For i = 1 To UBound(v, 1)
For j = 1 To UBound(v, 2)
'code to modify or utilise element v(i,j) goes here
Next j
Next i
r.Value = v 'slap v back onto sheet (if you modified it)
Voilà. No use of default properties or anything that could be confused as such. As a bonus, this will speed up your code execution.

There probably isn't a better answer than to put a post-it on my monitor saying "Have you remembered to use Set today?", but if anyone has any suggestions to help me, I'd appreciate hearing them. In particular:
I would slightly change the wording on the Post It
"Are you sure you have not forgotten using Option Explicit and Error Handling?"
Otherwise trust me there is no better way! Having said that, I would like to confirm that "Using Set" is the least of your worries. What should be on the top of your main worries is "Writing Good Code" and this doesn't come overnight. It all comes by practice.
My advice to all beginners. Never assume! For example, .Value is the default property of the range so
Range("A1") = "Blah"
is correct. But still avoid using that.
Always Fully qualify your variables
Always Use Option Explicit
Always Use Error handling
For example, This works.
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim rng As Range
On Error GoTo Whoa
Set ws = ThisWorkbook.Sheets("Sheet1")
Set rng = ws.Range("A1")
rng.Value = "Blah"
Exit Sub
Whoa:
MsgBox Err.Description
End Sub
Now let's try the above code without using the Set command. Try the below code. What happens?
Option Explicit
Sub Sample()
Dim ws As Worksheet
Dim rng As Range
On Error GoTo Whoa
ws = ThisWorkbook.Sheets("Sheet1")
rng = ws.Range("A1")
rng.Value = "Blah"
Exit Sub
Whoa:
MsgBox Err.Description
End Sub
Recommended Read.
Topic: To ‘Err’ is Human
Link: http://siddharthrout.wordpress.com/2011/08/01/to-err-is-human/

I think enderland has highlighted the basic solution, which is to handle processing multiple cells in a loop. To take it further, I'd suggest using a For Next loop for cycling through cells. Probably on of the most common bits of code I write is something like:
Dim cell as Excel.Range
Dim rngCellsToProcess as Excel.Range
Set rngCellsToProcess = 'whatever you set it to
For each cell in rngCellsToProcess
'do something
Next cell
This won't eliminate the need for Set, but may help remind you to use it, while making it clearer what's going on.

Maybe write your own custom function and use it instead ?
Sub offset_rng(ByRef my_rng As Range, _
Optional row As Integer, Optional col As Integer)
Set my_rng = my_rng.Offset(row, col)
End Sub
Can be used like this:
Sub test()
Dim rng As Range
Set rng = Range("A1")
offset_rng my_rng:=rng, col:=1
rng.Value = "test1"
offset_rng my_rng:=rng, col:=1
rng.Value = "test2"
offset_rng my_rng:=rng, col:=1
rng.Value = "test3"
offset_rng my_rng:=rng, col:=1
rng.Value = "test4"
End Sub

Related

Excel file decided one day to format all of the cells that were General to Custom [$-409]ddd

I've been using this particular file for a number of years (expense tracking/checkbook). It has a few simple macros, but none of them have acted up after several years of fine-tuning to do anything like the effect I've encountered. I'm running Excel 2013, if that makes any difference to the scenario.
Several months ago I noticed that when entering data into an unused area on one of the worksheets, the result shown would be "Mon", "Tue", etc. Looking at the formatting drop-down list in the toolbar showed Custom, instead of General, as I would have expected (the specific formatting is [$-409]ddd). For a long time I just adjusted the formatting on the new work to whatever I needed it to be (General, Accounting, Percentage, etc) and carried on. It's become frustrating recently and I decided to investigate further.
It appears that ALL of the cells that were normally formatted as General, are really formatted as Custom. Most of the cells I didn't notice it on are simply text like Balance, Contribution, etc. so I didn't realize the formatting had changed. Only the cells that I specifically formatted as Accounting, Number, Percentage, etc. remain unaffected by the blanket "Custom-ization".
I don't have any code in my macros that do blanket changes to [$-409]ddd, only one section of code that applies "mmm dd" on one specific page, and it's hard coded to "mmm dd".
Does anyone have any clues on what may have happened? I'm open to suggestions on how to remedy the situation as well. I'm considering just a brute-force macro that walks through all of the cells in all of the worksheets, checks the formatting against [$-409]ddd and changes them to General.
This can happen if the Normal Style has been corrupted. Examine it (using right-click) and fix if necessary:
Gary's Student is most likely right as to the cause of this. Anyway, if you don't know how to fix it that way or the cause turns out to be something else, here is a brute-force way to remedy the situation in all worksheets in the workbook.
Some words of caution:
1) This will take a very long time to run, since it loops through all cells in the workbook.
2) Make sure you insert the name of the wrong number format exactly right, or it won't work.
3) Make a copy of the workbook in question before you try this to make sure you don't break anything unintentionally.
Sub resetNumberFormats()
Dim sht As Excel.Worksheet
Dim cll As Range
Dim wrongNumberformat As String
Application.ScreenUpdating = False
wrongNumberformat = "[$-409]ddd"
For Each sht In Worksheets
For Each cll In sht
If wrongNumberformat = cll.NumberFormat Then cll.NumberFormat = "General"
Next cll
Next sht
Application.ScreenUpdating = True
End Sub
Edit
The following code is much much faster and works in an instant by me. Try this instead:
Sub setNumberFormats()
Dim sht As Excel.Worksheet
Dim cll As Range
Dim wrongNumberformat As String
' insert VBA code for wrong number format below
wrongNumberformat = "[$-409]ddd"
With Application
.FindFormat.NumberFormat = wrongNumberformat
.ScreenUpdating = False
End With
On Error Resume Next
For Each sht In Worksheets
Do While Err.Number = 0
sht.Cells.Find(What:="*", SearchFormat:=True).NumberFormat = "General"
Loop
Err.Clear
Next sht
Application.ScreenUpdating = True
End Sub

Excel VBA Copy-paste-move

I am only new to VBA coding so I don't have much of a clue of what I am doing unfortunately. I was hoping that I could get a hand here.
What I want to do is.
Step One: Copy a line (which has formulas) and paste it in the same position but as values only.
Step Two: Move a selection of cells down one line.
Step Three: Copy a line (with formulas) and past it on another line with the formulas.
This is all done on the same sheet.
If I could get some help with this or some direction to some tutorials that would be really appreciated.
Normally I wouldn't do this without your showing some effort, but why not? The below are about as simple as I can get. Please still though, use the macro recorder to see how this all works. It's how a lot of us get started.
Sub StepOne()
Dim myRow As Long
myRow = 1 ' We will "copy/paste" on Row 1
' To avoid using the clipboard, you can simply set two ranges' values equal. This will
' clear out any formulas and leave the values.
Rows(myRow).Value = Rows(myRow).Value
End Sub
And the second:
Sub StepTwo()
Dim rng As Range
'Change this as required. Note the use of `Set` with a `Range` type.
Set rng = Range("A1:A10")
rng.Cut rng.Offset(1, 0)
End Sub
And the last one:
Sub StepThree()
' Not tellin :P! You should be able to get this. Hint: look at StepOne.
End Sub
Edit: Ah, I realize now that Step Three is a little more involved than setting two ranges equal since you want to maintain the formula. However, I'll leave this as a learning opportunity for you to investigate. If you can't figure it out, let me know and I can guide you. :D

VBA Error 1004 - PasteSpecial method of Range class

Recently I started getting the error 1004: PasteSpecial method of Range class failed. I know people have said before that it might be that it is trying to paste to the active sheet when there is none, however, as you can see, everything is based on ThisWorkbook so that shouldn't be the problem. It happens extra much when Excel doesn't have the focus.
'references the Microsoft Forms Object Library
Sub SetGlobals()
Set hwb = ThisWorkbook' home workbook
Set mws = hwb.Worksheets("Code Numbers") ' main worksheet
Set hws = hwb.Worksheets("Sheet3") ' home worksheet (Scratch pad)
Set sws = hwb.Worksheets("Status") ' Status sheet
Set aws = hwb.Worksheets("Addresses") ' Addresses sheet
End Sub
Sub Import()
Call SetGlobals
hws.Select
'a bunch of code to do other stuff here.
For Each itm In itms
Set mitm = itm
body = Replace(mitm.HTMLBody, "<img border=""0"" src=""http://www.simplevoicecenter.com/images/svc_st_logo.jpg"">", "")
Call Buf.SetText(body)
Call Buf.PutInClipboard
Call hws.Cells(k, 1).Select
Call hws.Cells(k, 1).PasteSpecial
For Each shape In hws.Shapes
shape.Delete
Next shape
'Some code to set the value of k
'and do a bunch of other stuff.
Next itm
End Sub
Update: mitm and itm have two different types, so I did it for intellisense and who knows what else. This code takes a list of emails and pastes them into excel so that excel parses the html (which contains tables) and pastes it directly into excel. Thus the data goes directly into the sheet and I can sort it and parse it and whatever else I want.
I guess I'm basically asking for anyone who knows another way to do this besides putting it in an html file to post it. Thanks
This probably will not exactly answer your problem - but I noticed a few things in your source code that are too long to place in a comment, so here it is. Some of it is certainly because you omitted it for the example, but I'll mention it anyway, just in case:
Use Option Explicit - this will avoid a lot of errors as it forces you to declare every variable
Call SetGlobals can be simplified to SetGlobals - same for Call Buf.SetText(body) = Bof.SetText Body, etc.
No need to '.Select' anything - your accessing everything directly through the worksheet/range/shape objects (which is best practice), so don't select (hws.Select, hws.Cells(k,1).Select)
Why Set mitm = itm? mitm will therefore be the same object as itm - so you can simply use itm
You're deleteing all shapes in hwsmultiple times - for each element in itms. However, once is enough, so move the delete loop outside of the For Each loop
Instead of putting something in the clipboard and then pasting it to a cell, just assign it directly: hws.Cells(k, 1).Value = body - this should solve your error!
Instead of using global variables for worksheets that you assign in 'SetGlobals', simply use the sheet objects provided by Excel natively: If you look at the right window in the VBE with the project tree, you see worksheet nodes Sheet1 (sheetname), Sheet2 (sheetname), etc.. You can rename these objects - go to their properties (F4) and change it to meaningful names - or your current names (hwb, mws, ...) if you want. Then you can access them throughout your code without any assignment! And it'll work later, even if you change the name of Sheet3to something meaningful! ;-)
Thus, taking it all into account, I end up with the following code, doing the same thing:
Option Explicit
Sub Import()
'a bunch of code to do other stuff here.
For Each shape In hws.Shapes
shape.Delete
Next shape
For Each itm In itms
Call hws.Cells(k, 1) = Replace(itm.HTMLBody, "<img border=""0"" src=""http://www.simplevoicecenter.com/images/svc_st_logo.jpg"">", "")
'Some code to set the value of k
'and do a bunch of other stuff.
Next itm
End Sub

Referring to Dynamic Named Ranges in VBA

I'm having troubling referring to a Dynamic Name Range in VBA.
My ranges are defined as
=OFFSET(Sheet!$B$2,0,0,COUNTA(Sheet!$B:$B)-1,1)
My code should search one range for all entries in another range, the intention being that any missing entries will be added. So far I have
Sub UpdateSummary()
Dim Cell As Range
Dim rngF As Range
Set rngF = Nothing
' Step through each cell in data range
For Each Cell In Worksheets("Aspect").Range("A_Date")
' search Summary range for current cell value
Set rngF = Worksheets("Summary").Range("Sum_Date").Find(Cell.Value) // Does not work
If rngF Is Nothing Then
' Add date to Summary
End If
Set rngF = Nothing
Next Cell
End Sub
The For loop seems to work ok. However, using the .Find method is giving me an error message.
Application-defined or object-defined error
It does work if I replace the named range with a specific range ($B$2:$B$5000), so it seems to be down to how the named range is being passed.
Any ideas would be appreciated.
Thanks.
The error is almost definitely because Excel can't find a named range Sum_Date that refers to a range on a worksheet named Summary. The most common causes are
Sum_Date refers to a sheet other than Summary. Check the RefersTo property of Sum_Date and make sure nothing is misspelled.
There is not a named range Sum_Date, that is, it's misspelled in the VBA code. Check the spelling of the named range in the Name Manager.
There is an error in the RefersTo formula of Sum_Date. It sounds like you already verified that this isn't the case.
I've had the a similar if not the same problem & here's how I solved it:
I first realized that the method I used to create my named range, using the Name Manager, my named range had a scope of Workbook. This is important because, it doesn't belong to the worksheet, & therefore will not be found there.
So, Worksheets("Summary").Range("Sum_Date") would not work for me.
Since my range belonged to the workbook, the way I was able to find is to use ActiveWorkbook.Names("Sum_Date")
For me I used it to remove the formula from named range that I am using in many places. The huge advantage is that named range is updated only once instead of the formula being called for every cell location that ranged is called. Huge time delay difference!
Public last_Selection As String
Private Sub Worksheet_Change(ByVal Target As Range)
'excel data change detection
If Range(last_Selection).Column = 2 Then
'Disable events, so this only executes once
Application.EnableEvents = False
'This can be done with a complex formula in a cell,
'but this is easily understood
Range("B1").End(xlDown).Select
ActiveWorkbook.Names("last_Entry").Value = ActiveCell.Row
'Re-enable so this routine will execute on the next change
Application.EnableEvents = True
End If
End Sub
Private Sub Worksheet_SelectionChange(ByVal Target As Range)
'constantly store the last cell to know which one was previously edited
last_Selection = Target.Address
End Sub
I know this is a very old thread, but I had the same issue today and I was looking for solution for quite a long time. So maybe this will help someone.
The named "range" defined by the =OFFSET(...) formula is actually a named FORMULA, so in VBA you have to evaluate it first to get the range. E.g.:
Set rgNamedRange = Worksheets("Summary").Evaluate("Sum_Date")
Credits to a guy named "shg" from mrexcel.com, who got me on right track. :)
I have been experimenting with this for a few days and eventually I came up with the following. It may not be the most efficient but it did work for me!
The named range of "OhDear" was set up in the normal way
Dim vItem As Variant
Set vItem = Names("OhDear")
Debug.Print vItem.Name
Worth a try don't you think!
This does not work if instead of using a variant you use something like: Dim Nm as Name: Set Nm = Names("OhDear"). Any variations using 'Nm' failed!!!

How to get/set unique id for cell in Excel via VBA

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.