September 2010
A pattern for GUI programming
This
document summarizes the way LightWidgets are intended to be used. The
style for GUI programming is based on PluggableViews in MVC and
PluggableMorphs in Morphic. The main idea is to have a reusable set of
standard widgets that can be customized when used by application developers. There is a strict
separation between views and models. Models don't know about views,
they are never aware of them. Views know about their model and update
it directly. Therefore, there's no need for views to trigger events, and they don't do it.
This description is not only conceptual, or theoretic. The rules described here are to actually be followed.
GUIs
are built by composing widgets. The main view is a subclass of
CompositeLW. There is a complete separation between Model and Views.
Rule 1. Models should never include GUI code
They
must be completely unaware of possible Views that operate on them.
There could be at any time any number of different views active on the
same model. They could belong to different technologies or frameworks.
They could even be remote and run on a different computer. There could
be no view at all. For example, the model could be driven by scripts or
reside on a server and receive external commands. However, this
document will only describe local LightWidgets GUIs. Rule 2. Views should never include any model code
The
view could be replaced anytime with a different one. Besides, a model
should be able to run without any GUI at all. So any logic that belongs
in the model but is included in the GUI will eventually be missing. The
Views should query and modify Models only through public protocols,
called 'Inquiries' and 'User Commands'. Rule 3. Views know about the model they operate on
Views
have an instance variable to hold their model. They can query Model
Inquiries when needed. They can also issue User Commands when
appropriate. Models are usually subclasses of ActiveModel. Let's
consider a small example. We are building a GUI to operate on some
Person objects. We'll consider an EntryField for the birthday of
aPerson. LightWidget includes the following instance variables:
target : Holds the Model. For our example, the model of the EntryField
would be the Person. We call it target, because sometimes it might be a
view aspect : It is a symbol, the getter for the aspect we are showing. In this case it would be #birthday. aspectAdaptor : It is a symbol, a message that is sent to the aspect to
adapt it to the widget. As the widget is an EntryField and the aspect
is a Date, the aspectAdaptor could be #asString. This relies the Model
from the need to provide an appropriate getter for each kind of
possible widget for each attribute. action : The action is the setter used to update the aspect on the model. In this example, it is #birthday:. actionAdaptor : It is used to adapt the value the user entered in the
widget for use as an argument of action. In this example it could be
#asDate.
It is usually a good idea to initialize model aspects
with reasonable defaults, and avoid nil values. This saves a lot of
#ifNil: messages in the gui. Rule 4. View Structure
A Model could have a tree-like structure. It could be composed of other Models. This is not mandatory. Views
always have a tree-like structure. The leaves are simple widgets. The
internal nodes are CompositeLWs. They can all share the same model, or
they could could use different parts of the bigger model. Anyway, they
are always customized with the aspec and action. Rule 5. GUI construction
The
construction of the Views tree and the customization of each widget is
done by a main view. The main view also specifies how the Views are
notified of model changes for updating. Rule 6. Instance variables in viewsAdditional
instance variables in GUIs are of two kinds: They can be uses to hold
sub-views, or to hold 'Model Extensions'. Possible uses of Model
Extensions include: Holding information that can be obtained
from the aspect, but that could be expensive and it makes sense to
cache. For example, our EntryField could hold an array if indices of
word starts and ends or some other internal detail. Holding
state that is meaningful for the widget, but that it doesn't make any
sense to keep in the model. An example could be the cursor position in
our EntryField. Others could be visual options, such as a graph type or
graph style for an application generated graph. Not-yet-commited information, entered by the user, but awaiting for OK / Cancel.
In general, Model Extensions usually are re-fetched from the model, or re-set to default values when the model changes. Rule 7. View updating because of model changesAny
widget (in fact, any Morph) can redraw itself when needed, with the
#changed method. But when there is a change in the Model, the views
must be updated appropriately. All the Model Extensions must be
updated, and all sub-views must be updated too.
When there is a
change in the Model, the Views must receive the #modelChanged message.
A main view (i.e. a view that is not subview of another view with the
same Model) must send itself #beMainViewOn: on construction. This does
'target when: #selfChanged send: #safeModelChanged to: self'. The Model
must trigger event #selfChanged when appropriate. #safeModelChanged
will eventually update all subviews recursively. So only a main view
should receive the #selfChanged event. Models are usually subclasses of
ActiveModel, to use the more advanced events implementation there.
This is the implementation of #beMainViewOn: . This message should be used to set the model of a main view. beMainViewOn: aModel "We are a main view on aModel. This means: - aModel is a real model, i.e. not a widget. - no aspect or aspectAdaptor. We show the whole thing. - no action or actionAdaptor. There is no main action. - we must update ourselves on #selfChanged event" self target: aModel aspect: nil aspectAdaptor: nil modelChangeEvent: #selfChangedThe
main update method is #modelChanged. #safeModelChanged is only to
guarantee that the update is done in the User Interface process, in the
inter-cycle paus. The implementation of #modelChanged at LightWidget is: modelChanged "The model changed is some way. This is usually the pace to call #targetAspect to fetch the current value of the aspect from the model, and to store it in some Model Extension. We must update all Model Extension instance variables with values from the model (i.e. target) or with appropriate defaults. We must update ourselves and all subviews to reflect the model's new state"
self updateView#modelChanged must be reimplemented in classes with model extensions. Check the implementors to see how they work. After updating the model and model extensions, #updateView is called. This is the implementation at LightWidget: updateView "The model or some Model Extension changed is some way. We must update ourselves to reflect the new state.
This is the place to update secondary Model Extensions or any other state that must be updated after model or Model Extension change.
This method is usually reimplemented in CompositeLWs, to update subviews. The subviews should be sent one of the following messages: target: target:aspect: target:aspect:aspectAdaptor: target:aspect:aspectAdaptor:aspectChangeEvent: to update their model and do a full update, as triggered by #modelChanged" self changedWarning:
Never implement other methods like #updateViews. If for performance
reasons the updating of subviews must be splitted in parts, then the
views and subviews must be restructured accordingly. Then, each part
can be updated as a whole with the #updateView method. Each part can be
updated by more specific model change events, or alternatively, they
might be set different submodels. Both options are described below.
The update of widgets should never trigger the action of the widget. Rule 8. View updating because of Model Extension changesIf
the target of a widget is another widget, the action is a User Command
on the target widget. These methods should not update the model,
because if this was the case, the target should be the model and not
the widget. Therefore, User Command methods in widgets can only update
Model Extensions or trigger view actions, such as opening new views,
etc. If they update Model Extensions they should call #updateView, so
the change is shown in the widget and its subviews. sampleUserCommand: data modelExtension1 := data. self updateView "Must call updateView because the model didn't change, and it will not trigger any change event"Rule 9. Subview updating because of submodel changes
If
the model has a tree-like structure, its view will send #beMainViewOn:
aSubModel to some subviews with a part of the model as the argument. In
this case, subviews will need to be notified of the events of their own
models. This is because the submodel might trigger the #selfChanged
event, and only the views on it should be updated. Views on the bigger
model don't need to be updated. This is good for performance when
having complex models and views. Rule 10. Subview updating because of model minor changes
There
is another reason for subviews receiving event notifications. A model
could trigger a more specific #someAspectChanged event and NOT the main
#selfChanged event. This could be done to avoid superfluous and
extensive views updating. In this case, some specific view on the view
tree should receive the #updateView message, and only the widgets that
are part of it will be updated. So, the owning view should send #target:aspect:aspectAdaptor:modelChangeEvent: to these subviews. The implementation is: target: aModel aspect: aSymbol aspectAdaptor: anotherSymbol modelChangeEvent: eventSymbol "Widgets are notified of model changes by being sent #modelChanged. This happens when: - The widget is given a new model (or target widget), aspect or aspect adaptor - An owner view is updated In addition, main views are updated from model events. See #beMainViewOn: But other widgets might update on more specific events from the model. This is useful to update only a small subview, and not the whole main view. This message is sent to such widgets, to set this specific event. Warning: When models change, they should trigger just one event. It might be #selfChanged (the most general one) or a more specific one. But it should not trigger more than one event for each change." self target: aModel aspect: aSymbol aspectAdaptor: anotherSymbol. target when: eventSymbol send: #safeModelChanged to: selfWarning:
When a model triggers more specific change events we must make sure
some widget will be notified of them. Otherwise, those changes could
not be shown to the user. Rule 11. Accessing views
Nobody
should ever query a widget for value or status. A widget should not
even query itself for current value or such. The last value or state
entered by the user should be stored in the model and/or Model
Extensions. When needed, it should be retrieved from there. The only
legitimate accesses to subviews are in #initialize and in #updateView.
Check implementors of #updateView. Rule 12. Model updating
Views
DO NOT trigger events. This is not "Event Oriented Programming". This
is Object Oriented Programming. The model is updated using the action
and the optative actionAdaptor. Methods that react to user activity
should update the model by just using the action, a simple message.
They are not allowed to ask the model for some other object to work on
it. They are not allowed to send other messages to the model. They are
allowed to modify Model Extensions. If they do, they should also
send 'self modelChanged' because an action might not modify the model
and therefore there could not be a model change event. See
ButtonLW>>mouseUp: for an example of this.
If you ever
feel the need to update the object answered by the aspec, instead of
sending a new value to the model (ivar target), it is because that
aspect should be the real model. Rule 13. GUI building
Main
views know about their subviews. Therefore it's them, in theire
#initialize method, who build the subviews and customizes them. Views
are created before assigning target or model to the main view.
Afterwards, the model or target is set, and #modelChanged is called. As
seen before, this will set the model or target of all subviews
recursively. Misc. notes
I believe nobody should do
#modelChanged, but only #safeModelChanged. Think a bit about this.
Maybe if we're certain we're in the UI process, #modelChanged is ok... If
a visual detail like #fontColor: in a LabelLW is updated, after
updating the ivar, the widget should do 'self changed'. Check the code
to see that it is actually done!
Thanks for reading.
|