Data Binding
You can add data-binding
capabilities to an ActiveX control with little more than a few mouse clicks.
As is not the case for intrinsic controls, you can create controls that bind
multiple properties to database fields. All you have to do is tick the Property
Is Data Bound check box in the Data Binding section of the Procedure Attributes
dialog box, shown in Figure 17-10, for all the properties that you want to make
data aware.
You can create as many data-bound
properties as you like, but you must select the This Property Binds To DataField
option for one of them only. If no property is bound to the DataField property,
the Extender object won't expose all the Dataxxxx properties that are
necessary to actually bind the control. Because such properties are exposed by
the Extender object, their availability depends on the host environment.
Figure 17-10.
The Procedure Attributes dialog box includes all the options for creating
data-aware properties.
PropertyChanged
and CanPropertyChange methods
To support data binding
in code, you don't have to do anything more than you already do for persistent
properties. In each Property Let procedure, you must call the PropertyChanged
method, which informs Visual Basic that the property has changed and that
the database field should be updated before the record pointer moves to another
record. If you omit this call, the database field won't be updated. You can also
update the field immediately if you select the Update Immediate option in the
Procedure Attributes dialog box.
Visual Basic also provides
the CanPropertyChange method, which queries the data source to determine
whether it's safe to update the field. You could use the following code in the
Property Let procedure of a property called CustomerName. (The
statements that have been added to the code by the wizard are in boldface.)
Public Property Let CustomerName(New_CustomerName As String)
If CanPropertyChange("CustomerName") Then
txtCustomerName.Text = New_CustomerName
PropertyChanged "CustomerName"
End If
End Sub
You should be aware, however,
that you don't strictly need to call the CanPropertyChange method because
under Visual Basic 5 and 6 it always returns True, even if the database field
can't be updated. You should use this function only for compatibility with future
versions of the language that might implement it. For all the properties that
call this method before doing the update, you should also select the Property
Will Call CanPropertyChange Before Changing option in the Procedure Attributes
dialog box. Again, at this time there's no point in doing that, but it doesn't
cause any harm either. The choice is yours.
To correctly support data
binding, the constituent controls must update the corresponding bound property
when their contents change. Typically this is done in the Change or Click
event procedure, as in the following code snippet:
Private Sub txtCustomerName_Change()
PropertyChanged "CustomerName"
End Sub
The DataBindings
collection
As I mentioned before, only
one property can be bound to the DataField Extender property. Because
you can bind multiple properties, you need to provide developers with a method
for associating each bound property to the corresponding database field. This
association can be done either at design time or during execution.
For each property that you
want to make bindable at design time, you must select the Show In DataBindings
Collection At Design Time option in the Procedure Attributes dialog box. If this
option is selected for one or more properties, the DataBindings item appears
in the Properties window. When you click on it, Visual Basic brings up the dialog
box shown in Figure 17-11. Note that it's OK that the property bound to the DataField
property also appears in the DataBindingscollection.
Visual Basic 6 permits you
to bind properties in the DataBindingscollection to fields in different
Data Sources, and you can also select a distinct DataFormat for each one
of them. In Visual Basic 5, you could bind properties only to the same Data Source.
Figure 17-11.
The DataBindings dialog box lets developers associate properties with database
fields at design time.
All the bound properties
appear in the DataBindingscollection at run time, regardless of whether
they appear in the collection at design time. You can't add new items to this
collection through code, but you can change the database field to which a property
is bound:
' Bind the CustomerName property to the CompanyName database field.
Customer1.DataBindings("CustomerName").DataField = "CompanyName"
Another common task for
the DataBindings collection is to cancel changes in fields so that the database
record won't be updated:
Dim dtb As DataBinding
For Each dtb In Customer1.DataBindings
dtb.DataChanged = False
Next
For more information about
the DataBindingscollection, see the online Visual Basic documentation.
The DataRepeater
control
Visual Basic 6 lets you
create custom grid-like controls, using the DataRepeater control (contained in
the Msdatrep.ocx file). This control works as a container of other ActiveX controls:
It can host any type of controls, but it's especially useful with custom ActiveX
controls.
Say that you want to display
a table of records, but you don't want to use a standard Visual Basic grid control-such
as the DataGrid or Hierarchical FlexGrid control-because you need maximum flexibility
for interaction with the user or because you want to display information that
can't be embedded in a regular grid (images, for example). Figure 17-12 shows
a custom grid built on the DataRepeater control that displays the Publisher table
from the Biblio.mdb database. To create such a custom grid, you must execute
these basic steps:
-
Create an AddressOCX control
that contains all the fields you need; this is the object that will be replicated
in the DataRepeater control.
-
For all the properties that you want to
expose in the DataRepeater control-that is, Name, Street, City, Zip, and State-make
the property data bound and have it appear in the DataBindings collection at
design time.
-
Save the project, compile it into a stand-alone
OCX file, and load the client application where you want to display the custom
grid.
-
Drop an ADO Data control on the client
form, and then set its ConnectionString and RecordSource properties
to point to the table in the database that provides the data. (You can also
use any other ADO data source, including a DataEnvironment object.)
-
Drop a DataRepeater control on the form,
have its DataSource property pointing to the ADO Data control, and select
the AddressOCX ActiveX control from the list that appears when you click on
the RepeatedControlName. (This list includes all the OCXs that are registered
in your system.)
-
Bring up the DataRepeater control's custom
property page, switch to the RepeaterBindings tab, and associate the bound
properties exposed by the inner ActiveX control with the database fields. You
can also set in the Format tab the DataFormat property for each field.
Figure 17-12.
The DataRepeater
control lets you create custom views of your database tables.
The complete source code
of the demonstration program is on the companion CD.
The DataRepeater control
has some rough edges, and you must pay attention to many details to have it working
properly:
-
The UserControl must be
compiled into an OCX file; otherwise, it can't be hosted in the DataRepeater
control. You can't use an intrinsic Visual Basic control with a DataRepeater.
-
All the bound properties in the inner
ActiveX control should return String values; you can then format these values
using the DataFormat options offered by the DataRepeater control. Moreover,
all the properties must be visible in the DataBindings collection at design
time; otherwise, the DataRepeater control won't see them.
-
The constituent controls on the child
form should call the PropertyChanged method whenever the user changes
their values; otherwise, the database won't be updated correctly.
-
The DataRepeater control creates only
one instance of the control; this control is used to let the user edit values
for the current record, whereas all other rows are just images of the control.
You might notice some incorrect repaints every now and then.
The DataRepeater control
exposes several properties, methods, and events that augment its potential and
flexibility. For example, you can directly access the active instance of the
child control to set additional properties (RepeatedControl property),
find the line number of the current record (ActiveRow property), change
the DataRepeater's appearance (by assigning the Caption, CaptionStyle,
ScrollBars, RowIndicator, and RowDividerStyle properties),
get or set a bookmark to the current or the visible records (using the CurrentRecord
and VisibleRecords properties), and so on. You can also monitor users'
actions-for example, when they scroll the contents of the list (ActiveRowChanged
and VisibleRecordsChanged events) or select another row (CurrentRecordChanged
event).
Interestingly, it's even
possible to load a different child ActiveX control at run time by assigning a
new value to the RepeatedControlName property. In this case, you must
associate the bound property with fields by using the properties of the RepeaterBindings
collection. (You can provide the user with a list of bindable properties
using the PropertyNames property.) Whenever a new child control is loaded
at run time, the DataRepeater fires a RepeatedControlLoaded event, which
the programmer can use to correctly initialize the new control.
What's missing
The data binding mechanism
offered by Visual Basic is fairly complete, although a few features aren't directly
supported and you have to implement them yourself.
For example, there's no
direct support for controls that bind a list of values to a secondary
Data source, as the DataList and DataCombo controls do. You can implement this
feature by exposing a custom property-such as RowSource-to which developers
can assign the secondary Data control (or another ADO-compliant data source).
Here the problem to solve is: You can't display a custom list in the Properties
window, so how do you let the developer select the data source at design time?
The answer is based on custom property pages, which are described in the next
section.
One thing that at first
seems to be impossible is to decide at run time which property binds to the DataField
Extender property. In this situation, the solution is actually simpler than it
might appear: Create an additional property that binds to DataField and that
delegates to one of the other properties exposed by the control. This mechanism
can be made extremely flexible by means of the new CallByName function.
For example, let's say that you want to give developers the ability to bind any
property among those exposed by the Customer control. You need to create two
additional properties: BoundPropertyName, which holds the name of the
bound property, and BoundValue, which does the actual delegation. This
is the code in the Property Get and Let procedures for the latter
property:
' BoundValue binds directly to DataField, but the value actually stored
' in the database depends on the BoundPropertyName property.
Public Property Get BoundValue() As Variant
BoundValue = CallByName(Me, BoundPropertyName, vbGet)
End Property
Public Property Let BoundValue (New_BoundValue As Variant)
CallByName Me, BoundPropertyName, vbLet, New_BoundValue
End Property
You should make BoundValue
hidden so that developers are discouraged from using it directly.
Property Pages
The majority of ActiveX
controls that you find in the Visual Basic package or buy from third-party vendors
are equipped with one or more custom property pages. In this section, you'll
see how easy it is to create property pages for your own ActiveX controls.
Even if the Visual Basic's
Properties window is usually sufficient to enter property values at design time,
there are at least three reasons why you should create custom property pages.
First, they greatly simplify the job of the programmers that are using your control
because all properties can be grouped in a logical way. Second, and more important,
property pages give you much greater influence over how properties are set at
design time. For example, you can't show a combo box in the Properties window
with a list of values built dynamically, nor can you let developers drop down
a mini-editor to enter multiple values (as they do when editing the List property
of ListBox and ComboBox controls). These restrictions are easily overcome with
property pages. Third, property pages permit you to localize the design-time
user interface of your controls for different languages.
So that you can see property
pages in action, I created a SuperListBox ActiveX control, an expanded ListBox
that exposes an AllItems property (which returns all the items separated
by a carriage return character) and allows you to enter new items at run time
using a pop-up menu. My control also gives the programmer the ability to bind
either the Text property or the ListIndex property to the DataField,
thus overcoming one of the few limitations of the data binding mechanism in Visual
Basic. This control employs a number of interesting programming techniques-such
as API functions to implement a columnar format-and you might want to browse
its source code on the companion CD.
Running the
Property Page Wizard
You can add a property page
to an ActiveX Control project with the Add Property Page command from the Project
menu, but you can save a lot of work and time using the Property Page Wizard.
(You have to install this add-in from the Add-In Manager dialog box.) In the
first step of the wizard, you can create custom property pages, select their
order, and decide whether you want to keep standard property pages. (See Figure
17-13.) Visual Basic automatically adds the StandardColor, StandardFont, and
StandardPicture pages (for properties that return OLE_COLOR, StdFont, and StdPicture
values, respectively), but you can also decide to deactivate them if you want.
Figure 17-13.
The first step of the Property Page
Wizard is the point at which you create new pages and change the order of selected
pages.
In the second step of the wizard, you decide on which page
each custom property will be displayed. All the properties that you leave in
the leftmost list box (as shown in Figure 17-14) won't be displayed on any property
page.
Figure 17-14.
In the second step of the Property Page Wizard, you decide
which properties will be shown on which page.
When you click on the Finish button, the
wizard creates one or more PropertyPage modules. For each property that you assigned
to the page, the wizard generates a Label control (whose Caption is the
name of the property) and a TextBox control that holds the value of the property,
or a CheckBox control if the property returns a Boolean value. If you want a
fancier user interface—for example, ComboBox controls for enumerated properties—you
have to modify what the wizard has produced. Figure 17-15 shows the General property
page for the SuperListBox control after I rearranged the controls and converted
a couple of TextBox controls into ComboBox controls.
Figure 17-15.
The property page generated by the Property Page Wizard,
after some retouching.