>



Chapter 18

The Extender and Ambient Objects


CONTENTS

If I'm doing my job right, by now you should be beginning to feel comfortable with the way ActiveX controls fit together. You should understand the different states in which a control exists. You should be familiar with the main events in the life of a control and a few of the most important methods and properties.

Chapter 19, which deals with the way you define properties and events in your control, is going to be extremely important when it comes to implementing your controls. You can think of this chapter, perhaps, as a long introduction to the next one. It's a chapter in which we'll introduce the rest of the fundamental building blocks on which a control is based. For if the UserControl object forms the core of your ActiveX control, then surely the Extender and Ambient objects define the environment in which it lives.

The Extender Object

Most of the properties of the Extender object are already familiar to you-they are found in almost every Visual Basic form or control. The purpose of this section is not to tell you about those properties or how they are used from the developer's perspective. Instead, the purpose is to show you how to deal with them from a control author's perspective.

To begin, try creating a new control, closing its designer, and adding it to a form. Do not add any properties-this is a completely empty control.

When you look at the properties of the control in the container's design time, depending on the property settings of the UserControl object, you will see the properties shown in the following table

.

Extender Properties for an Empty ActiveX Control Comments
Name Standard
Align Only appears if the control's Alignable property is True
Cancel Standard-only appears if the control's DefaultCancel property is True
DataBinding
DataChanged
DataField
DataSource
Only appears if the control has a property that is bound to a database. See Chapter 19.
Default Standard-only appears if the control's DefaultCancel property is True
DragIcon

 

DragMode

 

Enabled Only appears if the control has an Enabled property-more on this later
Height

 

HelpContextId Only appears if the control's CanGetFocus property is True
Index

 

LeftStandard
Negotiate Only appears if the control's Alignable property is True
TabIndexOnly appears if the control's CanGetFocus property is True
TabstopOnly appears if the control's CanGetFocus property is True
Tag 
ToolTipText 
Top 
VisibleStandard
WhatsThisHelpID 
WidthStandard

The following items about this table should be noted.

In addition to these properties, Visual Basic provides the following Extender properties that can be accessed by a developer at runtime.

Runtime-Only Extender Properties, Methods, and Events Comments
Container propertyThe container (typically a form) that contains the control
Object propertyReferences the underlying object, not the extender
Parent propertyStandard-the container of the control. The UserControl object also provides a Parent property, which is always available regardless of the container.
Drag method 
Move method 
SetFocus method 
ShowWhatsThis method 
Zorder method 
DragDrop event 
GotFocus eventOnly appears if control's CanGetFocus property is True
LostFocus eventOnly appears if control's CanGetFocus property is True
DragOver event 

From the point of view of the developer (the person using your control), there is no difference between Control properties and Extender properties. They appear as a single interface. How is this possible? When you, as a developer, reference a control, you are in fact referencing the control's extender. The Extender object reflects the properties and methods of the object itself. This is shown in Figure 18.1.

Figure 18.1 : The relationship between the control and the extender.

There are some things you must keep in mind with respect to the Extender object:

The ch18tst1.vbg program group contains two projects: ch18ctls.vbp project contains a number of controls that demonstrate the techniques described in this chapter. The ch18tst1.vbp project is a test program that exercises those controls.

The ch18ctla control is completely empty except for two functions that allow you to access the control's internal Extender and Me objects using the following code:

Public Function InternalExtender() As Object
   Set InternalExtender = UserControl.Extender
End Function

Public Function InternalMe() As Object
   Set InternalMe = Me
End Function

You should not generally expose these objects in this manner, but it is useful for test purposes. Place the control on a form, add a command button, and add the following code to its Click event:

Private Sub Command1_Click()
   If ch18CtlA1.Container Is ch18CtlA1.Parent Then
      Debug.Print "Parent and container are the same"
   Else
      Debug.Print "Parent and container are not the same"
   End If
   If ch18CtlA1 Is ch18CtlA1.Object Then
      Debug.Print "Control and control's Object are the same"
   Else
      Debug.Print "Control and control's Object are different"
   End If
   If ch18CtlA1.Object Is ch18CtlA1.InternalMe Then
      Debug.Print "Extender's object property is the control's Me"
   Else
      Debug.Print "Extender's object property is not the control's Me"
   End If

   If ch18CtlA1 Is ch18CtlA1.InternalExtender Then
      Debug.Print "Control is the control's extender"
   Else
      Debug.Print "Control is not the control's extender"
   End If
End Sub

The results are as follows:

From this, we can generate Table 18.1, which shows how to access the various properties as both a control author and a developer using the control.

Table 18.1: Navigating between Control, Container, and Extender

ObjectAccess as a Developer Access as a Control Author
Control myctl Extendermyctl Usercontrol.Extender
Control myctl object itself (not the extender) myctl.ObjectMe
Container of control "myctl" myctl.Parent
myctl.Container
UserControl.Extender.Container
UserControl.Extender.Parent
UserControl.Parent

Impact of Scoping Rules

You are already quite familiar with the general rules for scoping of variables, properties, and methods. When you define an item with the same name as an item that exists at a higher level, the other item is hidden. Thus if you have a global variable named x and define x in a procedure, the global one is hidden until the procedure exits.

The same principle applies with Control properties as they relate to Extender properties. For example: say you add a Tag property to your control. The VB extender already has a Tag property that it manages. Since the developer accesses the extender, that property takes priority. This is demonstrated in the ch18tst1.vbp project with the ch18ctlb.ctl control. This control implements its own Tag property and member variable using the following code:

Public Property Get Tag() As String
   Tag = m_Tag
End Property

Public Property Let Tag(ByVal vNewValue As String)
   m_Tag = vNewValue
   Debug.Print "Setting internal tag"
   Report
End Property

Public Sub Report()
   Debug.Print "Internal Tag variable is " & m_Tag
   Debug.Print "Internal Tag property is " & Tag
   Debug.Print "Extender Tag is " & Extender.Tag
End Sub

The test code in form frmch18test1 is attached to a second command button as follows:

Private Sub Command2_Click()
   ch18CtlB1.Tag = "Setting tag Property"
   ch18CtlB1.Object.Tag = "Setting object tag property"
End Sub

The results are as follows:

Setting internal tag
Internal Tag variable is Setting object tag property
Internal Tag property is Setting object tag property
Extender Tag is Setting tag Property

From this we can conclude that there are two separate Tag variables-one implemented and belonging to the control, the other by the container. You can access these variables as shown in Table 18.2.

Table 18.2: Scoping Example with Tag Property for ch18CtlB1

Tag Being AccessedAccess As Developer Access As Author
Extender Tag property managed by Visual Basic ch18CtlB1.TagUserControl.Extender.Tag
Internal Tag property managed by your control ch18CtlB1.Object.TagTag Me.Tag

Why would you ever want to define a property, such as Tag, which is already implemented by the container? Because you may want to guarantee that people using your control have access to a Tag property for the control, and you cannot be sure that your control's container will always support a Tag property on its extender.

Accessing Extender Properties

The biggest catch to using Extender properties is that you can never be certain whether or not they are actually present. This means that unless you want your control to be limited to Visual Basic or a subset of containers, you must take precautions when accessing Extender properties so your control degrades gracefully. There are two approaches for doing this.

The error trapping approach is obvious. The second approach can be handled in two ways. You can create a routine that attempts to access a property and records the results. The following code from the ch18ctlb.ctl control shows how this can be accomplished:

Private TagIsPresent As Boolean
Private xyzIsPresent As Boolean
Private zzzispresent As Boolean
Private Sub CheckExtender()
   Dim testnumber As Integer
   Dim v As Variant
   TagIsPresent = True
   xyzIsPresent = True
   zzzispresent = True
   On Error GoTo itp1
   testnumber = 0
   v = Extender.Tag
   testnumber = 1
   v = Extender.xyz
   testnumber = 2
   v = Extender.zzz
   Exit Sub
itp1:
   Select Case testnumber
      Case 0
         TagIsPresent = False
      Case 1
         xyzIsPresent = False
      Case 2
         zzzispresent = False
   End Select
   Resume Next
End Sub

The routine simply checks the desired properties one by one, setting a module level flag to indicate which ones fail. A simpler version of this routine could use a Boolean array, where each entry corresponds to a particular Extender property.

An even easier approach uses the apigid32.dll dynamic link library included with this book. This DLL was written originally for the Visual Basic Programmer's Guide to the Win32 API and contains a variety of useful low-level functions, one of them being the function called agIsValidName. The function is declared as follows:

Declare Function agIsValidName& Lib "apigid32.dll" (ByVal o As Object, ByVal_ lpname$)

Its use is demonstrated in the following code from the ch18ctlb.ctl control:

Debug.Print "Tag property present: " & agIsValidName(Extender, "Tag")
Debug.Print "xyz property present: " & agIsValidName(Extender, "xyz")

This function is actually quite simple internally. It simply calls the GetIDsOfNames method on the object's dispatch interface to see if it gets a valid return value. As you may recall from Part 1 of this book, Visual Basic 5.0 requires that all interfaces it uses be either dispatch interfaces or dual interfaces that contain the dispatch interface functions along with those you define.

Which approach should you take? It's really up to you. I tend to use the latter because most of the controls I create take advantage of Desaware's tools (which is not surprising, since those tools were created primarily for me and my staff to be able to create more powerful controls in the first place). Since we're distributing apigid32.dll anyway (or one of its big brothers, which also contain the IsValidName function), there is no additional cost in terms of resources or performance in using the DLL function.

Control Dependencies

The Extender properties that are made available for a particular control may depend on settings in the control itself. These dependencies are entirely up to the container. The following are the most important of these dependencies that you should be aware of as a Visual Basic programmer.

Enabled

The Enabled property is strange. To see why, load the EnTest1.vbp project in the Chapter 18 samples directory on your CD-ROM. This project contains a single control called EnCtl1.ctl, which contains a command button and a public Enabled property. The code for this control is as follows:

Option Explicit

Public Property Get Enabled() As Boolean
   Enabled = UserControl.Enabled
End Property

Public Property Let Enabled(ByVal New_Enabled As Boolean)
   UserControl.Enabled() = New_Enabled
   PropertyChanged "Enabled"
End Property

'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
   UserControl.Enabled = PropBag.ReadProperty("Enabled", True)
End Sub

'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
   Call PropBag.WriteProperty("Enabled", UserControl.Enabled, True)
End Sub

This code should be quite familiar to you by now-it is the standard way in which a public property is implemented and persisted. The UserControl object's internal Enabled property is set according to the public variable. A Report function has been added to the control. It prints the state of the Enabled property, the UserControl enabled property, and the Command1 enabled property to the Immediate window as follows:

Public Sub Report()
   Debug.Print "Within control code - "
   Debug.Print "Enabled property is: " & Enabled
   Debug.Print "UserControl.Enabled property is: " & _
   UserControl.Enabled
   Debug.Print "Command1.Enabled is: " & Command1.Enabled
   Debug.Print "Extended Enabled is: " & Extender.Enabled
   Debug.Print "End within control code."
End Sub

The project contains a single form, enForm1, which contains a timer and the EnCtl1 control. During the Timer control's Timer event, the following trace routine is called:

Private Sub Timer1_Timer()
   Debug.Print "Enabled property is: " & Enabled
   Debug.Print "Control's Enabled property is: " & _
   EnableTestControl.Enabled
   Debug.Print "Report: ";
   EnableTestControl.Report
End Sub

The EnCtl1 control has two properties set in the VB property window for the form: The Enabled property is set to True, and the Name property is set to EnableTestControl. The Enabled property of the form is set to False.

Now, before you run this sample, let's take a moment and review all of the different Enabled properties we are actually dealing with. In the control (control author's point of view) the properties are these:

SyntaxWhat You Are Accessing
EnabledThe public Enabled property for the control.
UserControl.EnabledThe UserControl object Enabled property.
Command1.EnabledThe Enabled property of the constituent command button on the control.
Extender.EnabledThe Enabled property provided by the container as part of the Extender object. Due to the scoping rules described earlier, this is the property seen by developers using the control unless they explicitly access the control's Enabled property using the Object property and the syntax EnableTestControl.Object.Enabled.

From the container (application developer's point of view):

SyntaxWhat You Are Accessing
EnabledThe Enabled property for the form (actually, the Enabled property for the form's Extender object).
EnableTestControl. EnabledThe Enabled property provided by the container as part of the Extender object for the control.
EnableTestControl. Object.EnabledThe public Enabled property for the control. (Developers should never access this property.)

When you run the application, you will see the following displayed in the Debug window.

Enabled property is: False
Control's Enabled property is: False
Report: Within control code -
Enabled property is: True
UserControl.Enabled property is: True
Command1.Enabled is: True
Extended Enabled is: False
End within control code.

The Enabled property for the form is disabled. This makes sense because that is what was set for the form in the VB Property window.

The control's Enabled property, which is on the Extender object, is False. Keep in mind that it was set to True at design time. But the public Enabled property for the control, along with that of the UserControl object and the command button within the control, are all True! What's going on? Why is the control's Extender Enabled property False while the Control properties are actually enabled?

This occurs because of a convention specific to Visual Basic. First, consider how Windows works. When you disable a window, all of its child windows continue to appear as enabled. They will not receive keystrokes or mouse input because their parent form is disabled. If you want them to appear as disabled, you must disable them individually. This is similar to the situation we saw in Chapter 17 with regards to visibility, where controls on a form can have their Visible properties set to True, yet they are obviously hidden when their container is hidden.

In this example we disabled the form but did not disable the control. The developers of Visual Basic, for reasons that are probably lost in history and certainly undocumented, decided that when a container is disabled, the controls it contains should read as disabled as well (even though they have not been set explicitly to disabled). They could not, however, just disable the controls. Doing so would change their appearance, violating the standard behavior of Windows applications. So they let the controls remain enabled (as far as they were concerned), but rigged it so the Enabled property on the Extender object for controls on a disabled container would return False.

Now, in order for this trick to work, there is one important requirement. There has to be a way to let Visual Basic know which property is the Enabled property. It's not enough to assume that the property will actually be named Enabled-the control author may choose to localize the property name for different languages or use a different term entirely. How can you, as a control author, let Visual Basic know which of your properties is the Enabled property for the control?

The solution lies in one of the characteristics of the IDispatch interface discussed back in Part 1 of this book. Remember how each property in the interface has a unique dispatch identifier number (Dispatch ID or DispID)? You may also recall that negative Dispatch ID numbers are defined by COM to indicate certain standard properties. These values do not affect the behavior of the interface or property itself but may be used by a container to provide special handling for properties if it chooses to do so. Dispatch ID number -514 is the identifier number for the Enabled property.

You can use the VB Tools, Procedure Attributes menu command to bring up the Procedure Attributes dialog box. Select the Enabled property and click the Advanced button. In the Procedure ID combo box you will see that Enabled is selected. When the Enabled procedure ID is selected, Visual Basic automatically sets the Dispatch ID for the property to -514.

When you create a blank control with no properties, Visual Basic does not create an Enabled property on the Extender object by default. However, if you assign any of the control's properties to the Enabled procedure ID, Visual Basic will add an Enabled property to the extender for the control. When you set the Enabled property using code or the VB property window, Visual Basic will also set the property that you specified with this ID, regardless of the actual name of the property.

What happens if you add an Enabled property to the control but do not set the Procedure ID for the property to Enabled? Visual Basic does not create an Enabled property on the Extender! The Enabled property you see in the VB Property window for the control is the control's Enabled property.

If you set the Procedure ID for the EnCtl1.ctl control to (None) and run the program again, you will see the following debug messages in the Debug window (you may need to save and reload your project for the system to reset itself properly after this change):

Enabled property is: False
Control's Enabled property is: True
Report: Within control code -
Enabled property is: True
UserControl.Enabled property is: True
Command1.Enabled is: True
Extended Enabled is: True
End within control code.

As you can see, there is no difference between the Enabled property as seen by the developer using the control and that seen by the control's author.

To conclude: If you want your control to exhibit the standard Visual Basic behavior for the Enabled property, you must do the following:

Align and Negotiate

If you set the Alignable property in your control to True, the extender will display an Align property and possibly a Negotiate property as well (depending on the container).

Your control can read the extender's Align property to determine whether your control has been aligned. You need not take any action in this case; the container is responsible for repositioning your control.

If your control uses a Toolbar control to implement a toolbar, the Negotiate property is provided to allow the developer to determine how the toolbar is displayed on an MDI form. Refer to your Visual Basic documentation for information on using the Toolbar control provided with Visual Basic.

Cancel and Default

If you set the DefaultCancel property of your control to True, Visual Basic will add a Default and Cancel property to your control's extender. You can then read these Extender properties to determine whether your control has been set to be the Default or Cancel control for the container.

It is customary for a Default control to show a different appearance from a standard control of the same type. You could look at the default Extender property, but Visual Basic provides an easier way to determine when this is necessary. The Ambient object has a DisplayAsDefault property that is set to True when your control should appear in its default state. Why use the Ambient property instead of the extender?

The ch18CtlB.ctl control demonstrates this using the following code:

Private Sub UserControl_AmbientChanged(PropertyName As String)
   If PropertyName = "DisplayAsDefault" Then
      UserControl.Refresh
   End If
End Sub

Private Sub UserControl_GotFocus()
   Label2.Caption = "Has Focus"
End Sub

Private Sub UserControl_LostFocus()
   Label2.Caption = "Lost Focus"
End Sub

Private Sub UserControl_Paint()
   Dim s$
   If Extender.Default Then
      s$ = "Default"
   End If
   If Extender.Cancel Then
      s$ = s$ & " Cancel"
   End If
   If s$ = "" Then s$ = "Normal"
   Label1.Caption = s$
End Sub

The control contains two constituent Label controls that are used to display the current state of the control. Open the frmCh18Test2 form in design mode and try changing the control's Default and Cancel property in the VB Property page (keep in mind that by doing so you are directly changing the Extender properties). When you change the Default property, the control will update immediately. When you change the Cancel property you will not see the change until the control is redrawn (try changing the size of the control after changing the Cancel property to see this).

Is there a way to determine when an Extender property has changed? I don't know of any, other than polling. If you find one, please let me know.

Container Dependencies

Allow me to close this section with two warnings. Visual Basic's Extender control and design environment is exceptionally robust. Other containers are likely to have fewer Extender properties and less sophisticated support for features you may have incorporated into your control. You should review all use of Extender properties to make sure that your control will still work if they are missing. There are a number of options for how to handle different containers with regard to Extender properties:

Terrible Evil Things You Should Never Do

The container and parent properties give you access to the Container object and all of its properties and methods. This means your control can do the following:

I could probably go on for pages with variations on the possibilities. It is possible for controls to be terribly nasty to an application.

It is fundamental to the philosophy of components that the developer using your control be responsible for the container. The developer is also responsible for setting and using Extender properties and events (since those are provided by the container). As a control author, keep your hands off the container and other controls!

Ambient Properties

It is sometimes easy to get confused between Extender and Ambient properties. Like so many aspects of ActiveX control development, the difference between them is a matter of perspective. Extender properties are those that are added to your control by the container and are intended to be used primarily by developers who are using your control to create applications. Ambient properties are those that are provided by the container and are intended to be used by control authors.

Ambient properties are accessed through the AmbientProperties object, which is accessed via the Ambient property of the UserControl object for your control. The AmbientProperties object always includes a core set of standard properties, unlike the Extender object, whose properties vary based on the container and control configuration. However, this does not mean that all of the standard Ambient properties are, in fact, supported by each container. If an Ambient property is not supported by the container, the AmbientProperties object simply returns a default value for the property.

Consider, for example, the UserMode property. If a container does not support a design-time mode, it need not provide a UserMode ambient property. The AmbientProperties object sees that the property is missing in the container and always returns a default value of True for its own UserMode property.

A container can provide additional container-specific Ambient properties. These properties are always late bound, and you should use error trapping when accessing them. Table 18.3 shows the standard properties for the Ambient object. The default value shown indicates the value returned for the property when it is not supported by the container. The UserControl object's AmbientChanged event is triggered any time an Ambient property is changed.

Table 18.3: The Standard Ambient Properties

PropertyComments Default Value
BackColorBackground color of the container. &H80000005
DisplayAsDefaultIf the control's DefaultCancel property is True, and the developer specifies that this is the default control, this property returns True. Refer to the description of the DefaultCancel property in Chapter 17. False
DisplayNameThe name of the control assigned by the developer. ""
FontThe font of the container, or the default font recommended by the container for use by the control. Note that the container's FontTransparent property does not trigger an AmbientChanged event. MS Sans Serif 8
ForeColorForeground color of the container. &H80000008
LocaleIDSee chapter text. Current system default
MessageReflectSee chapter text. False
PaletteA Picture property specifying the container-recommended palette (usually the palette in use by the container).  
RightToLeftIndicates that the control should draw text from right to left on Hebrew or Arabic versions of Windows. False
ScaleUnitsThe name of the units in use by the container. See chapter text.  
ShowGrabHandlesSee chapter text. True
ShowHatchingSee chapter text. True
SupportsMnemonicsSee chapter text. False
TextAlignIndicates the container's text alignment or the default text alignment for the control. Zero
UserModeTrue indicates that the control is in end-user mode, which means runtime for Visual Basic. True
UIDeadSee chapter text. False

Specific Properties

The following Ambient properties are worthy of further comment.

Encapsulated Properties

The MessageReflect, ShowGrabHandles, ShowHatching, SupportsMnemonics, and UIDead ambient properties are important to controls but are handled entirely by Visual Basic's implementation of ActiveX controls. You can safely ignore them. Here is a short description of these properties in case you are interested:

ScaleUnits

This property can be used to determine the scale units of the container. When Visual Basic is the container, ScaleUnits can take the values User, Twip, Point, Pixel, Character, Inch, Millimeter, Centimeter; these are the constant names of the ScaleMode property.

The possible values of this property are not defined by the ActiveX specification. Thus, containers can use any strings they wish. This means that this property may not be reliable under all containers for determining the actual scale mode of the container. It is intended primarily for controls to be able to display the container coordinate system when necessary.

When Visual Basic is the container, you can also access the container's ScaleMode property directly through the Extender object's Container property.

LocaleID

A Locale is a 32-bit value that identifies the language and platform for the thread or system. Bits 0 through 15 (the low-order word) identify the language. Bits 16 through 19 specify how sorting works under this language. It is typically 0 but may be set to 1 for Far East Unicode environments. Figure 18.2 shows the structure of a Locale identifier.

Figure 18.2 : The Locale identifier.

The low-order word is divided into two parts. The low 10 bits (bits 0 through 9) indicate the language. The high 6 bits (bits 10 through 15) indicate a subset of the language-to differentiate, for example, between U.S. English and U.K. English. The values for the supported languages can be found in any good Win32 API declaration file: Look for the constants with the prefix LANG_. The subsets, or sublanguage constants, are identified with the prefix SUBLANG_. Many functions take the default system or user locales, which are given the special constant values

Public Const LOCALE_SYSTEM_DEFAULT = &H800
Public Const LOCALE_USER_DEFAULT = &H400

A great deal of information is associated with locales. For example, the country name, names of days and months (and their abbreviations), the symbol used for local currency, the format used for dates and times, and other information that tends to vary from country to country. This information can go a long way in helping you create truly international applications using Visual Basic. The subject of locales and the Win32 API functions that are available to work with them are discussed further in the Visual Basic 5.0 Programmer's Guide to the Win32 API.

UserMode

The UserMode ambient property is probably the most important one. It is almost certainly the one that you will use most often. A setting of True for this property indicates that the control's container is in run mode or End-User mode. False indicates that the container is in a design-time or developer mode. Keep in mind that not every container supports a design-time environment.

The most common uses for this property are:

Strategies for Using Ambient Properties

There are a number of approaches you can use when working with Ambient properties. In order to illustrate this, let's take a close look at the BackColor property.

This is another one of those properties that require you to keep close track of which object you are considering. The ch18Test1 project group includes the ch18ctlD control, which contains three constituent Text Box controls. This means that you, as the control author, have to deal with six different BackColor properties on six different objects, as shown in Table 18.4.

Table 18.4: A Plethora of BackColor Properties

PropertyDescription
Text1.BackColorAuthor only-sets the background color of the Text1 control
Text2.BackColorAuthor only-sets the background color of the Text2 control
Text3.BackColorAuthor only-sets the background color of the Text3 control
UserControl.BackColorAuthor only-sets the background color of the ActiveX control
BackColorPublic-Does whatever you the control author wishes
Ambient.BackColorPublic-The current background color of the control's container

This raises some interesting questions. Exactly what should this control look like? What determines the background colors of the various text boxes and the control itself? You have a number of options:

I would not dare to suggest which approach you should take. But I would strongly recommend that you ask yourself these questions when you are designing your control.

The ch18CtlD control demonstrates all of these approaches in a manner that is hopefully educational and definitely unattractive. The public BackColor property defines the background color for the control and the Text1 constituent control. The property functions are as follows:

Public Property Get BackColor() As OLE_COLOR
   BackColor = UserControl.BackColor
End Property

Public Property Let BackColor(ByVal New_BackColor As OLE_COLOR)
   UserControl.BackColor() = New_BackColor
   Text1.BackColor = UserControl.BackColor
   PropertyChanged "BackColor"
End Property

There are two things to note in this code. First, the property type is OLE_COLOR. This property type is defined by referencing the standard OLE (stdole) library, which is referenced by default by Visual Basic 5.0. The OLE_COLOR variable is a 32-bit long, and variables of this type can be assigned directly to and from Visual Basic numeric variables. Why then should you use the OLE_COLOR type? Because Visual Basic is a very smart container, and when it sees a property with the OLE_COLOR type, it adds the color selection popup to the VB property window for that control.

The other point of interest is that two objects are "connected" to the BackColor property. When the property is set, both the UserControl and the Text1 control's BackColor property are set. The intent in this example is for these two objects to always have the same background color. The property is saved in the UserControl_WriteProperties function with the following code:

Call PropBag.WriteProperty("BackColor", UserControl.BackColor, &H8000000F)

It is read as follows:

UserControl.BackColor = PropBag.ReadProperty("BackColor", &H8000000F)
Text1.BackColor = UserControl.BackColor

When writing the property, we can use the value from either the UserControl or the Text1 object, since they are the same. When reading the property we must be sure to set both to the new value. It is also necessary to initialize the property in the UserControl_InitProperties event. There are two approaches for doing so. One is

UserControl.BackColor = PropBag.ReadProperty("BackColor", &H8000000F)
Text1.BackColor = UserControl.BackColor
PropertyChanged "BackColor"

The other is to simply use

BackColor = &H8000000F

In this case the public BackColor setting serves exactly the same purpose. The advantage to using the public property in this manner is obvious-it allows you to reuse the same code and helps you avoid forgetting some crucial operation (such as calling PropertyChanged or setting all of the values that are necessary). There will be cases, however, where you cannot do this; for example, when you have code or error trapping in the public property setting routine that you do not want called when reading the property settings from a file.

The Text2 constituent control takes a different approach. It is set using an independent property called BackColor2. As such it is completely independent of the other background colors, and its property procedures are quite straightforward:

' Background color for text2
Public Property Get BackColor2() As OLE_COLOR
   BackColor2 = Text2.BackColor
End Property

Public Property Let BackColor2(ByVal vNewValue As OLE_COLOR)
   Text2.BackColor = vNewValue
   PropertyChanged "BackColor2"
End Property

The Text3 constituent control is somewhat more complex. Its background color can be set to follow the container color or set independently using the BackColor3 property. This imposes some additional requirements on the control code:

Listing 18.1 shows the remainder of the control module. This example takes a simple approach to letting the developer choose between the different modes for the Text3 control. It uses a separate property called Text3Ambient, which is True when the Text3 control should follow the Ambient BackColor property value. The value for this property is stored in private variable m_Text3Ambient. A second private variable, m_Text3Backcolor, is used to hold the value of the BackColor3 property.

Another possible approach would be to define an "impossible" color value, meaning the control should return to the ambient background color mode. In this case, you would examine the BackColor3 property Let function parameter. If it was some invalid value, such as &HFFFFFFFF, you would set the m_Text3Ambient property to True instead of setting the m_Text3Backcolor variable. This approach is somewhat awkward, because it requires the developer to remember that the way to reset the control to its ambient background mode is to type -1 or &HFFFFFFFF into the BackColor3 property edit box in the VB Property window. Since colors are usually set using the pop-up palette values, this is not an intuitive approach. A separate variable to control the Text3 control mode is much easier for developers to remember.


Listing 18.1: Partial Listing for ch18ctlD.ctl
' Should we use the ambient or back3color value
Private m_Text3Ambient As Boolean
Private m_Text3Backcolor As OLE_COLOR

Private Sub UserControl_AmbientChanged(PropertyName As String)
   If PropertyName = "BackColor" Then
      SetText3Color
   End If
End Sub

'Load property values from storage
Private Sub UserControl_ReadProperties(PropBag As PropertyBag)
   UserControl.BackColor = PropBag.ReadProperty("BackColor", &H8000000F)
   Text1.BackColor = UserControl.BackColor
   Text2.BackColor = PropBag.ReadProperty("BackColor2", &H80000005)
   m_Text3Backcolor = PropBag.ReadProperty("BackColor3", &H80000005)
   m_Text3Ambient = PropBag.ReadProperty("Text3Ambient", True)
   SetText3Color  ' Update after read
End Sub

'Write property values to storage
Private Sub UserControl_WriteProperties(PropBag As PropertyBag)
   Call PropBag.WriteProperty("BackColor", UserControl.BackColor, &H8000000F)
   Call PropBag.WriteProperty("BackColor2", Text2.BackColor, &H80000005)
   Call PropBag.WriteProperty("BackColor3", m_Text3Backcolor, &H80000005)
   Call PropBag.WriteProperty("Text3Ambient", m_Text3Ambient, True)
End Sub

Public Property Get BackColor3() As OLE_COLOR
   If Ambient.UserMode Then
      ' At runtime, return the actual color in use
      BackColor3 = Text3.BackColor
   Else
      BackColor3 = m_Text3Backcolor
   End If
End Property

Public Property Let BackColor3(ByVal vNewValue As OLE_COLOR)
   m_Text3Backcolor = vNewValue
   PropertyChanged "BackColor3"
   SetText3Color
End Property

Public Property Get Text3Ambient() As Boolean
   Text3Ambient = m_Text3Ambient
End Property

Public Property Let Text3Ambient(ByVal vNewValue As Boolean)
   m_Text3Ambient = vNewValue
   PropertyChanged "Text3Ambient"
   SetText3Color
End Property

Private Sub SetText3Color()
   If m_Text3Ambient Then
      Text3.BackColor = Ambient.BackColor
   Else
      Text3.BackColor = m_Text3Backcolor
   End If
End Sub

The actual setting of the Text3 background color is accomplished using the SetText3Color function. This function needs to be called any time the Text3 background color may change. This includes after the properties are read during the UserControl_ReadProperties event and any time the Text3Ambient or BackColor3 properties are set.

To see the effects of this code, bring up the frmCh18Test3 form in design mode and experiment with the various property values using the VB property window.

As you have seen, the Ambient and Extender objects interact closely with properties that you define for your ActiveX control. But as you will see in the next chapter, you have only begun to delve into issues relating to ActiveX control properties.