2

I have a class module which hosts a worksheet_change sub, and in that sub a Userform has to pop up. I want to use a number of variables from the class module in the Userform's code. Whatever I do, however, I can't get it to work.

I have tried to apply the method from this very lenghty guide, but to no avail. Other threads on SO weren't able to help me.

Private cell As Range

Public WithEvents m_wb As Workbook

Property Get cellr() As Range
    Set cellr = cell
End Property

Property Set cellr(cellrange As Range)
    Set cell = cellrange
End Property

Public Property Set Workbook(wb As Workbook)
    Set m_wb = wb
End Property

Public Property Get Workbook() As Workbook
    Set Workbook = m_wb
End Property

Public Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) 'simplified, but accurate
    Application.EnableEvents = False

    For each cell in Target
        ReplaceTask.Show
    Next cell

    Application.EnableEvents = True
End Sub

In the userform_initialize macro, I need to be able to get the name of the m_wb workbook, as well as the cell (preferably as a range variable, otherwise just the address) in the For each cell in Target loop. For each variable in the code below I get

Error '424' object required

which shows the variables are not public..

Private Sub UserForm_Initialize()
Debug.Print cellrange.Address
Debug.Print cell.Address
Debug.Print cellr.Address
Debug.Print m_wb.Name
'....

I am positive it's my inability to understand how these properties work that's holding me back.. If someone could shine some light on what I am doing wrong, please!

Tim Stack
  • 3,209
  • 3
  • 18
  • 39
  • `ReplaceTask.txtSomeTextBox.Text=cell.address` or use properties on the userform, – Nathan_Sav Feb 07 '19 at 11:32
  • Where would I place this line and what does it do? Seems like it would just populate a textbox on the userform with the cell address – Tim Stack Feb 07 '19 at 11:35
  • 1
    if you had a text box on your form called txtSomeTextBox, it would put the address in, so you can do that after the `.show` or use properties in the form, more elegant, and create the form as an object, say `frm` then populate `set frm.RangePassed=cell` and `frm.Show` Also, try to avoid variable names, which are close to excel's own, like `cell` – Nathan_Sav Feb 07 '19 at 11:40

2 Answers2

2

To make this work it needs at least a public object variable of type of your class. And this object variable must be set to be a new instance of your class. This object variable then, and only this object variable, is the public accessable instance of your class.

Example:

Let your class be named clsWorkbook and having following code:

Option Explicit

Private m_cell As Range

Private WithEvents m_wb As Workbook

Property Let cell(cellrange As Range)
    Set m_cell = cellrange
End Property

Property Get cell() As Range
    Set cell = m_cell
End Property

Public Property Let Workbook(wb As Workbook)
    Set m_wb = wb
End Property

Public Property Get Workbook() As Workbook
    Set Workbook = m_wb
End Property

Private Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) 'simplified, but accurate
    Application.EnableEvents = False

    For Each m_cell In Target
        ReplaceTask.Show
    Next m_cell

    Application.EnableEvents = True
End Sub

Let your UserForm named ReplaceTask having following code:

Option Explicit

Private Sub UserForm_Initialize()
 Debug.Print oWB.Workbook.Name
 Debug.Print oWB.cell.Address
End Sub

And in a default Module have following code:

Option Explicit

Public oWB As clsWorkbook

Public Sub test()
 Set oWB = New clsWorkbook
 oWB.Workbook = ThisWorkbook
End Sub

Now, after Sub test() was run, do changig something in a worksheet in the workbook the code is in. This should trigger the Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) of your class object oWB then, which shows the user form which also can access oWB.Workbook.Name and oWB.cell.Address.


Because of the discussion about the need of a global instance of clsWorkbook lets have a complete example which one can reconstruct and which shows how the clsWorkbook can be a private class member:

Let your class be named clsWorkbook and having following code:

Option Explicit

Private m_cell As Range

Private WithEvents m_wb As Workbook

Property Let Cell(cellrange As Range)
    Set m_cell = cellrange
End Property

Property Get Cell() As Range
    Set Cell = m_cell
End Property

Property Let Workbook(wb As Workbook)
    Set m_wb = wb
End Property

Property Get Workbook() As Workbook
    Set Workbook = m_wb
End Property


Private Sub m_wb_SheetChange(ByVal Sh As Object, ByVal Target As Range) 'simplified, but accurate
    Application.EnableEvents = False

    Dim frm As ReplaceTask
    For Each m_cell In Target
        Set frm = New ReplaceTask
        frm.Init Me
        frm.Show
    Next m_cell

    Application.EnableEvents = True
End Sub

Let your UserForm named ReplaceTask having following code:

Option Explicit

Private m_ParentClass As clsWorkbook

Friend Sub Init(ByVal p As clsWorkbook)
    Set m_ParentClass = p
    Me.Caption = p.Workbook.Name & " : " & p.Cell.Address
End Sub

And in default class module ThisWorkbook have following code:

Option Explicit

Private oWB As clsWorkbook

Private Sub Workbook_Open()
    Set oWB = New clsWorkbook
    oWB.Workbook = Workbooks.Open("P:/Mappe1.xlsx")
End Sub

Now the clsWorkbook gets instantiated while workbook open and is a private member of ThisWorkbook and it's workbook member is the workbook which was opened addditional. There the SheetChange are listened by the clsWorkbook oWB instance.

And because the ReplaceTask user form gets instantiated in clsWorkbook and was given the class instance as parameter, this user form knows the class members too.

Axel Richter
  • 56,077
  • 6
  • 60
  • 87
  • Creating a global variable for several forms to use is a bad idea. It is much better to stop using `ReplaceTask.Show` and start using `= New ReplaceTask`, set properties, and then `.Show()`, like Nathan_Sav [suggested](https://stackoverflow.com/questions/54571425/using-a-class-module-variable-in-a-userform-error-424-object-required#comment95942708_545714250). – GSerg Feb 07 '19 at 12:08
  • @GSerg: And that will work without having `Public oWB As clsWorkbook` to be a global variable? What will represent the instance of the class `clsWorkbook` then? Or what global variable are you referencing to? The question here was why the OP was not able accessing the class members? Or have I misunderstood the problem? – Axel Richter Feb 07 '19 at 12:14
  • `clsWorkbook` is a class, so there has to be a place holding an instance of `clsWorkbook` for its code to run, but that place does not need to be global, like it probably isn't for the OP now. – GSerg Feb 07 '19 at 12:18
  • @GSerg: But in OP's code there is no such instance neither global nor other, that's the problem. And if not global where shall it be then? But do convincing me by providing your own answer. – Axel Richter Feb 07 '19 at 12:25
  • Thanks Axel, I figured it out with the help of your code. I already had a variant of the default module in your answer. After setting `oWb` as public, all was missing was referring to `oWb.cell.Address` instead of `cell.Address`... I thank you greatly! – Tim Stack Feb 07 '19 at 12:51
1

The user form and the class with the event handler are two different scopes. You cannot expect to be able to refer to members of a different scope without qualifying that scope. The code in your UserForm_Initialize interprets cellrange and cellr as local variables declared in the user form itself. You don't have such variables declared in the user form, and you are not using Option Explicit, so instead of a compile time error you are getting a runtime error 424 when the code implicitly assumes it's Dim cellrange As Variant which was never initialized and is therefore Empty.

To fix the problem, you need to tell the instance of the user for which instance of the event-handling class it should get the properties from. For that it would be enough to put this in the UserForm:

Private m_ParentClass As ThatClassThatCreatesForms

Friend Sub Init(ByVal p As ThatClassThatCreatesForms)
  Set m_ParentClass = p
End Sub

and change the For Each loop in the parent class as:

For each cell in Target
    ReplaceTask.Init Me
    ReplaceTask.Show
Next cell

You have to have a separate "Init" method because VBA classes cannot have constructors with parameters.

Then the code in ReplaceTask can use m_ParentClass.cell, m_ParentClass.Workbook etc. But you cannot do that from UserForm_Initialize because Init has not been called yet. It is not a problem though, simply move the code from UserForm_Initialize into Init.


To take it one step further, I would advice that you stop are using the implicit form instance. It is a good practice to create the instances manually:

For each cell in Target
    Dim f As ReplaceTask
    Set f = New ReplaceTask
    f.Init Me
    f.Show
Next cell
GSerg
  • 76,472
  • 17
  • 159
  • 346
  • And how gets `ThatClassThatCreatesForms` initially instantiated? Where is the class's workbook property set, so the `Sub m_wb_SheetChange` is able to work at all? That also not needs to be global? – Axel Richter Feb 07 '19 at 12:53
  • Thank you for this, especially the last comment is what intrigues me as I have never known this was another way (and a better one) of launching userforms. I'll apply this method of some of the other userforms I am using in this project, as I did encounter some ugly issues with the regular `userform.show` function. I would accept both answers if I could! – Tim Stack Feb 07 '19 at 12:55
  • @AxelRichter I have no idea where `ThatClassThatCreatesForms` is, and it does not matter because the OP already has it instantiated, which is a prerequisite for its `m_wb_SheetChange` to run. And no, that instance does not have to be global, it can be private. – GSerg Feb 07 '19 at 12:55
  • 1
    @TimStack Creating global variables works (hence is a solution), but it is [not recommended](https://stackoverflow.com/q/484635/11683). – GSerg Feb 07 '19 at 12:57
  • As your method allows for using all variables in the class module, I have accepted your answer – Tim Stack Feb 07 '19 at 13:24
  • @TimStack A global variable also allows to use all member the class exposes, my method is no better in this regard. It is better in the sense that you don't have to create a global variable. – GSerg Feb 07 '19 at 13:27
  • Understood. I am also getting a `User-defined type not defined` compile error on the `Private m_ParentClass As ThatClassThatCreatesForms`. – Tim Stack Feb 07 '19 at 13:29
  • @TimStack Because you need to replace `ThatClassThatCreatesForms` with the name of your class that contains the event handler. – GSerg Feb 07 '19 at 13:30
  • Ah.. Should have known. This whole class method is really confusing as I mended it with an example I found months ago online and never bothered to read into it. Thanks to both of you for the help, got it all working now. – Tim Stack Feb 07 '19 at 13:33