Control Binding

Introduction

Control binding is a visual programming mechanism in which you can make controls communicate by simply wiring them together, with no code needed. REALbasic has many predefined bindings, and allows developers to define their own bindings. Unfortunately, control binding is very nearly undocumented by Real Software; as a consequence, it goes unused by the majority of Rb developers. This article aims to fill the documentation gap and encourage developers to bind.

Using RB's Built-in Control Bindings

A Warmup Example

Let's get a feel for what one can do with a control binding by way of a simple example. Create a new REALbasic project. In Window1, add a Listbox control and a Pushbutton control. In the Open event handler of Listbox1, add a few rows.

Sub Open
  me.AddRow "Row 0"
  me.AddRow "Row 1"
End Sub

Now, while holding down the Shift and Command keys, click on the listbox and drag the mouse to the push button. Observe that the push button becomes highlighted. This tells you that there is a control binding defined between Listbox and PushButton objects. Release the mouse button while the mouse is over the push button. The following dialog box will appear.

The New Binding dialog lists all of the control bindings defined between Listbox and PushButton. The one binding listed causes the push button to be active if and only if the listbox has a selected row. Select that binding, and click "OK". Window1 will now show a line connecting the listbox and the push button.

You can select the binding by carefully clicking on the line; once it is selected, the binding information is given in the Properties window.

Now, run the project in the IDE and observe the bind in action. Selecting a listbox row causes the push button to be enabled, and clearing the selection causes the push button to be disabled.

Why you should be impressed

Obviously, enabling a push button, or other control, when a row is selected, is a task easily accomplished with a little code. In the Change event handler of your listbox, add the following line of code.

PushButton1.Enabled = (me.ListIndex > -1)

Now run your project in the IDE. Oops -- there's a glitch; the push button is enabled, even though no row in the listbox is selected. This is simple enough to fix; simply disable the push button when the window opens.

But this is the problem with writing code; it's usually not error-free. In contrast, using a binding allows to do what you need without writing any code. You'll need to write code to implement your own bindings, but once it works, you can use it over and over. And bindings can be much more complex than this.

Built-in Bindings

The RB Developer's Guide discusses bindings in Chapter 3 (see "Object Binding") and gives a list of built-in bindings. You can always determine if a binding exists on the fly by simply attempting to create a binding between two controls. If no binding exists, then the target of the drag will not become highlighted.

Perhaps the most common built-in binding allows the target control to be enabled or disabled, depending on the state of the source control. For example, you can bind a Listbox to a PushButton or BevelButton so that the button is enabled precisely when the Listbox has a row selected. This is quite useful; consider a situation in which you have a list of URLs and an "Edit" button. Clicking the Edit button opens a new window in which the user can edit the URL. By enabling the button only when a row is selected, you do not need to check the the ListIndex property of the Listbox when implementing the action of the Edit button. And using a binding to handle the enabling is, of course, easy and fast.

Custom Bindings

Once you begin to use control bindings, you'll eventually want a binding that doesn't come with REALbasic. Conveniently, REALbasic makes it possible and quite simple to write your own control bindings. We'll explore how to write bindings in a series of examples. The associated project contains all of the code.

The bindingInterface Class Interface

A control binding is defined by a class implementing the bindingInterface class interface. It has one method, Bind(source as Object, dest as Object). In a class implementing bindingInterface, the Bind method must contain a compiler directive that tells Rb about the bind. It has the form

#pragma bindingSpecification sourceClassName, destClassName, NewBindingWindowText

where NewBindingWindowText is a string describing the bind. In this string, you use the labels %1 and %2 to refer to the source and target objects, respectively. For example,in a binding which enables a RectControl when the value of a Checkbox is true, the compiler directive might look like

#pragma bindingSpecification Checkbox, RectControl, "Enable %2 when %1 is checked".

As defined, this binding will work to connect a Checkbox (or a Checkbox subclass) to a RectControl. If, for example, you choose to bind an object called Checkbox1 to Listbox1, Rb will substitute those names for the labels %1 and $2 when it displays the New Binding Window; the message would read

"Enable Listbox1 when Checkbox1 is checked"

and you would select it to specify the binding.

Note that the classes you specify in the bindingSpecification must be Control, or subclasses of Control; you cannot use Object. But, as we'll see later, you can drag an instance of an arbitrary class into a window and define a binding that works for it. You can also specify class interfaces. Unfortunately, the bindingSpecification pragma isn't documented, so my remarks are supported by experiment only.

"Hello World"

Let's start with a trivial example. Define a new class TrivialBinder. In the Properties window, put "bindingInterface" in the Interfaces field. Open the Code Editor for TrivialBinder and add a new method Bind(source as Object, dest as Object), as required to implement bindingInterface. Add the following code.

Sub Bind(source as Object, dest as Object)
  #pragma bindingSpecification Control, Control, "Trivially bind %1 to %2"
End Sub

Open Window1 and drag two controls of your choice to the window. Now shift-command drag from one control to the other; the target control should become highlighted as you drag the mouse over it. Release the mouse and the New Binding window should appear. There will be at least two choices, because the specification isn't restrictive enough. That's fine, because it's a trivial example. You can experiment by changing the specification to bind instances of particular classes. The project should run, but the bind doesn't do anything.

To make a bind that does something pointless, add ActionNotificationReceiver to the list of interfaces implemented by TrivalBinder. ActionNotificationReceiver is a built-in class interface that has one method, PerformAction(). Add this method to the TrivialBinder class.

Sub PerformAction()
  Msgbox "Hello world"
End Sub

Then change the Bind method to the following.

Sub Bind(source as Object, dest as Object)
  #pragma bindingSpecification ActionSource, Control, "Say hello when %1 is pushed"
  ActionSource(source).AddActionNotificationReceiver me
End Sub

ActionSource is a class interface implemented by the PushButton and BevelButton classes. So drag a PushButton and some other control to Window1 and bind them using your new custom binding. I point out that you can bind the PushButton to several controls, with the result being a Msgbox for each of the binds. Run the project and enjoy.

The Basic Framework

In general, a control binding consists of the following: a source object, a target object, a binding object that implements bindingInterface. The source object and target object are usually controls. The binding object implements bindingInterface, so that it can describe the binding to RB, and so RB can set the binding when the window opens.

These objects typically communicate as follows. The binding object registers itself as a client of the source object in the binding object's Bind method. When the the source object announces an event, it tells the binding objects, who in turn act on the target object.

These objects do not necessarily need to be distinct; in some built-in bindings, the binding object and target object are the same. But for writing custom bindings, it is better to maintain the distinction.

Binding Creation

When a window containing bindings is created, Rb instantiates a binding object for each bind and calls its Bind method; note that this happens before and Open events are called. If the binding object needs to know about some property of either the source or target control, then the value of this property must be set in the Properties window of the control.

As we'll see later, you can create a binding "in code", as opposed to dragging in the IDE. You may need to do this if you want to define multiple bindings between two controls, as Rb supports only a single binding.

And now let's tackle a more substantial example.

Binding a StaticText to a LittleArrows Control

You might want to use a StaticText to display a value that can be changed using a LittleArrows control. With a little work, you can do this with a bind.

First, define a subclass NotifyingLittleArrows of LittleArrows. Add two public methods AddValueChangedReceiver(receiver as LittleArrowsValueChangedReceiver) and RemoveValueChangedReceiver(receiver as LittleArrowsValueChangedReceiver). LittleArrowsValueChangedReceiver is a class interface that you're about to define. It will have two methods, UpArrow and DownArrow.

To the NotifyingLittleArrows, add a private property receivers(-1) as LittleArrowsValueChangedReceiver.

Below are the implementations of the Add and Remove subroutines.

Sub AddValueChangedReceiver(receiver as LittleArrowsValueChangedReceiver)
  If me.IndexOf(receiver) = -1 then
    me.receivers.Append receiver
  End if
End Sub

Sub RemoveValueChangedReceiver(receiver as LittleArrowsValueChangedReceiver)
  dim index as Integer

  index = me.IndexOf(receiver)
  If index > -1 then
    me.Receivers.Remove index
  End if
End Sub

Private Function IndexOf(receiver as Object) dim i, U as Integer

  U = UBound(me.receivers)
  For i = 0 to U
    If me.receivers(i) = receiver then 
      Return i
    End if 
  Next 
  Return -1 
End Function

A bit of explanation about the implementation -- I write a lot of classes that have Add and Remove methods as part of the interface. For classes such as this one, where I expect that there will be only a small number of objects added, I'll just use an array to hold the objects. For many objects, it would be more efficient to use a Dictionary.

Loop code is a common source of errors. Thus I like to pull it out into a separate method when the opportunity arises, as above. That IndexOf takes an Object as parameter is a small habit of convenience, since I use the code in lots of classes.

Next, we need to implement the Up and Down event handlers of NotifyingLittleArrows. Here is the Up code; the Down code is similar.

Sub Up()
  dim i, U as Integer

  U = UBound(me.receivers)
  For i = 0 to U
    me.receivers(i).ArrowUp
  Next
End Sub

Finally, add a New Event Close(), and implement the Close handler as follows.

Sub Close()
  Close
  Redim me.receivers(-1)
End Sub

Although it doesn't matter for this example, in general I don't know if some other object might hang onto a NotifyingLittleArrows reference, so as a precaution I clear out the LittleArrowsValueChangedReceiver references in the Close event.

We now turn to the implementation of a binding object. Define a new class StaticTextValueChangedBinder. In the Properties window, add LittleArrowsValueChangedReceiver and bindingInterface to the Interfaces field. Implement the Bind method as follows.

Sub Bind(source as Object, dest as Object)
  Const AlignRight = 2
  #pragma bindingSpecification NotifyingLittleArrows, StaticText, "Update %2 when %1 is changed"

  NotifyingLittleArrows(source).AddValueChangedReceiver me
  me.ValueDisplay = StaticText(dest)
  me.ValueDisplay.TextAlign = AlignRIght
  me.ValueDisplay.text = "0"
End Sub

Next, we need to implement the LittleArrowsValueChangedReceiver interface, which consists of two subroutines, ArrowUp and ArrowDown. ArrowDown is similar to ArrowUp.

Sub ArrowUp
  me.ValueDisplay.text = Str(Val(me.ValueDisplay.text) + 1)
  me.ValueDisplay.Refresh
End Sub

If the user holds the mouse down to scroll repeatedly, the Refresh call ensures that he'll be able to watch the numbers whipping by.

Finally, add a private property ValueDisplay as StaticText, and you're ready to bind.

A Binding Framework

The next example is somewhat more complex, and shows one way to write a small framework to support various bindings for a control possessing a complex event structure.

Listboxes and EditFields have relatively complex event structures, but EditField has more limited support for bindings. So we'll use the EditField as our base class. Define a subclass NotifyingEditField. Our first task is to define an interface for event receivers, and to add the capability to register event receivers with NotifyingEditField.

We'll define a class interface EditFieldNotificationReceiver, and add the following methods.

GotFocus(e as EditField)
KeyDown(e as EditField, Key as String) as Boolean
LostFocus(e as EditField)
TextChanged(e as EditField)
SelectionChanged(e as EditField)

To NotifyingEditField, add the methods

AddEditFieldNotificationReceiver(receiver as EditFieldNotificationReceiver)
RemoveEditFieldNotificationReceiver(receiver as EditFieldNotificationReceiver)

The implementation of these methods is essentially the same as in the previous example. But note that I've already made a significant design decision. The EditFieldNotificationReceiver class interface could have been split into five separate class interfaces, one for each event, with corresponding Add and Remove methods required for NotifyingEditField. This means that every EditFieldNotificationReceiver wil be notified of every event, although it's likely that it will only be interested in certain events. The advantage of this design is that the interface is relatively simple. The disadvantage is that it is potentially slower, so we'll need to keep this in mind as we implement the design. For the purposes of illustration, the relative simplicity of the interface is a plus. And I expect the total number of EditFieldNotificationReceivers to be few, so that the speed cost should be dominated by things like typing speed.

Now, let's turn to constructing the client framework. Since the interface of EditFieldNotificationReceiver is relatively complex, we can avoid the hassle of typing in all five methods in every binding class we write by first writing an abstract class AbstractEditFieldNotificationReceiver to implement the EditFieldNotificationReceiver interface. We'll then expose events to be implemented in subclasses; the effect will be that the subclasses will look like pieces of the EditField.

Add to AbstractEditFieldNotificationReceiver the methods from EditFieldNotificationReceiver, and then the following corresponding new events.

EditFieldGotFocus(e as EditField)
EditFieldKeyDown(e as EditField, Key as String) as Boolean
EditFieldLostFocus(e as EditField)
EditFieldSelectionChanged(e as EditField)
EditFieldTextChanged(e as EditField).

Then the implementation of each method of AbstractEditFieldNotificationReceiver will consist of calling the corresponding event.

Now, let's write some bindings.

A PushButton that knows when the EditField contains text

For the purpose of data validation, you would like the "Save" Pushbutton in a window to be enabled only if a certain EditField has some text entered. So let's define a subclass PushButtonTextChangedEnabler of AbstractEditFieldNotificationReceiver. This subclass implements bindingInterface as follows.

Sub Bind(source as Object, dest as Object)
  #pragma bindingSpecification EditField, PushButton, "Enable %2 when %1 has text"
  
  NotifyingEditField(source).AddEditFieldNotificationReceiver me
  me.button = PushButton(target)
  me.button.Enabled = (EditField(source).text <> "")
End Sub

PushButtonTextChangedEnabler requires a property button as PushButton to hold a reference to the PushButton on which it will act.

Adding the binding action requires only implementing the EditFieldTextChanged event handler.

Sub EditFieldTextChanged(e as EditField)
  me.button.enabled = e.text <> ""
End Sub

A Date Formatter

Now let's define another subclass of AbstractEditFieldNotificationReceiver that formats the EditField's text as a date when the EditField loses focus, or resets the focus if the text is invalid. This example is of independent interest, as it shows that one can set up bindings using classes other than controls.

Our class, EditFieldDateFormatter, will violate my earlier suggestion that source, bind, and target be distinct objects. When source and target are both built-in classes, That suggestion was based on my belief that it is better to write a new binding class than to write a control subclass for the target. In this case, however, there is no advantage to writing two new classes.

Here is the code.

Sub Bind(source as Object, dest as Object)
  #pragma bindingSpecification NotifyingEditField, EditFieldDateFormatter, "Format text of %1 when it loses focus"
  
  NotifyingEditField(source).AddEditFieldNotificationReceiver me
End Sub
Sub EditFieldLostFocus(e as EditField) dim d as Date If ParseDate(e.text, d) then e.text = d.ShortDate Else MsgBox "This isn't a valid date." e.SetFocus End if End Sub

To establish the binding in a window, drag the EditFieldDateFormatter row from the project window to the window of your choice. The result of the drop will be represented as a control that looks vaguely like a monitor. Now you can connect the controls as with any other binding.

Other Ideas Some other suggestions for extending the framework we've just built:

Defining Bindings in Code

Unfortunately, RB won't let you define two bindings between the same pair of objects. But the metaphor is so convenient that you want to use it anyway and define multiple bindings. The answer is to define a binding in code. It's not as nice as being able to establish and see the binding in the IDE, but it's not much more difficult.

Let's consider an example I mentioned earlier, in which you have a Listbox and a BevelButton. The BevelButton is to be enabled when the Listbox has a row selected, and clicking the button tells the listbox that the user wants to edit the record represented by the selected row.

We're going to extend the example a bit to use buttons to create, edit, and delete records. The idea is that you will have several windows, each displaying a different table, and you'd like to minimize your work.

Each window will have three buttons and a listbox. The New Button will always be enabled, while the Edit and Delete buttons will be enabled only if a row is selected in the listbox. Bindings that enable a BevelButton when a listbox row is enabled are built-in, so let's go ahead and wire them up.

Next, let's write the custom bindings. As in the NotifyingEditField example, our implementation of the bindings will be factored into a base class that implements actionNotificationReceiver and exposes an event, and subclasses that implement bindingInterface and add binding-specific behavior. (Confession: it certainly didn't start out this way; what you're seeing is the code after refactoring and spiffifying.) After I sketch the classes, I'll add a few more comments on the design.

First is the RecordListbox subclass of Listbox. For the purposes of our example, it is enough to give it methods CreateRecord, EditSelectedRecord, and DeleteSelectedRecord that do nothing but throw up a Msgbox to show that they've been called.

BevelButton implements the ActionSource class interface. Unfortunately, Action Source, or the fact that BevelButton implements it, is not documented. At the end of this article is my documentation of the control binding interfaces. For now, it's enough to know that BevelButton implements it, and that ActionNotificationReceivers (another undocumented class interface) register with ActionSources to be notified when an action occurs(i.e. the button is pushed).

Next, we'll define a RecordListboxAction class that implements ActionNotificationReceiver. It has a private property list as RecordListbox. We'll also declare two new events, ButtonClicked() and ButtonCaption() as String. The second event allows us to set the button's caption in each subclass.

The ActionNotificationReceiver interface specifies one method, PerformAction(). Its implementation consists only of calling the ButtonClicked event.

Finally, we need a constructor. I point out that when you create a control binding in the IDE, the constructor of the binding object is not called. But we're going to create these bindings in code, so the constructors will be called.

Sub RecordListboxAction(source as BevelButton, target as RecordListbox)
    ActionSource(source).AddActionNotificationReceiver me
  me.list = target
  source.caption = ButtonCaption
End Sub

There is also a constructor RecordListboxAction(source as BevelButton, target as RecordListbox) whose code is essentially the same. I did it this way instead of writing one constructor RecordListboxAction(source as ActionSource, target as RecordListbox), because such a constructor would have required branching on the class of the source button. If, in the future, I want to add support for another button class, I can add a new constructor instead of modifying existing code.

We now add three subclasses of RecordListboxAction, NewRecordActionBinder, EditRecordActionBinder, and DeleteRecordActionBinder. Each of these subclasses will implement bindingInterface. Here is the implementation of Bind for EditRecordActionBinder; the code for the other two classes differs only in the bindingSpecification. Note that I'm calling the constructor in the Bind method because, as I mentioned above, the constructor will not be called if I create the bind in the IDE.

Sub Bind(source as Object, dest as Object)
  #pragma bindingSpecification EditButton, Listbox, "Edit selected record of %2 when %1 is pushed"
  
  RecordListboxAction(ActionSource(source), RecordListbox(dest))
End Sub

Finally, we need to implement the ButtonClicked and ButtonCaption events. Implementing the ButtonClicked event consists of calling the appropriate method of RecordListbox, and ButtonCaption simply returns a caption string.

Now, let's bind some controls. Here's how to bind an Edit button to a RecordListbox.

Sub Open()
  dim binder as RecordListboxAction
  
  binder = new EditRecordActionBinder(me, RecordListbox1)
End Sub

That's it. Note that by putting this code in the button's Open event handler, if you later delete the button, you delete the binding at the same time; if you established the binding in Window.Open, then you'd need to go there to delete the code.

The button holds a reference to the EditRecordActionBinder (and this is the only reference), and the EditRecordActionBinder holds a reference to the Listbox. So when the window is closed, the button is destroyed, and there are no more references to the EditRecordActionBinder, and it goes away -- or, it should.

But there is a bug in the BevelButton class. If you add an ActionNotificationReceiver using its AddActionNotificationReceiver method and do not remove it explicitly by calling RemoveActionNotificationReceiver, the ActionNotificationReceiver is not destroyed when the BevelButton is destroyed. PushButton, which also implements ActionSource, does not have this problem. This problem is easy enough to work around, but I chose not to clutter up the example with the workaround.

Control Binding Class Interfaces - The Missing Documentation

REALbasic includes close to twenty class interfaces for use in implementing control bindings. Unfortunately, very few of them are documented. Nor does the documentation reveal which interfaces are implemented by which controls. But, equipped as you now are with the secrets of writing your own bindings, you can manage nicely without using the built-in interfaces.

I have attempted to document the built-in class interfaces. I believe that I've identified all of them, but the purpose of a few of them continues to elude me.

RB Control Binding Class Interfaces Documentation
Example Project


If you've found this page useful, then feel free to


© 2003 Charles Yeomans
Last Modified 2/12/2003