Cocoa Design Patterns: Bindings and Controllers
- Role of Bindings and Controllers
- Collaboration of Patterns Within Bindings and Controllers
- Bindings and Controllers Limitations and Benefits
Chapter 29, Controllers, describes the roles of Coordinating Controllers and Mediating Controllers within Model View Controller design pattern that permeates Cocoa. Coordinating Controllers initialize, load, and save the Model and View subsystems. Mediating Controllers manage the flow of data between view objects and model objects to minimize coupling between the subsystems. Cocoa supplies the NSApplication, NSDocumentController, NSDocument, NSWindowController, and NSViewController classes among others to provide reusable implementations of most common coordinating tasks. Cocoa also includes NSObjectController, NSArrayController, NSTreeController, and NSUserDefaultsController, which provide reusable implementations of some common mediating tasks.
Cocoa's reusable Controller subsystem classes go a long way toward simplifying the design and development of traditional "glue" code needed to meld a model and a view into a cohesive application. The MYShapeDraw example in Chapter 29 shows how patterns like Outlets, Targets and Actions, Notifications, and Data Sources are used in combination with the Controllers pattern to implement full-featured Controller subsystems. However, starting with Mac OS X version 10.3, Cocoa Bindings technology has enabled a higher level of abstraction for Mediating Controllers. Bindings further reduce the amount of code needed to implement Controller subsystems and can be configured in Interface Builder to nearly eliminate code for mediating tasks.
Role of Bindings and Controllers
Bindings and Controllers work side-by-side with other patterns like Targets and Actions, Data Sources, and Notifications. You can use Bindings to reduce the amount of mediating "glue" code in your applications, but as always, there is a trade-off. Look at each application design situation on a case-by-case basis to decide which approach makes the most sense. This chapter provides the information you'll need to evaluate whether to use Bindings and Controllers or other patterns or some mixture.
Bindings keep model objects and view objects synchronized so that changes in one subsystem are automatically reflected in the other. Like almost all Cocoa technology, bindings are implemented to reduce or eliminate coupling between objects. Bindings are based on the string names of object properties as opposed to compiled addresses or offsets, and bindings are configurable at design time and runtime.
NSController classes are valuable components of any Cocoa application that uses the Model View Controller pattern, whether bindings are used. In contrast, bindings should only be used in combination with controller objects like NSObjectController and NSArrayController. Whenever two objects are bound, at least one of them should be a controller. Controllers can be bound to each other. View objects can be bound to a controller. Model objects can be bound to a controller. Avoid binding View subsystem objects directly to Model subsystem objects. Don't bind view objects together or model objects together.
The simplest example of binding within a Model View Controller application is shown in Figure 32.1, which depicts a text field with has its own floatValue property bound to the floatValue property of whatever object is selected by an instance of NSObjectController. Chapter 29 explains the concept of selection within controllers. The NSObjectController's content outlet is set to an instance of MYModel, which provides a floatValue property. The content of an NSObjectController instance is just one object unlike an NSArrayController which uses an array of objects as its content. The selection provided by an NSObjectController is always the content object.
Figure 32.1 Binding within a Model View Controller application
The binding shown in Figure 32.1 keeps the floatValue of the text field synchronized with the floatValue of the MYModel instance. If the value in the text field is changed by the user, the change is reflected in the bound MYModel instance. Just as importantly, if the value in the bound MYModel instance is changed, the bound text field is automatically updated.
A slightly more complex binding is shown in Figure 32.2. Both a text field and a slider are bound to the floatValue property of a MYModel instance. If the user moves the slider, the floatValue of the MYModel instance is updated, which in turn causes the text field to be updated. If the user enters a value in the text field, the floatValue of the MYModel instance is updated, which in turn causes the slider to be updated. If the floatValue of the MYModel instance is changed programmatically through an appropriate Accessor method, both the slider and the text field are automatically updated to display the new value.
Figure 32.2 More binding within a Model View Controller application
Bindings are used in much more elaborate ways than shown in Figure 32.1 and Figure 32.2. The value of bindings is magnified when you have more complex models and more complex views. Core Data models, complex views, and the NSController classes integrate well with bindings and provide opportunities to almost eliminate traditional controller glue code. Nevertheless, the bindings technology is not dependent on Core Data or complex views, and all of the Cocoa technologies can be used without bindings.
Bindings Avoid Coupling
Bindings are defined by string keys that identify the objects and properties to bind. Key Value Coding (described in Chapter 19, "Associative Storage") provides the underlying mechanism used to obtain the runtime values of properties from associated keys. The use of string keys avoids any need for the objects that are bound together to know anything about each other. Any two properties of any two objects can be bound together, and as long as properties corresponding to the string keys can be found at runtime, the binding will function. String keys minimize coupling between bound objects and allow dynamic changes at runtime. For example, if you bind a text field's value to a property of an array controller's selection, the text field will be automatically updated any time the selection changes or the value of the selected object's bound property changes. In other words, the text field isn't bound to any specific object. It's bound to whatever object is selected by the controller at any particular moment.
String keys provide even more flexibility by supporting key paths. A key path is a series of '.' separated keys that specify a sequence of properties to access. For example, if each employee object has a name property and a department property, and each department object has a manager who is also an employee, you could bind the value of a text field to the "selection.department.manager.name" key path of an array controller. At runtime, the text field's value is then synchronized to the name of the manager of the department of the selected employee. The selection is an employee object. The binding asks for the selected employee's "department" property. It then asks for the department's "manager" property. It then asks for the manager's "name" property.
It's also possible to use operators, which provide synthetic properties. For example, if each department has an array property called "employees," you can create a binding to "selection.department.employees.@count". The @count operator returns the number of objects in the array obtained from the employees property of the department property of the selected employee. A description of the operators supported for use with Cocoa collection classes is available at http://developer.apple.com/documentation/Cocoa/Conceptual/KeyValueCoding/Concepts/ArrayOperators.html.
The Importance of Using Controllers with Bindings
Chapter 1, "Model View Controller," made the case that application data shouldn't be stored in the user interface. Instead, the Model View Controller design pattern partitions the application and stores application in a Model that's independent of any View. If you bind the properties of two View objects directly together, you are most likely diluting the benefits of Model View Controller design pattern. In the worst case, you're right back to storing crucial application data in the user interface. Therefore, it's best to bind View objects to other objects outside the View layer.
But why not bind View objects directly to Model objects? One reason is that Cocoa's NSController subclasses all implement the NSEditorRegistration informal protocol. Informal protocols are explained in Chapter 6, "Category." The NSEditorRegistration protocol provides methods for view objects to inform a controller when editing is underway. It's important for controllers to have that information so that partial edits can be validated and changes can be saved without requiring the user to explicitly commit every edit that's started. NSControllers keep track of which view objects have unfinished edits and can force the view objects to request completion of each edit or discard the intermediate values. For example, if a user is typing in a text field and then closes the window containing the text field, the relevant NSControllers update the Model with the contents of the text field. The Model update causes the document to be marked as needing to be saved and then you are offered a chance to save changes before the window closes. If you don't include a controller in each binding between a View object and a Model object, then you must replace the NSEditorRegistration protocol functionality, and Model objects are a poor place to implement requests for completion of edits taking place in the View. Therefore, you need a controller to mediate between the View and the Model.
Another reason to include controllers in your bindings is that NSControllers keep track of the current selection and sometimes provide placeholder values for bound properties. Being able to bind to the current selection as opposed to a specific object makes bindings very flexible.
Finally, spaghetti bindings are as much of a problem as spaghetti code and lead to similar maintenance hassles. The discipline of including NSControllers in every binding clarifies the relationships between objects and serves as visual documentation for bindings. If you inspect a controller object in Interface Builder, there is a visible list of all bindings that involve that controller object, as shown in Figure 32.3. It's straightforward to inspect the controller objects whenever you open an unfamiliar .nib file. If bindings exist between other objects, the only way you can find them is by inspecting each end every object in the .nib. Religiously including controllers in bindings is a wise design guideline and serves the same purpose as coding standards: it reduces the number of places programmers need to look to understand the system.
Figure 32.3 Inspecting bindings with Interface Builder