The macro recorder is a wonderful thing. It’s one of the tools at your disposal to assist you in your journey, a good way to observe code that does exactly what you just did, and learn what parts of the object model to use for doing what. The problems begin when you see macro recorder code constantly invoking Range.Select
and Worksheet.Activate
, working against the Selection
object, generating dozens of repetitive statements and redundant code that can easily trick a neophyte into thinking “so that’s how it’s done!” – the macro recorder is a great way to familiarize with a number of APIs, …and that’s all it needs to be.
There are very few ways to write more inefficient and bug-prone code, than to use the macro recorder’s output as a model of how VBA code should be written. How to avoid Select and Activate has to be the single most linked-to post in the VBA tag on Stack Overflow, yet an untold number of young souls remain to be saved from the curse of the macro recorder.
Of course, we have all the tools we need to defeat that curse. I’m not going to repeat everything in that very good SO thread, but the crux of it boils down to, in my opinion, a few simple things.
Early Binding and Guard Clauses
From an automation standpoint, Selection
is an interesting object. In the Excel object model, Selection
is a Shape
that’s selected, the Chart
you just clicked on, the Range
of cells you navigate to. If the current selection is relevant to your code, consider making it an input with an explicit object type: the selection you’re expecting very likely has a very specific type, like a Range
. Simple example:
Public Sub MyMacro()
Selection.Value = 42 'multiple possible errors
End Sub
If Selection
is pulled from that code and taken in as a Range
parameter instead, we eliminate all ambiguities and restore the natural balance of the universe by coding against the Range
interface rather than against Object
– which means compile-time validation and IntelliSense:
Public Sub MyMacro()
If Not TypeOf Selection Is Excel.Range Then Exit Sub '<~ that's a *guard clause*
DoSomething Selection
End Sub
Private Sub DoSomething(ByVal target As Range)
target.Value = 42
End Sub
Note the similarities between MyMacro
in the first snippet, and DoSomething
in the second one – it’s what they do differently that makes… all the difference. Now the procedure can work with any Range
object, whether it’s actually selected or not.
Working with Selection
is never really needed: what you can do against Selection
you can do with any Range
if what you mean to work with is a Range
, or any Chart
if what you mean to work with is a Chart
.
It might look like it’s more code, more complicated to write – it might even be. But binding these types at compile-time makes things much simpler, really. When we make a member call against Object
, the compiler doesn’t even care that the member exists. This involves overhead at run-time, and a non-zero chance of error 438 being raised when the member does not exist. Like Variant
, Object
is very flexible… too much for its own good.
A member call against Selection
is inherently late-bound. Exactly like dynamic
in C#, you want to break out of it, as soon as possible: if you’re expecting a Range
, declare a Range
and run with it, be explicit. In turn, you’ll be rewarded with VBA blowing up with a type mismatch error (13) early on, rather than with some object doesn’t support property or method error (438), possibly far removed from where the problem really stems from – the instruction that wrongly assumed a Selection
could be treated like any old Range
:
Public Sub MyMacro()
Dim cell As Range
Set cell = Selection '<~ type mismatch if Selection isn't a Range
End Sub
The macro recorder will never generate a single control flow statement. No loops, no conditionals, no variables, no structure. If you take something that’s tedious, and make a computer do it tediously for you, you may be “getting the job done”, but you’re 1) being very mean to your computer, and 2) you could easily be misusing the object model in ways that make it inefficient. The best way to tell a computer to do something 20 times isn’t to tell it 20 times to do one thing!
By simply introducing/declaring variables to hold the Worksheet
and Range
objects we’re working with, we eliminate the need to Select
and Activate
everything we touch, and Selection
becomes useless, for the most part – if your macro is kicked off with a keyboard shortcut and works with whatever is selected at that time, more power to you – but that doesn’t mean your entire code needs to work with Selection
, only that you need to validate what’s selected and switch to early binding as early as possible, using properly typed local variables.
Use Range.Select
when you need to programmatically visually select cells in the worksheet, for the user to see that selection being made and later perhaps interacted with.