ВНИМАНИЕ: Это не окончательная версия документа (как и видно из текста)!
Эта версия документа взята из дистрибутива VisualWorks 7.4.1
(Перевод - Pollock - Соединение модели и GUI (VisualWorks))
A GUI is only of any interest because it provides a way for the user to interact (interface) with a domain, which is where the real work of an application program is performed. Typically the interaction is bidirectional, with the GUI displaying or in some graphical way representing some part of the data managed by the domain model, and the domain model data being updated according to input by the user to the GUI.
A change to the GUI might be as small as updating a display field. Other changes affect what are reasonable user actions, and so might need to be reflect in the GUI by disabling a button, menu item, or some other widget. Other conditions might require that additional windows be opened or closed. And so on. Some of these changes will be dictated by the domain, though others might be determined within the GUI itself.
In the Pollock framework, this interaction is performed using the Trigger Event system (refer to the Application Developer's Guide for a general description). The event system provides a general mechanism for communication between objects, whether they be in the GUI or the domain model. Accordingly, it provides a mechanism for communicating between the GUI and the domain, between parts of the GUI, or between parts of the domain.
In this chapter we discuss how to use the event system to define this interaction, and how to control the GUI from the running application program.
GUI to Domain CommunicationПравить
A GUI provides the user input interface for an application. Users initiate actions by entering information, selecting options, and clicking on buttons to instruct the application in how to perform its work. In the Pollock framework, each of these actions triggers an event. When appropriately configured, that event notifies the underlying domain model of the change or requested action.
Each window and widget is built to trigger events under specified circumstances. Buttons, for example, trigger a #clicked event when the user clicks on them. Input fields trigger an event when the user changes the field data. All panes, including widgets, trigger mouse motion events when the user moves the mouse into or out of the boundaries of the pane.
Only some of these events will be of interest to your application or its domain. The events of interest you hook up to the appropriate domain objects by specifying an action to occur when the event is triggered.
For example, suppose a domain model needs the user to pick a number between one and ten. There are a lot of ways to use a GUI to collect this information--punching buttons on a "key pad," selecting a check box, or entering a number in an input field, for example. Suppose we settle on having the user enter the number in an input field and then clicking a "Submit" button. Every time the user changes the value in the input field, by typing a digit or deleting one, the input field triggers a #changed event. We can ignore these because we only want to use the value of the input field when the button is clicked. When the user clicks the button, it triggers a #clicked event. The handler we write for that event from that button then collects the value from the input field and sends a normal Smalltalk message to the domain model with the value as argument.
Domain to GUI CommunicationПравить
Often, the domain model is changing independently of user actions to a GUI. Perhaps the GUI is reflecting the state of a database, the data in which is changing due to other user or process actions. Or, the GUI is displaying the results of a monitoring program that is periodically updating the status of a system.
In this case, you can configure the domain to trigger an event when a value is updated. Usually this involves adding a line to an update method to trigger the event immediately following updating the value. The GUI is then configured to handle the event from that object, and to update its display accordingly.
There is an alternative to using events. You can define a polling loop in your application that periodically requests a value from the domain model, and updates the GUI accordingly. This has the advantage that it does not require enhancing the domain model with event triggering messages. It does, however, require an additional running process to maintain the loop. Both approaches have their uses.
GUI to GUI CommunicationПравить
It is also not uncommon for parts of a GUI to communicate with each other. For example, two or more widgets might provide different views of the same data, such as a slider showing a position on a scale and a display field showing a numeric representation.
In some cases this can be done as simply as the two widgets sharing the same model. Then, when the model value changes, all widgets sharing that model are notified, via events, that the change has occurred and they can update.
In other cases the communication may need to be more explicit. But, again, this is done by configuring the widgets to trigger events and defining handlers for those events.
Widget Value ModelsПравить
Most widgets in a GUI are responsible for displaying or accepting values, most often as either numbers or as text. Each time the underlying value changes the GUI must be updated, and any time the user changes the value in the GUI the underlying value must be updated.
A widget that presents data (such as an input field) relies on an auxilliary object, called a value model, to manage the data it presents. That is, instead of holding onto the data directly, a data widget delegates this task to its value model. When a data widget accepts input from a user, it stores this data in its value model. When a data widget needs to update its display, it asks its value model for the data to be displayed.
The VisualWorks value model mechanism provides a uniform set of messages for accessing the data to be presented, allowing all data widgets to store and refresh their data in a standard way. Two messages are central to the value model:
- Returns the data value from the value model.
- Sets data value in the value model and triggers #aboutToChange, #changing, and #changed events.
Other objects, such as the application model, can also send these messages to a value model to obtain or change a widget's data.
A data widget is a dependent of its value model, in the sense that the widget depends on its value model to notify it when the relevant data has changed. The widget responds to such notification by asking the value model for the new data and displaying it. This keeps the widget's display synchronized with changes made programmatically to the data.
Choosing a Value ModelПравить
There are three standard value model objects used in VisualWorks, implemented by these three classes:
- BufferedValue (and CopiedBufferedValue)
There are several other special-purpose adaptors as well. Browse the ValueModel subclasses for the full collection.
As described in Chapter 1, "Creating a GUI Programmatically", each widget that needs a model either has one assigned when the widget is added, or uses a default model. In general this is an ObserveredValue on an appropriate object. This model might be held in an instance variable of the user interface class, or accessed directly from the widget. The ObservedValue responds appropriately to the value and value: messages.
You can always coordinate values in a domain model with those held in an the model of a widget, and so you can always use an ObservedValue as the widget's model. Often the easiest way to do this is to define an instance variable in the user interface to hold the value. In early development, this provides a way to set up a GUI and verify that it operates properly, possibly using only test values. In an application with a domain model well separated from the application model, using an ObservedValue requires that you provide the logic necessary to coordinate values between the user interface and domain.
For many applications, using a ObservedValue is not the preferable approach, because it involves maintaining a value twice: in both the domain model and user interface. Rather, it is frequently better to configure the user interface to get at least some of its widgets' values directly from the domain model. This is done by using an AccessAdaptor, which redirects the value and value: messages to the domain model. In addition to reducing the overhead of the application model, getting values from the domain model is also frequently simpler than ensuring that the domain and aspect values are synchronized.
Sometimes the domain model provides the data needed by the widget in the form required by the widget, but not always. For simple cases you simply provide the accessor method to the AccessAdaptor. When the data is more complex, and so requires more manipulation, you can either provide "access path," which is a sequence of methods, or provide a block as the accessor. (In this way, AccessAdaptor includes the functionality provided by both AspectAdaptor and PluggableAdaptor in the original GUI framework.) All of these are illustrated below.
BufferedValue and CopiedBufferedValue are similar, but are used in cases where updating the model requires updating several values at once, keeping the values all synchronized. This is necessary, for example, in may database applications. These also are illustrated later in this chapter.
Configuring an ObservedValueПравить
An ObservedValue is the most basic type of value model for a widget. An ObservedValue holds a value of a type suitable for its widget. The object type is different for different widgets.
The primary responsibility of the ObservedValue is to respond to the messages value, to return the held value, and value:, to change the held value and trigger events indicating that it has changed. An ObservedValue does little else, and so is very simple to configure and use.
Accordingly, configuring the model consists primarily of setting up handlers for its events: #aboutToChange, #changing, and #changed. The #changed event is the most commonly handled, since it is normally of interest that the change has occurred, and less commonly that it is either about to change or in the process of changing.
Consider a simple domain model that simply increments or decrements its current value upon request (see ValueModifer class in the Pollock-ExampleDomain package). The following sections describe a few ways to use an ObservedValue to represent its current value to an InputField widget.
The examples in the Pollock-ValueModels package illustrate three approaches.
Configuring the Widget Model in an Instance VariableПравить
Frequently, the simplest to understand way of configuring an widget's model is to hold the ObservedValue in an instance variable, and assign the variable as the widget's model. This is particularly useful in cases where the value of the widget is referenced frequently in the application, since it is easier to reference the value by a variable name than by digging it out of the widget model. The value might also need manipulating in various ways, as is the case for simple application in which the user interface and domain models are combined.
To hold the model for the widget in an instance variable, define a class and add a variable to the definition. IncrementorWithInstVar uses the currentValue instance variable for this purpose. Then, in the createInterface method, it specifies the model for the InputField as teh variable:
createInterface | display button1 button2 label | display := Pollock.InputField new id: #displayField; beEnabled: false; model: currentValue; yourself. ...
The variable's initial value needs to be set, which can be done in the initialize method:
initialize super initialize. domainModel := ValueModifier new. currentValue := self domainModel value printString asObservedValue.
The widget expects an ObservedValue holding a text object, but the domain model returns a numeric value. The printString message converts that to a String, which works for the widget.
Next, we need to update the displayed value when the model increments its value, which only happens when the Next button is clicked. The hookupInterface method configures the button's #clicked event to sent an increment message to the user interface itself, which in turn sends an increment message to the domain model (a little polymorphism there). That increments the value in the domain, but now the widget's model needs to be updated. It is an ObservedValue, so it is updated by sending it a value: message with the new value.
increment self domainModel increment. self currentValue value: self domainModel value printString
Again, the value comes back from the domain model as a number, so needs to be converted to a String. But, this time, there's no need to wrap it in the ObservedValue, since the value: message changes the value within the ObservedValue.
That is essential code necessary for this example to update the display. When the value is changed with the value: message, the necessary events are triggered to inform the widget that it has been updated.
Configuring the Default Widget ModelПравить
If you do not specify a model for a widget when defining the interface, an appropriate model is created by the widget when its model is first accessed by a model message sent to the widget. This is always an ObservedValue on an appropriate object, based on the requirements of the widget.
IncrementorWithDefaultModel illustrates how to configure the user interface to use and update the default model. No instance variable in the user interface is used to hold the model value, which is held directly in the widget's model variable.
In the createInterface method, no model is specified for the widget:
createInterface | display button1 button2 label | display := Pollock.InputField new id: #displayField; beEnabled: false; yourself. ...
There is no variable to initialize, so the initialization method is simply:
initialize super initialize. domainModel := ValueModifier new.
The currentValue method now retrieves the current value of the domain model, rather than returning the instance variable holding widget's model:
currentValue ^self domainModel value printString
The value is updated in the widget, as in the previous example, when the Next button is clicked, which triggers the #clicked event, which invokes the increment message:
increment self domainModel increment. (self widgetAt: #displayField) model value: self currentValue
Without an instance variable to update, we need to access and update the widget's model and update it directly. Access the widget's model with the expression:
(self widgetAt: #displayField) model
You need to use this expression any time you access the widget's model. To update the model's value, send it a value: message, as shown above. To simply get its value, send a value message:
(self widgetAt: #displayField) model value
Because the default model is created with a default value, an empty AdvancedText in the case of an input field, the GUI initially does not display the current value of the domain model. We can repair this in several ways, but opt for updating the widget's model value in the createInterface method, after the widget is defined:
createInterface | display button1 button2 label | display := Pollock.InputField new id: #displayField; beEnabled: false; yourself. display frame: (FractionalFrame fractionLeft: 0.5 top: 0.3 right: 0.5 bottom: 0.3). display frame leftOffset: -30; rightOffset: 30; bottomOffset: 20. self addComponent: display. (self widgetAt: #displayField ) model value: self currentValue.
Assign the Widget Model with an Accessor MethodПравить
The approach described here is a variation on the default model approach. Instead of leaving the model content unspecified, depending on the default behavior to provide it, the value can be provided in an accessor method. This also addresses the initial display issue we had to address at the end of the preceding section. IncrementorWithAccessMethod implements this variation.
The current value of the domain model is still retrieved by the currentValue method. This time we will leave the returned value unconverted to a String, for no essential reason other than to be different:
currentValue ^self domainModel value
In some contexts there may be practical value to this, providing the numeric value to manipulate within the user interface. In this context it is simply a variation.
In the createInterface method, we use this accessor method to supply the initial value to the input field's model:
createInterface | display button1 button2 label | display := Pollock.InputField new id: #displayField; beEnabled: false; model: self currentValue printString asObservedValue; yourself. ...
Because the currentValue method returns a numeric value, it must be converted here to a String and wrapped in an ObservedValue.
Notice that, unlike the first example in which the model held an instance variable, the model holds an ObservedValue on a specific value. To update its value, we have to access the model through the widget as we did for the default model. So, the increment method is the same, except for the stylistic variation already mentioned:
increment self domainModel increment. (self widgetAt: #displayField) model value: self currentValue printString
Configuring an AccessAdaptorПравить
An ObservedValue can be used for most circumstances to provide the model for a widget. In some cases, however, it is not sufficient. For cases in which
- two UIs share and update the same model,
- a change can occur to a domain object from outside the system (e.g., Databases),
- a change can occur to a domain object from inside the system, independent of the UI, or
- access to the domain is complex
an AccessAdaptor is more appropriate. An AccessAdaptor replaces the ObservedValue as the model for the widget.
An AccessAdaptor responds to value and value: messages, to get and set the value, just like an ObservedValue. However, to get and/or set the value in the model itself requires a mapping between these messages and messages understood by the model. This mapping is defined by the accessor, to get the value, and the mutator, to set the value. For complex accessors, you can also set an access path, which is a collection of accessors.
Defining an AccessAdaptorПравить
To create an AccessAdaptor, send a with: instance creation message with an expression that returns the domain model object:
- Creates a new AccessAdaptor and assigns the model object for the adaptor.
Then specify the adaptor's properties with the following messages:
- Assigns the accessor expression for retrieving the widget value from the domain model. anAccessor is either a uniary selector symbol, an integer if the value is held in an indexed variable, a one-argument block, or an ObservedValue.
- A collection of selector symbols and/or integers, specifying a succession of access operations preceding the one specified by accessor:.
- Assigns the mutator expression for setting the domain model value from the widget value. aMutator is either a keyword selector symbol, an integer if the value is held in an indexed variable, a one-argument block, or an ObservedValue.
- If the domain model triggers event aSymbol when it updates the target value, this message configures the adaptor to watch for that event, and update the value accordingly.
While the model object is assigned by the with: instance creation message, you can also assign or change the model by sending a model: message:
- Assings or replaces the model object for the adaptor.
For both the with: and model: messages, the object can be an ObservedValue. This allows for changing the model object without affecting the rest of the adaptor. To replace the model object, send a value: message to the ObservedValue with the new object. The model object is changed and the appropriate events are triggered to effect the update.
Adapting a Changing ModelПравить
Frequently domain model values change independently of any GUI connected to it. For example, if the domain model may be monitoring external processes or other data, and the GUI must be regularly updated to reflect such changes.
The GUI framework relies on the the domain model to trigger events when its values change. An AccessAdaptor can then be configured to respond to the event with the appropriate updates.
RandomWatcher is a simple GUI that displays the current value of a object that changes independently of the GUI. RunawayRandoms provides the independently updating domain, by generates a random number stream and updating to the next random every second. The #currentChanged event is triggered each time the current random changes:
next "Get next random and notify of update" current := generator next. self triggerEvent: #currentChanged with: current.
The adaptor can defined and assigned as the widget's model in the createInterface method for RandomWatcher, for example:
createInterface | display label button1 button2 | display := Pollock.InputField new id: #displayField; beEnabled: false; model: ((AccessAdaptor with: Smalltalk.Examples.RunawayRandoms allInstances first) accessor: [ :mod | mod current printString]; updateTrigger: #currentChanged ; yourself).
Here the AccessAdaptor is created with an existing instance of RunawayRandoms as its model (for the sake of the example, the initialize method ensures that there is one). In this case, a block is used to specify its accessor, which sends current to the model to get the value, then printString to make it acceptable to the InputField. The updateTrigger: message registers the widget's interest in that event, so it can update its value each time the model changes its value.
Corrdinating Two GUIs on the Same ModelПравить
Often it is useful to have to views of the same domain model. If the domain model triggers an event when its value changes, then it is easy for each user interface to be updated; simply configure each to register an interest in the event.