In VBA, how do I access the subject of a With statement from within that statement? - excel

I am working on a quicker way to cycle through a column in a table in some previously written code. The problem I have is that at some point I need to assign the subject of the With statement (a single cell range) to an array of ranges, depending on the value of the range and nearby cells.
I have trimmed the code and taken only those bits which are necessary to the problem. See below:
Dim wb As Workbook
Dim wsFit As Worksheet
Dim fittingsTable As ListObject
ReDim fittings(0) As Range
Dim x As Integer
Dim y As Integer
Set wb = ActiveWorkbook
Set wsFit = wb.Worksheets("Fittings")
Set fittingsTable = wsFit.ListObjects("FittingsTable")
For x = 1 To fittingsTable.DataBodyRange.Rows.Count
With fittingsTable.DataBodyRange(x, 15)
If .Value <> vbNullString And .Value <> "0" Then
If .Offset(0, -2).Value <> "TBC" Then
'Do some stuff
Set fittings(y) = 'PROBLEM HERE
Else
'Do other stuff here
End If
End If
End With
Next
I want to assign fittingsTable.DataBodyRange(x, 15) to fittings(y), but I have no idea how to access the range that is the subject of the With statement.
I know that I could assign the desired range to another variable before the With statement begins, and then assign that variable to fittings(y) instead, but I feel like there must be a simple way to access the initial subject of the With statement so that I don't end up clogging my code with yet more variables. I could also use the .Address property to assign the range using the worksheet, but at this point I'm genuinely curious about finding a more direct way.

Your With block is holding a Range object reference.
You can use the .Cells (parameterless) property to retrieve a reference to that Range object:
Set fittings(y) = .Cells
Or, to make it more explicit that it's a single-cell range:
Set fittings(y) = .Cells(1, 1)
This makes an implicit default member call that ultimately ends up being equivalent to:
Set fittings(y) = .Item(1, 1)
That works for a Range. For a lot of other classes, there is no property that returns a reference to the object. For example, a Collection:
With New Collection
.Add 42
Set foo = ???? ' can't get a reference to the Collection object!
End With
The general solution is to extract the With block variable into a local variable, and now that variable is accessible just like any other local:
Dim c As Collection
Set c = New Collection
With c
.Add 42
Set foo = c
End With
For a custom class that you control, you can have a property getter that returns Me:
Public Property Get Self() As Class1
Set Self = Me
End Property
And now the With block variable is accessible through that property:
With New Class1
.Something = 42
Set foo = .Self
End With

Related

Excel VBA throws "Run-time error '91': Object variable or With block variable not set" when setting a range [duplicate]

Pardon me as am a newbie in VBA.
Sometimes I use
Dim r as Range
r = Range("A1")
Other times I use
Set r = Range("A1")
What is the difference? And when should I use what?
There's no reason to use set unless referring to an object reference. It's good practice to only use it in that context. For all other simple data types, just use an assignment operator. It's a good idea to dim (dimension) ALL variables however:
Examples of simple data types would be integer, long, boolean, string. These are just data types and do not have their own methods and properties.
Dim i as Integer
i = 5
Dim myWord as String
myWord = "Whatever I want"
An example of an object would be a Range, a Worksheet, or a Workbook. These have their own methods and properties.
Dim myRange as Range
Set myRange = Sheet1.Range("A1")
If you try to use the last line without Set, VB will throw an error. Now that you have an object declared you can access its properties and methods.
myString = myRange.Value
Dim declares the variable.
Dim r As Range
Set sets the variable to an object reference.
Set r = Range("A1")
However, I don't think this is what you're really asking.
Sometimes I use:
Dim r as Range
r = Range("A1")
This will never work. Without Set you will receive runtime error #91 Object variable or With block variable not set. This is because you must use Set to assign a variables value to an object reference. Then the code above will work.
I think the code below illustrates what you're really asking about. Let's suppose we don't declare a type and let r be a Variant type instead.
Public Sub test()
Dim r
debug.print TypeName(r)
Set r = Range("A1")
debug.print TypeName(r)
r = Range("A1")
debug.print TypeName(r)
End Sub
So, let's break down what happens here.
r is declared as a Variant
`Dim r` ' TypeName(r) returns "Empty", which is the value for an uninitialized variant
r is set to the Range containing cell "A1"
Set r = Range("A1") ' TypeName(r) returns "Range"
r is set to the value of the default property of Range("A1").
r = Range("A1") ' TypeName(r) returns "String"
In this case, the default property of a Range is .Value, so the following two lines of code are equivalent.
r = Range("A1")
r = Range("A1").Value
For more about default object properties, please see Chip Pearson's "Default Member of a Class".
As for your Set example:
Other times I use
Set r = Range("A1")
This wouldn't work without first declaring that r is a Range or Variant object... using the Dim statement - unless you don't have Option Explicit enabled, which you should. Always. Otherwise, you're using identifiers that you haven't declared and they are all implicitly declared as Variants.
Dim: you are defining a variable (here: r is a variable of type Range)
Set: you are setting the property (here: set the value of r to Range("A1") - this is not a type, but a value).
You have to use set with objects, if r were a simple type (e.g. int, string), then you would just write:
Dim r As Integer
r=5
Dim simply declares the value and the type.
Set assigns a value to the variable.
If a variable is defined as an object e.g. Dim myfldr As Folder, it is assigned a value by using the keyword, "Set".
Dim is short for Dimension and is used in VBA and VB6 to declare local variables.
Set on the other hand, has nothing to do with variable declarations. The Set keyword is used to assign an object variable to a new object.
Hope that clarifies the difference for you.
According to VBA help on SET statement it sets a reference to an object.so if you change a property the actual object will also changes.
Dim newObj as Object
Set var1=Object1(same type as Object)
Set var2=Object1(same type as Object)
Set var3=Object1(same type as Object)
Set var4=Object1(same type as Object)
Var1.property1=NewPropertyValue
the other Vars properties also changes,so:
Var1.property1=Var2.property1=Var3.property1=Var4.property1=Object1.Property1=NewpropertyValue`
actualy all vars are the same!

Optimization in named ranges usage [duplicate]

Pardon me as am a newbie in VBA.
Sometimes I use
Dim r as Range
r = Range("A1")
Other times I use
Set r = Range("A1")
What is the difference? And when should I use what?
There's no reason to use set unless referring to an object reference. It's good practice to only use it in that context. For all other simple data types, just use an assignment operator. It's a good idea to dim (dimension) ALL variables however:
Examples of simple data types would be integer, long, boolean, string. These are just data types and do not have their own methods and properties.
Dim i as Integer
i = 5
Dim myWord as String
myWord = "Whatever I want"
An example of an object would be a Range, a Worksheet, or a Workbook. These have their own methods and properties.
Dim myRange as Range
Set myRange = Sheet1.Range("A1")
If you try to use the last line without Set, VB will throw an error. Now that you have an object declared you can access its properties and methods.
myString = myRange.Value
Dim declares the variable.
Dim r As Range
Set sets the variable to an object reference.
Set r = Range("A1")
However, I don't think this is what you're really asking.
Sometimes I use:
Dim r as Range
r = Range("A1")
This will never work. Without Set you will receive runtime error #91 Object variable or With block variable not set. This is because you must use Set to assign a variables value to an object reference. Then the code above will work.
I think the code below illustrates what you're really asking about. Let's suppose we don't declare a type and let r be a Variant type instead.
Public Sub test()
Dim r
debug.print TypeName(r)
Set r = Range("A1")
debug.print TypeName(r)
r = Range("A1")
debug.print TypeName(r)
End Sub
So, let's break down what happens here.
r is declared as a Variant
`Dim r` ' TypeName(r) returns "Empty", which is the value for an uninitialized variant
r is set to the Range containing cell "A1"
Set r = Range("A1") ' TypeName(r) returns "Range"
r is set to the value of the default property of Range("A1").
r = Range("A1") ' TypeName(r) returns "String"
In this case, the default property of a Range is .Value, so the following two lines of code are equivalent.
r = Range("A1")
r = Range("A1").Value
For more about default object properties, please see Chip Pearson's "Default Member of a Class".
As for your Set example:
Other times I use
Set r = Range("A1")
This wouldn't work without first declaring that r is a Range or Variant object... using the Dim statement - unless you don't have Option Explicit enabled, which you should. Always. Otherwise, you're using identifiers that you haven't declared and they are all implicitly declared as Variants.
Dim: you are defining a variable (here: r is a variable of type Range)
Set: you are setting the property (here: set the value of r to Range("A1") - this is not a type, but a value).
You have to use set with objects, if r were a simple type (e.g. int, string), then you would just write:
Dim r As Integer
r=5
Dim simply declares the value and the type.
Set assigns a value to the variable.
If a variable is defined as an object e.g. Dim myfldr As Folder, it is assigned a value by using the keyword, "Set".
Dim is short for Dimension and is used in VBA and VB6 to declare local variables.
Set on the other hand, has nothing to do with variable declarations. The Set keyword is used to assign an object variable to a new object.
Hope that clarifies the difference for you.
According to VBA help on SET statement it sets a reference to an object.so if you change a property the actual object will also changes.
Dim newObj as Object
Set var1=Object1(same type as Object)
Set var2=Object1(same type as Object)
Set var3=Object1(same type as Object)
Set var4=Object1(same type as Object)
Var1.property1=NewPropertyValue
the other Vars properties also changes,so:
Var1.property1=Var2.property1=Var3.property1=Var4.property1=Object1.Property1=NewpropertyValue`
actualy all vars are the same!

Tabbing into a list or combobox on a sheet

I have an array loaded with cell addresses that are passsed into a sub that handles tab order on a sheet. I want to be able to put a control name, i.e. "MyListBox" in that array and have my function handle it. However, I cannot get it to resolve in the .activate method. If I implicitly name the control it will work but I need it to "macro expand / resolve" to the actual control name so I can say Array(x).Activate.
Here is the code I'm fumbling with to no avail. I've tried it with and without the MSFORMs declaration. I've tried concatenating the command "activesheet." & arr(x) and many other things. I'm pretty sure I'm probably missing something simple but can't seem to find it.
Sub TabIntercept()
Dim arr, a, x, nxt, sel
Dim cMyListBox As MSForms.ListBox
If TypeName(Selection) <> "Range" Then Exit Sub 'Exit if (eg) a shape is selected
Set sel = Selection.Cells(1) 'if multiple cells selected use the first...
arr = GetTabOrder(ActiveSheet.Name) 'this function loads the tab order from a table
If UBound(arr) = -1 Then
Application.OnKey "{TAB}"
Exit Sub
End If
For x = LBound(arr) To UBound(arr)
If Left(arr(x), 3) = "lst" Or Left(arr(x), 3) = "cmb" Then 'Look for a control - they all start with lst/cmb
Set cMyListBox = Sheets("Resources & Process").arr(x) 'HERE IS THE ISSUE
arr(x).Activate
End If
If sel.Address() = sel.Parent.Range(arr(x)).Address() Then
'loops back to start if at end...
nxt = IIf(x = UBound(arr), LBound(arr), x + 1)
sel.Parent.Range(arr(nxt)).Select
Exit For
End If
Next x
End Sub
Set cMyListBox = Sheets("Resources & Process").arr(x) 'HERE IS THE ISSUE
First, declare a Worksheet variable for that sheet; the Workbook.Sheets property returns an Object, so all these member calls are implicitly late-bound, and you're coding blindfolded without compiler assistance.
Dim sheet As Worksheet
Set sheet = ActiveWorkbook.Worksheets("Resources & Process")
Note the ActiveWorkbook qualifier: if you have a specific Workbook object to use instead, use that. But consider always qualifying Workbook member calls, otherwise you're implicitly referring to whatever the ActiveWorkbook is, and eventually that will not be the workbook you're expecting.
Now, sheet.arr(x) isn't going to work, as IntelliSense is now showing you when you type that . dot operator: a Worksheet object would have to expose an indexed property named arr for that to work.
What you want to do, is get the OLEObject that is named whatever the value of arr(x) is.
You get OLE objects from the Worksheet.OLEObjects property:
Dim oleControl As OLEObject
Set oleControl = sheet.OLEObjects(arr(x))
If that succeeds, you've found your MSForms control - but it's wrapped in an OLE object and we now just need to unwrap it:
Set cMyListBox = oleControl.Object
If that fails, then the MSForms control isn't compatible with the declared type of cMyListBox. But now you get IntelliSense and compile-time validation for member calls against it: if you type cMyListBox. and there's an Activate member, then the call should be valid at run-time.

Refer to object properties dynamically

I have an exemplar of the class ClsFruit With following member variables:
I also have an excel sheet with data, like this:
I don't feel like Populating the object using direct references like
Fruit.Name = FruitSheet.Cells(1,2).Value
Fruit.Color = FruitSheet.Cells(2,2).Value
Fruit.Price = FruitSheet.Cells(3,2).Value
is the way to go because it's tons of repetitive code and positions of items on the worksheet might change in the future. So I wanted to loop through the first column in excel Name-Color-Priceand populate the object dynamically something like this:
Dim rg As Excel.Range
Set rg = FruitSheet.Range("A1", "A3")
Dim Cell As Variant
For Each Cell In rg
Fruit(Cell.Value) = Cell.Offset(0, 1).Value
Next Cell
But this Fruit(Cell.Value) construct doesn't work, I get "Object doesn't support this property or method" error. Is there a way around it?
You probably need to do something like that
For Each Cell In rg
Select Case Cell.Value
Case "Name"
fruit.Name = Cell.Offset(0, 1).Value
Case "Color"
fruit.Color = Cell.Offset(0, 1).Value
Case "Price"
fruit.Price = Cell.Offset(0, 1).Value
End Select
Next Cell
Another way would be to have corresponding propertiers in your class. Then you could use CallByName
For Each Cell In rg
CallByName fruit, cell.value, VbLet, Cell.Offset(0, 1).Value
Next Cell
Update: The class has to be changed like that
Option Explicit
Public mName As String
Public mColor As String
Public mPrice As Long
Property Let name(nValue As String)
mName = nValue
End Property
Property Get name() As String
name = mName
End Property
' continue with similar properties for the other member variables
Update 2:As pointed out in the comments it is not neccessary to have Let/Get etc. One can stick to public member variables and CallByName will just work fine. It is just IMHO the cleaner approach on the long run, see here
Using a worksheet proxy will do what you want, I think. This will return a collection of your objects:
WorksheetProxy class
Option Explicit
Private Property Get Table() As ListObject
Set Table = Sheet1.ListObjects(1)
End Property
Private Property Get NameColumnIndex() As Long
NameColumnIndex= Table.ListColumns("Name").Index
End Property
Private Property Get ColorColumnIndex() As Long
ColorColumnIndex= Table.ListColumns("Color").Index
End Property
Private Property Get PriceColumnIndex() As Long
PriceColumnIndex= Table.ListColumns("Price").Index
End Property
Private Property Get Data() As Collection
Dim result As Collection
Set result = New Collection
Dim currentRow As ListRow
For Each currentRow In Table.ListRows
Dim currentItem As ClsFruit
Set currentItem = New ClsFruit
currentItem.Name= currentRow.Range(ColumnIndex:=NameColumnIndex).value
currentItem.Color= currentRow.Range(ColumnIndex:=ColorColumnIndex).value
currentItem.Price= currentRow.Range(ColumnIndex:=PriceColumnIndex).value
result.Add currentItem
Next
Set Data = result
End Property
Mathieu Guindon has a discussion of this approach here: https://rubberduckvba.wordpress.com/2017/12/08/there-is-no-worksheet/
In the comments section is a link to his example workbook.

What is the difference between dim and set in vba

Pardon me as am a newbie in VBA.
Sometimes I use
Dim r as Range
r = Range("A1")
Other times I use
Set r = Range("A1")
What is the difference? And when should I use what?
There's no reason to use set unless referring to an object reference. It's good practice to only use it in that context. For all other simple data types, just use an assignment operator. It's a good idea to dim (dimension) ALL variables however:
Examples of simple data types would be integer, long, boolean, string. These are just data types and do not have their own methods and properties.
Dim i as Integer
i = 5
Dim myWord as String
myWord = "Whatever I want"
An example of an object would be a Range, a Worksheet, or a Workbook. These have their own methods and properties.
Dim myRange as Range
Set myRange = Sheet1.Range("A1")
If you try to use the last line without Set, VB will throw an error. Now that you have an object declared you can access its properties and methods.
myString = myRange.Value
Dim declares the variable.
Dim r As Range
Set sets the variable to an object reference.
Set r = Range("A1")
However, I don't think this is what you're really asking.
Sometimes I use:
Dim r as Range
r = Range("A1")
This will never work. Without Set you will receive runtime error #91 Object variable or With block variable not set. This is because you must use Set to assign a variables value to an object reference. Then the code above will work.
I think the code below illustrates what you're really asking about. Let's suppose we don't declare a type and let r be a Variant type instead.
Public Sub test()
Dim r
debug.print TypeName(r)
Set r = Range("A1")
debug.print TypeName(r)
r = Range("A1")
debug.print TypeName(r)
End Sub
So, let's break down what happens here.
r is declared as a Variant
`Dim r` ' TypeName(r) returns "Empty", which is the value for an uninitialized variant
r is set to the Range containing cell "A1"
Set r = Range("A1") ' TypeName(r) returns "Range"
r is set to the value of the default property of Range("A1").
r = Range("A1") ' TypeName(r) returns "String"
In this case, the default property of a Range is .Value, so the following two lines of code are equivalent.
r = Range("A1")
r = Range("A1").Value
For more about default object properties, please see Chip Pearson's "Default Member of a Class".
As for your Set example:
Other times I use
Set r = Range("A1")
This wouldn't work without first declaring that r is a Range or Variant object... using the Dim statement - unless you don't have Option Explicit enabled, which you should. Always. Otherwise, you're using identifiers that you haven't declared and they are all implicitly declared as Variants.
Dim: you are defining a variable (here: r is a variable of type Range)
Set: you are setting the property (here: set the value of r to Range("A1") - this is not a type, but a value).
You have to use set with objects, if r were a simple type (e.g. int, string), then you would just write:
Dim r As Integer
r=5
Dim simply declares the value and the type.
Set assigns a value to the variable.
If a variable is defined as an object e.g. Dim myfldr As Folder, it is assigned a value by using the keyword, "Set".
Dim is short for Dimension and is used in VBA and VB6 to declare local variables.
Set on the other hand, has nothing to do with variable declarations. The Set keyword is used to assign an object variable to a new object.
Hope that clarifies the difference for you.
According to VBA help on SET statement it sets a reference to an object.so if you change a property the actual object will also changes.
Dim newObj as Object
Set var1=Object1(same type as Object)
Set var2=Object1(same type as Object)
Set var3=Object1(same type as Object)
Set var4=Object1(same type as Object)
Var1.property1=NewPropertyValue
the other Vars properties also changes,so:
Var1.property1=Var2.property1=Var3.property1=Var4.property1=Object1.Property1=NewpropertyValue`
actualy all vars are the same!

Resources