As discussed in "Understanding Control Lifetime and Key Events," earlier in this chapter, instances of controls are continually being created and destroyed — when form designers are opened and closed, when projects are opened and closed, when projects are put into run mode, and so on.
How does a property of a control instance — for example, the Caption property of a Label control — get preserved through all this destruction and re-creation? Visual Basic stores the property values of a control instance in the file belonging to the container the control instance is placed on; .frm/.frx files for forms, .dob/.dox files for UserDocument objects, .ctl/.ctx files for UserControls, and .pag/.pgx files for property pages. Figure 9.13 illustrates this.
Figure 9.13 An .frm file contains saved control properties
When you author a control, you must include code to save your property values before a control instance is destroyed, and read them back in when the control instance is re-created. This is illustrated in Figure 9.14.
Figure 9.14 Saving and retrieving property values
Figure 9.14 is slightly oversimplified. You don't actually have to close a form to cause the WriteProperties event procedures of control instances to be executed. Saving a form file to disk causes WriteProperties events to be raised for all controls on the form.
Tip This topic explains the mechanism for saving and retrieving property values in code, but you won't normally have to write all the code described here. The ActiveX Control Interface Wizard can generate most of the code to save and retrieve your property values.
Use the PropertyBag object to save and retrieve property values. The PropertyBag is provided as a standard interface for saving property values, independent of the data format the container uses to save its source data.
The following code example uses the Masked property, a Boolean property described in the related topic "Adding Properties to Controls."
Private Sub UserControl_WriteProperties(PropBag As _
PropertyBag)
' Save the value of the Boolean Masked property.
PropBag.WriteProperty "Masked", Masked, False
' . . . more properties . . .
End Sub
The WriteProperty method of the PropertyBag object takes three arguments. First is a string that identifies the property being saved, followed by value to be saved — usually supplied by accessing the property, as shown above.
Note If the value you're saving is contained in the default property of a constituent control, and that property is of type Variant — for example, the Text property of a text box — you must specify the property name. That is, you must use Text1.Text
instead of just Text1
.
The last parameter is the default value for the property. In this case, the keyword False is supplied. Typically, you would create a global constant, such as PROPDEFAULT_MASKED, to contain this value, because you need to supply it in three different places, in the WriteProperties, ReadProperties, and InitProperties event procedures.
It may seem strange, at first, to be supplying a default property value when you're saving the value of a property. This is a courtesy to the user of your control, because it reduces the size of the .frm, .dob, .pag, or .ctl file belonging to the container of the control.
Visual Basic achieves this economy by writing out a line for the property only if the value is different from the default. Assuming that the default value of the Masked property is False, the WriteProperty method will write a line for the property only if the user has set it to True.
You can easily see how this technique reduces the size of .frm files by opening a new Standard EXE project, adding a CommandButton to Form1, and saving Form1.frm. Use a text editor such as Notepad or Wordpad to open Form1.frm, and compare the number of properties that were written to the file for Command1 to the number of properties in the Properties window for Command1.
Wherever possible, you should specify default values for the properties of your control when initializing, saving, and retrieving property values.
Property values are retrieved in the ReadProperties event of the UserControl object, as shown below.
Private Sub UserControl_ReadProperties(PropBag As _
PropertyBag)
On Error Resume Next
' Retrieve the value of the Masked property.
Masked = PropBag.ReadProperty("Masked", False)
' . . . more properties . . .
End Sub
The ReadProperty method of the PropertyBag object takes two arguments: a string containing the name of the property, and a default value.
The ReadProperty method returns the saved property value, if there is one, or the default value if there is not. Assign the return value of the ReadProperty method to the property, as shown above, so that validation code in the Property Let statement is executed.
If you bypass the Property Let by assigning the property value directly to the private data member or constituent control property that stores the property value while your control is running, you will have to duplicate that validation code in the ReadProperties event.
Tip Always include error trapping in the UserControl_ReadProperties event procedure, to protect your control from invalid property values that may have been entered by users editing the .frm file with text editors.
If you create a property the user can set at design time, but which is read-only at run-time, you have a small problem in the ReadProperties event. You have to set the property value once at run time, to the value the user selected at design time.
An obvious way to solve this is to bypass the Property Let, but then you have no protection against invalid property values loaded from source files at design time. The correct solution to this problem is discussed in "Creating Design-Time-Only, Run-Time-Only, or Read-Only Run-Time Properties."
You can assign the initial value of a property in the InitProperties event of the UserControl object. InitProperties occurs only once for each control instance, when the instance is first placed on a container.
Thereafter, as the control instance is destroyed and re-created for form closing and opening, project unloading and loading, running the project, and so on, the control instance will only receive ReadProperties events. This is discussed in "Understanding Control Lifetime and Key Events," earlier in this chapter.
Be sure to initialize each property with the same default value you use when you save and retrieve the property value. Otherwise you will lose the benefits that defaults provide to your user, described in "The Importance of Supplying Defaults," earlier in this topic.
Tip The easiest way to ensure consistent use of default property values is to create global constants for them.
Except for standard objects, such as Picture, the ReadProperty and WriteProperty methods have only limited ability to store and retrieve binary data. Storing binary data in strings is problematic because of Unicode conversion and line length limitations in the .frm file, but the two methods will accept arrays of type Byte.
If you're already keeping the data for a property in a Byte array, you can save it by passing the Byte array to the Value argument of the WriteProperty method:
' Private storage for the Blob property.
Private mbytBlob(0 To 1023) As Byte
Private Sub UserControl_WriteProperties(PropBag As _
PropertyBag)
' Save the binary data for the Blob property.
PropBag.WriteProperty "Blob", mbytBlob
' . . . more properties . . .
End Sub
The data will be saved in the .frx file. When retrieving the data, assign the return value from the ReadProperty method to mbytBlob
. If mbytBlob
is of variable length, you must save its size as a separate entry, using a name such as "BlobSize," so that you can retrieve the size and ReDim the array before retrieving the data. (You do not need to have a BlobSize property in order to do this.)
You can also use this technique to store any binary property data that you can manage to copy to a Byte array.