PDA

View Full Version : Effectively using Classes and Property Let/Get; referring to parent or sub class...



GTO
06-28-2011, 02:45 AM
Greetings and Salutations,

Preface:

While designing a UserForm (hereafter, form), I experienced an issue with the calendar control (shocked,eh?). I noted that when going from an older (2000) App version to a newer App version (2003), the buttons (or at least the font on the buttons, I forget which) became incredibly small. I would note that I think I still was in Word, but I do not see that having any bearing.

Hence – I decided it was time to tackle something a tad bigger using class module(s) than I had heretofore. I decided to build myself a handy-dandy pseudo ‘calendar control,’ which is of course, something like 52 controls to replicate the Calendar control… Stop giggling.

Code Goal:

I would like to improve upon what I have done so far, and hopefully improve my understanding of when Set/Let/Get are advantageous, as well as efficiently referencing child/sub classes and parent classes. I admit that I may be using those terms incorrectly, but hope I am close.

After this thread, I am hoping to be able to add a couple of/few ‘stock’ classes to a project; a few lines of code to a form, and viola – a pseudo calendar control would be usable. Again, stop giggling; I can do it!

Reference the pseudo calendar control, or PCal for short – while I have seen several of examples of creating a calendar on a form (there are very nice examples in our KB), I was thinking a bit differently; something that might be easily sizeable, visible = true/false and such. That is not to say that such examples do not exist; as King Solomon mentioned, there is nothing new under the Sun. Regardless, I have not run into any.

I’m rather happy with what I’ve mustered so far, but at this point, am looking forward to hearing, “Yikes! You should do this in this part, and here’s why…”

Question:

Specific to a form, when the Initialize event creates an instance of a class, and we need the created class to be able to refer to the form:
Why is not the passed reference able to access all of the properties of the form? For instance (and being the bright lad I am, I’ll bet that this is important), the .Name property is gone?
Does the below seem a reasonable method, or is there a better way (see the bottom of ThisPButton_Click in cls_pcalPButton in the attachment)?Private Sub ThisPButton_Click()
'...statements....
n = -1
For Each UserForm In UserForms
If UserForm.Name = PCalParentForm.Name Then
n = n + 1
UserForms(n).PCalDateLong = CDate(cPCal.DateLongPicked)
Call UserForms(n).AssignDate(CDate(cPCal.DateLongPicked))

If cPCal.AssignReturnToControl Then
cPCal.PushDateToCtrl cPCal.Caller, VbLet, cPCal.DateLongPicked
End If


'***TESTING***
UserForms(n).Caption = Space(2) & UCase(Format(CDate(cPCal.DateLongPicked), "dd mmm yyyy"))
Exit For
End If
Next
End Sub

Due to the time here, I will have to check back tonight, but have been meaning to post (where does the day fly to so quickly?).

Thank you so much,

Mark

Bob Phillips
06-28-2011, 04:24 AM
Why is not the passed reference able to access all of the properties of the form? For instance (and being the bright lad I am, I’ll bet that this is important), the .Name property is gone?

I don't understand, why can't you access these properties? Have you tried using the Properties collection?

GTO
06-29-2011, 12:17 AM
I don't understand, why can't you access these properties?
Hi Bob, I hope your trip was a good one :-)

Well, I may have worded that poorly. The form's Name is but one property that doesn't seem to show up in intellisense, but I thought it a good example as it actually falls down if used.

By example - in a new wb, if I create a new userform and insert code:
Option Explicit

Dim cButtons As clsCreatedButtons

Private Sub UserForm_Initialize()

Set cButtons = New clsCreatedButtons
Set cButtons.ReferencedForm = Me

cButtons.ReferencedFormName = Me.Name
cButtons.CreateButton
End Sub And in the Class:
Option Explicit

Private CallingForm As MSForms.UserForm
Private WithEvents MyButton As MSForms.CommandButton
Private strFormName As String

Property Set ReferencedForm(UForm As UserForm)
Set CallingForm = UForm
End Property
Property Get ReferencedForm() As UserForm
Set ReferencedForm = CallingForm
End Property

Property Let ReferencedFormName(UFormName As String)
strFormName = UFormName
End Property
Property Get ReferencedFormName() As String
ReferencedFormName = strFormName
End Property

Function CreateButton()
Set MyButton = ReferencedForm.Controls.Add("Forms.CommandButton.1", "cmdBttn_1", True)
With MyButton
.Left = 20
.Top = 20
.Width = 64
.Height = 18
End With
End Function

Private Sub MyButton_Click()
'With property ReferencedForm and variable CallingForm both declared As UserForm...
CallingForm.Caption = "Hello"
'...plunks "Hello" onto the userform; but it's BELOW the titlebar!

'Also, I can get to the userform's name in a roundabout fashion, via...
CallingForm.Caption = CallingForm.ActiveControl.Parent.Name
'... (which of course still ends up below the titlebar), but if I try and use .Name...
CallingForm.Caption = CallingForm.Name
'... I get error 438, "Object doesn't support this property or method.
End Sub On the other hand, if I declare Private CallingForm As Object, as well as change it to As Object in the Property Set and Get, then when I step through (having already REM'd CallingForm.Caption = CallingForm.ActiveControl.Parent.Name) "Hello" shows up properly in the form's titlebar and everything is sunshine and flowers with CallingForm.Name.

I would mention that I have a bit of a grip (albeit loosely) on some events not being available when a given activex control is created in a class, though I would not pretend as to know the exact 'whys'. I was figuring certain properties not being available might be similar, but am confused as to why if I declare as Object, certain things work (like .Name), whereas if As UserForm, it goes Kaboom!

Similarily (and presuming I set the form's StartUpPosition to 0 during Initialize), then if it is passed to the class As Object, I can change the .Left and .Top properties, and watch it move. But As UserForm causes the same falling down.

Again confusing is that when As UserForm, setting the the form's Caption results in the string appearing below the titlebar! I hope that I am not missing something really simple and that I worded that a bit better.


Have you tried using the Properties collection?
Do you mean like ThisWorkbook.VBProject.VBComponents("UserForm1").Properties("Name").Value ?

Thank you,

Mark

GTO
06-30-2011, 12:36 AM
Do you mean like ThisWorkbook.VBProject.VBComponents("UserForm1").Properties("Name").Value ?

Thank you,

Mark

ACK! Obviously not, as the Properties property is not happening while the form is loaded. :dunno

Aflatoon
07-05-2011, 03:10 AM
The Userform class/interface is a generic one from which other userforms inherit. You cannot create a new instance of Userform (only Userform1 or whatever you have called your form) and hence it does not have properties such as Name or Top or Left, as they don't make sense. You should pass it as Object.

GTO
07-06-2011, 01:28 AM
Hi Aflatoon,

I am afraid I have run myself out of time today, but wanted to relay a thank you for your answer. Hopefully tomorrow is "smoother"...

Have a great day,

Mark

GTO
07-08-2011, 01:17 AM
Greetings All,

Okay, while I may not have my head totally wrapped around the why we must pass the form as an object, I believe it is making enough sense to me to proceed.

At the end of post #1, I asked as to whether using the userform collection would be a good way of getting a value passed back to a variable declared in the form's module. But, I think I have made progress and this would (hopefully) be better.

In the form's module:
Option Explicit

Dim cButton As clsButton
Dim lValue As Long

Property Let ValFromClass(SentVal As Long)
lValue = SentVal
End Property
Property Get ValFromClass() As Long
ValFromClass = lValue
End Property

Private Sub CommandButton1_Click()
Me.TextBox1.Tag = vbNullString
Me.TextBox1.Value = vbNullString
ValFromClass = 0
End Sub

Private Sub TextBox1_Change()
Me.Label1.Caption = Me.TextBox1.Tag
End Sub

Private Sub UserForm_Initialize()

With Me
.Height = 180
.Width = 240
.Caption = vbNullString
With .Label1
.BorderStyle = fmBorderStyleSingle
.Caption = vbNullString
.Left = 12
.Top = 6
.Height = 16
.Width = 72
End With
With .TextBox1
.Left = .Parent.Label1.Left + .Parent.Label1.Width + 12
.Top = 6
.Height = 16
.Width = 72
.Value = vbNullString
End With
End With

Set cButton = New clsButton
Set cButton.ParentForm = Me
cButton.AddButtons
cButton.CallerName = Me.TextBox1.Name
cButton.CustomPropName = "ValFromClass"
End Sub
In the Class (clsButton):
Option Explicit

Private WithEvents Bttn_1 As MSForms.CommandButton
Private WithEvents Bttn_2 As MSForms.CommandButton
Private WithEvents Bttn_3 As MSForms.CommandButton

Private oForm As Object
Private strCallerName As String
Private strCustomPropName As String

Property Set ParentForm(frm As Object)
Set oForm = frm
End Property

Property Let CallerName(s As String)
strCallerName = s
End Property

Property Get CallerName() As String
CallerName = strCallerName
End Property

Property Let CustomPropName(s As String)
strCustomPropName = s
End Property

Function AddButtons()

Set Bttn_1 = myForm.Controls.Add("Forms.CommandButton.1", , True)
With Bttn_1
.Top = 34
.Height = 22
.Left = 6
.Width = 142
.Caption = "CallByName to userform control"
End With
Set Bttn_2 = myForm.Controls.Add("Forms.CommandButton.1", , True)
With Bttn_2
.Top = 34 + 22 + 6
.Height = 22
.Left = 6
.Width = 142
.Caption = "CallByName to userform variable"
End With
Set Bttn_3 = myForm.Controls.Add("Forms.CommandButton.1", , True)
With Bttn_3
.Top = 34 + 22 + 6 + 22 + 6
.Height = 22
.Left = 6
.Width = 142
.Caption = "(return stored val if present)"
End With
End Function

Private Sub Bttn_1_Click()

'// Method 1: Use Property Get within same module.
CallByName oForm.Controls(CallerName), "Tag", VbLet, CLng(Date - 1)
'// method 2: Use the module level variable when wishing to return the current value and
'// within the same module.
CallByName oForm.Controls(strCallerName), "Value", VbLet, Format(Date - 1, "dd mmm yyyy")
End Sub

Private Sub Bttn_2_Click()
'// Set the value of a variable within the userform's module, by adding a Property Let
'// in the userform's module.
CallByName oForm, strCustomPropName, VbLet, Date + 1
End Sub

Private Sub Bttn_3_Click()
'// Just to demonstrate the return value stored in userform variable, if present)
If Not StrComp(CDate(CallByName(oForm, strCustomPropName, VbGet)), "12:00:00 AM") = 0 Then
oForm.Caption = CDate(CallByName(oForm, strCustomPropName, VbGet))
Else
oForm.Caption = "No val set"
End If
End Sub
Please see the small example workbook attached.

Presuming (and hoping...: pray2: ) that I am using CallByName effectively, I have a question towards Let/Get. In the first button's click event, you will see that I first referred to the property (method 1) to return the stored value, whereas in the second CallByName I simply referred directly to the variable.

While I understand the advantage of using the property (Get) from outside the module, to me, it would seem quicker to refer directly to the variable if we are in a procedure in the same module. Am I being sensible in my conclusions?

Aflatoon
07-08-2011, 02:04 AM
A couple of observations:
1. Your class refers to myForm when adding controls, not to oForm, which violates encapsulation.
2. I would generally recommend using properties rather than direct variable access, for much the same reason you created the properties in the first place:
if you decide to change implementation of a property, you may have to rewrite all your code;
if your property procedures do any sort of validation, it's a lot easier to just use that same code rather than having to validate the variable values yourself every time the class uses them internally;
it's an easy guarantee that any internal operations you do would produce the same results as if a consumer of your class did them using the same properties.
3. For this example, I don't really see why you wouldn't implement the whole thing as a form (which can then be called from anywhere) rather than a class that can only be called from a form anyway.

mikerickson
07-08-2011, 06:15 PM
A couple of observations.

1) A userform has been described as a custom class with attached interface.
Thus (if it is to inherit properties) a variable that refers to a userform must be declared either As Object or As Userfom1, not as msForms.Userform.

2) When creating classes that refer to controls on a userform, I find it useful to either have a Parent property, either explicitly set when the custom object is defined or a property like
'in class module
Dim myCheckbox as msForms.Checkbox
'...
Property Get ufParent() As Object
myParent as Object
Set myParent = myCheckBox
Do until Typename(myParent.Parent) = myParent.Name
Set Parent = myCheckbox.Parent
Loop
set ufParent = myParent
End Property
The Do loop is to handle situations where myCheckbox is in a Frame or other control.

GTO
07-09-2011, 01:13 AM
A couple of observations:

1. Your class refers to myForm when adding controls, not to oForm, which violates encapsulation.

ACK! Sorry about that Aflatoon, my utter bad. I was going too quick, used intellisense and forgot to change it.


2. I would generally recommend using properties rather than direct variable access, for much the same reason you created the properties in the first place:
if you decide to change implementation of a property, you may have to rewrite all your code;

Errrrr... droool... slobber... :stars: Okay, I'm not quite grasping this part at the moment, but I certainly accept it.


if your property procedures do any sort of validation, it's a lot easier to just use that same code rather than having to validate the variable values yourself every time the class uses them internally;
it's an easy guarantee that any internal operations you do would produce the same results as if a consumer of your class did them using the same properties.

Ahh! thank you, and I believe that I am grasping these points (at least loosely :) ).


3. For this example, I don't really see why you wouldn't implement the whole thing as a form (which can then be called from anywhere) rather than a class that can only be called from a form anyway.

I am not quite sure, but I presume you mean the workbook at post #1? As my goal is a pseudo calendar, at least as I imagine it, I would not be calling it except from a userform. I thought a moment about using a second form, but it seemed to me that I would facing the same learning curve as to passing the value from the second form to the first, as I am here, in passing the value from the created controls. As mentioned, CallByName seems like a neat thing, once I figured out exactly what I needed to supply for the arguments. (I am afraid I have had only limited time to study this, and can be a bit thick-headed sometimes...)

Further - whilst certainly only my subjective opinion, I am at least thus far, not seeing the users being able to move the pseudo-calendar hither and yon as advantageous. I am sure my efforts are very rough thus far, and appreciate your patience in reading what is by no means the 'prettiest' bit of coding.


A couple of observations...

Howdy Mike!

I am about done in, but I have copied your code and observations.

Hopefully I get at least a little time this weekend and a chance to post back on Monday.

Certainly my continued apprecation for everyone's help, and a great weekend to all :-)

Mark

Aflatoon
07-11-2011, 05:39 AM
Mark,
Would it not be easier to create a Calendar form that you can call from say a worksheet too as well as from a form? It makes everything a lot easier - you simply expose the return values as properties of your form so the calling code just creates an instance of the form and shows it (modally, so the code pauses) and then when the calendar is closed (you'd want to hide rather than unload it), the calling code resumes by reading its properties and then unloads it. Roughly:


Dim frm as CalendarForm
Set frm = new CalendarForm
frm.Show
If Not frm.Cancelled Then me.textbox1.value = frm.DateSelected
set frm = nothing


where your form implements a simple Boolean Cancelled property and a DateSelected property for the return value.