1

I have two subs and want to pass values from one to the other.

Option Explicit

Sub Test()
    Call HandleInput(ActiveSheet.Range("A1:C4"), 4, 2)
End Sub

Sub HandleInput(rng As Range, rowNumber As Long, colNumber As Long)
    Debug.Print rng.Cells(rowNumber, colNumber).Value
End Sub

However, sometimes I want to apply the same routine over the same range, but with a different rownumber and a different colnumber. I could just call the sub again with the new values and right now that seems to be by far the easiest option, but I still want know if there's a smart way to handle it with optional parameters in HandleInput:

Sub HandleInput(rng As Range, rowNumber As Long, colNumber As Long, Optional rowNumber2 As Long, _
Optional colNumber2 As Long, Optional rowNumber3 As Long, Optional colNumber3 As Long)
   ...
End Sub

This made me wonder:

Can I somehow tell VBA that, if rowNumber2 is being provided, a value for colNumber2 needs to be passed as well? I know I could try it with IsMissing() and switching the data type to Variant:

Sub HandleInput(rng As Range, rowNumber As Long, colNumber As Long, Optional rowNumber2 As Variant, 
     _ Optional colNumber2 As Variant, Optional rowNumber3 As Variant, Optional colNumber3 As Variant)
          If Not IsMissing(rowNumber2) Then
              If IsMissing(colNumber2) Then
                   MsgBox "Please enter a value for colNumber2."
                   End
              End If
          End If
End Sub

This requires lots of if-Statements, also in the other direction, (If NOT IsMissing(colNumber2) Then). And it only gets worse if more than two variables should be tied together. Any calculation that I try as a workaround gives me an error ("Type mismatch") when one value is missing, for example I tried:

If IsError(rowNumber2 * colNumber2) Then
   MsgBox "Error, please supply both rowNumber2 and colNumber2"
End If

Is there a native functionality for this? The only solution I came up with is supplying default values that I know won't occur "naturally":

Sub HandleInput(rng As Range, rowNumber As Long, colNumber As Long, Optional rowNumber2 As Long = -100, _
Optional colNumber2 As Long = -100, Optional rowNumber3 As Long = -100, Optional colNumber3 As Long = -100)

     If rowNumber2 = -100 Or colNumber2 = -100 Then
        MsgBox "Please enter a value for both rowNumber2 and colNumber2."
        End
    End If
End Sub
3
  • You could use an array as the arguments bettersolutions.com/vba/arrays/passing-arrays.htm Commented Feb 12, 2020 at 22:10
  • 3
    Review the concepts in passing parameters either ByRef or ByVal. Then make it a habit to tag every single parameter with one of those keywords. Commented Feb 12, 2020 at 22:22
  • @Peter T Will do, thanks for the hint Commented Feb 13, 2020 at 8:08

2 Answers 2

5

Magic defaults are a bad idea.

You need a concept of "something that represents two values that need to always go together" - that sounds very much like needing some kind of Tuple object that encapsulates two values; I'd go with the nuclear strongly-typed option, and add two new class modules - first some general-purpose ITuple interface:

'@Interface
Option Explicit

Public Property Get Item1() As Variant
End Property

Public Property Get Item2() As Variant
End Property

Public Function ToString() As String
End Function

And then a RangeLocation class that implements it:

'@PredeclaredId 'see https://github.com/rubberduck-vba/Rubberduck/wiki/VB_Attribute-Annotations
Option Explicit
Implements ITuple

Private Type TInternal
    RowIndex As Long
    ColumnIndex As Long
End Type

Private this As TInternal

Public Function Create(ByVal atRow As Long, ByVal atColumn As Long) As ITuple
    Dim result As RangeLocation
    Set result = New RangeLocation
    result.RowIndex = atRow
    result.ColumnIndex = atColumn
    Set Create = result
End Function

Public Property Get RowIndex() As Long
    RowIndex = this.RowIndex
End Property

Public Property Let RowIndex(ByVal value As Long)
    If value <= 0 Then Err.Raise 5
    this.RowIndex = value
End Property

Public Property Get ColumnIndex() As Long
    ColumnIndex = this.ColumnIndex
End Property

Public Property Let ColumnIndex(ByVal value As Long)
    If value <= 0 Then Err.Raise 5
    this.ColumnIndex = value
End Property

Private Property Get ITuple_Item1() As Variant
    ITuple_Item1 = this.RowIndex
End Property

Private Property Get ITuple_Item2() As Variant
    ITuple_Item2 = this.ColumnIndex
End Property

Private Function ITuple_ToString() As String
    ITuple_ToString = "R" & this.RowIndex & "C" & this.ColumnIndex
End Function

Note how it's impossible to have an instance of that object that encapsulates a negative row or column index. And now we can do this:

Dim a As ITuple
Set a = RangeLocation.Create(1, 1)

Which means we can also do this:

Public Sub DoSomething(ByVal source As Range, ParamArray values() As Variant)
    Dim i As Long
    For i = LBound(values) To UBound(values)

        Dim location As ITuple
        Set location = values(i)

        On Error Resume Next
        Debug.Print source.Cells(location.Item1, location.Item2).Value
        If Err.Number <> 0 Then Debug.Print "Location " & location.ToString & " is outside the specified source range."
        On Error GoTo 0

    Next
End Sub

...and it's now somebody else's job to make sure they're providing valid values - more precisely, it's the calling code's job:

Dim source As Range
Set source = ActiveSheet.Range("A1:C4")

DoSomething source, _
    RangeLocation.Create(4, 2), _
    RangeLocation.Create(1, 1), _
    RangeLocation.Create(2, 2)
    '...

If the caller tries to do RangeLocation.Create(0, -12), there will be a runtime error (because the Property Let members of the RangeLocation class don't allow negative values) and DoSomething will not even be invoked.

Sign up to request clarification or add additional context in comments.

Comments

3

You could handle the whoile thig with a ParamArray and range check the incomoing array

Sub HandleInput(rng As Range, ParamArray RCPairs() As Variant)

    If UBound(RCPairs) < 1 Then
        Err.Raise 513, "HandleInput", "Please enter at least one pair of RowNumber, ColNumber."
    ElseIf UBound(RCPairs) Mod 2 = 0 Then
        Err.Raise 513, "HandleInput", "Please enter a value for both RowNumber and ColNumber."
    End If

    ' ...

End Sub

Called like this

Sub Demo()
    HandleInput SomeRange, r1, c1, r2, c2 ' Works

    HandleInput SomeRange ' Error "Please enter at least one pair of RowNumber, ColNumber."

    HandleInput SomeRange, r1, c1, r2, c2, x ' Error: "Please enter a value for both RowNumber and ColNumber."

End Sub

Note: I cahnged your MsgBox, End to raising an error, so your calling code can decide what to do about the error. BTW using End is unwise, see here

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.