Version 44 (modified by 10 months ago) ( diff ) | ,
---|
MVC
This describes MVC version 2 (pom version 2.0.0 and higher). For version 1, check the v1 documentation
Model View Controller, or MVC in short, is a software design pattern used to develop GUIs.
- A model contains the data that the user is adjusting.
- A view shows the data in a GUI to the user (eg, graph, text field, slider position)
- A controller allows the user to change the data (eg dragging sliders, entering text or numbers, clicking check boxes)
A view and controller can, and often are, merged into a single component. For example a slider shows the current value, but the user can also drag the slider; or a checkbox shows the current setting (on or off) while the user can simply click on it to toggle the value.
This V2 version also adds history (allowing support for undo/redo) to the models. All models always serve the values matching the current ModelTime. When the time changes to something in the past, the model's value may change accordingly, at which they also issue a change notification.
Why a toolbox?
This toolbox is to help users develop an MVC based GUI. The standard java lacks some functionality: while Java provides a ListModel and a TableModel, it lacks other models such as a StringModel. Also java lacks the tools to couple a model that contains a list of values to the keys of a hashmap-model and it does not support history. We need these functionalities when creating compound models.
ModelTime
Before we can explain the MVC model, we need to explain a separate class ModelTime
. This is a time keeping model, NOT an MVC model but a class needed by the MVC model, therefore we explain it first.
The ModelTime is a listenable object containing the current time of the MVC models. All MVC models store a history, and at all times deliver the value stored at (or after) the current time.
By changing the current time (using ModelTime.set()) all MVC values can be reverted to another point in time. All your MVC models should connect to the same ModelTime instance to ensure they all act consistently.
You can use the DefaultModelTime if your MVC supports history/undo/redo, or use the NoModelTime if your MVC does not support history.
The MVC Model
The toolbox has a hierarchy of MVC Models. This section describes the various standard models.
Model and Compound Models.
At the root is the MVC Model
interface. A model basically is an object, which implicitly contains some values that can be adjusted. The interface only specifies that the object broadcasts Event objects whenever something changes in the model.
A Model is called a Compound Model if it contains sub-models. For instance a Person compound model may contain a StringModel containing the name, and a NumberModel containing the age.
Event object
All Event objects are objects containing details of a change that happened in a model. It contains a reference to the source Model of the event, and possibly a child event if the change happened on some sub-model. There are four types of changes to a model:
- Changed: a sub-model of the model changed
- Added: a model (eg a list) just got extended by addition of a new submodel
- Removed: a model (eg a list) just got shrinked by removal of a submodel
- Selected: a model (eg in a list) just got selected
The notification mechanism
As mentioned the Model notifies all listeners when something in the model changes. The convention we try to stick to (but can not enforce) is that Unacceptable
is thrown whenever the change in the model can not be accepted. This works as follows
- When setValue (or equivalent functions) receive a value that is unacceptable, eg because it's out of range, or because some parent model has an issue with it, an
Unacceptable
is thrown immediately - In the case of a compound object, the problem may not arise immediately but only indirectly. For instance suppose that Parsons with a name starting with "A" can not be older than 100 years. Let's assume this check is done on the Person level. The Person model listens to changes to name and age, and this listener will throw if there is a conflict. This throw will then end up in the event notifier of the name or age model where the offending change will have to be reverted. Therefore the event notification in general is of the type ThrowingListenable and changes need to be reverted in case of conflicts. setValue (which calls notifyChanges) generally will look like this
public void setValue(newval) { now=time.tick(); // see note below history.put(newval); oldval=this.val1; this.val1=newval; try { notifyListeners(new Changed(this, null)); } catch (Unacceptable e) { log(e.getMessage()); time.set(time.now() - 1); notifyListeners(new Changed(this, null)); } }
NOTE: time.tick() will increase the current time which will trigger our own time event listener. Our listener will then clear all events after the current time as "redo" (forwarding the time) is not possible after a new event occurs. |
Any issues are pushed into the log system. This allows the GUI implementation to show any logged issues in a proper way to the user.
The recording of the message is part of the job of the setValue function, as this is the top of the event chain.
Basic Models
- BasicModel is a generic implementation of Model ment to store primitive objects eg String or numbers. It introduces a getValue and setValue function. There also is a check() function. The intention of the check() function is that a Model throws an exception if the value passed into setValue does not meet additional requirements.
- StringModel is a BasicModel containing a String
- NumberModel is a BasicModel containing a Number. Actually it contains a BigDecimal, which allows arbitrary precision numbers to be entered and manipulated without loss of precision or rounding.
- RestrictedNumberModel is a NumberModel with a minimum and maximum value.
- BooleanModel is BasicModel containing a boolean
ListModel
ListModel contains a list of Models and thus is a compound model. Elements can be added and removed to the ListModel using add and remove. Individual submodels can be fetched with get(). Properties can be checked with getSize() and contains().
MapModel
The MapModel contains a Map, or dictionary, of values. Both key and value are a Model so this is a compound model. That key and value are a Model implies that both key and value can be manipulated directly, without calling the MapModel directly. All implementations of MapModel will listen to such changes and notify the change anyway. MapModel provides a getValue(key) and a put(key,value) function. MapModels have a getKeys function that returns a ListModel of the keys in the map. Any changes to this listmodel must be reflected immediately into the map. Removing keys generally is easy, but when a key is added the MapModel must also insert the proper value. This may be easy or complex, up to the point where a popup may be needed to ask more information from the user.
DefaultMapModel is the basic implementation of MapModel. It is implemented using a LinkedHashMap, which helps keeping a fixed order in the elements. This fixed order is important to ensure the display of the elements in the GUI remains fixed. It also allows setting a minimum number of elements in the map
MapFromKeys is an implementation of MapModel that generates a Map from a ListModel. The ListModel contains the Keys of the Map. Note that ListModel can change at any moment, and MapFromKeys will always adapt immediately to meet the change. MapFromKeys ensures that all keys always have a value. If needed, the create() function is called to automatically generate a new value for a key. When a key is removed and then later re-added, MapFromKeys may remember the old value, if isRetainValues is set. This mechanism allows new maps to be generated from changing lists or maps.
SelectionModel
A SelectionModel adds selection to a Model, by adding setSelection(), getSelection() and getListModel(). The primary implementation is the DefaultSelectionModel. DefaultSelectionModel takes a ListModel and just stores the selection separately. When a selected list item is removed, the selection is cleared.
Compound Model
A compound model is a model containing other Models, for instance the Person model may contain a StringModel
to contain the name and a NumberModel
to contain the age.
A compound model is usually not changed directly, it's the contained basic models that change. It does listen to its children but usually only checks the consistency of the contained subvalues (throwing Unacceptable if needed). The compound model normally never calls setValue to its children.
In rare examples it may be needed to modify submodels from a compound model. Then things can get complex pretty quickly. First, the compound model then will have to call setValue on one of its children. This will cause the clock to advance (as setValue normally does that). Therefore the compound model should not advance the clock itself. That call to setValue will also trigger a changed-notification that may have to be dealt with in a special way to avoid infinite loops. One example of this is the MultiplicationModel in the MultiplierExample. But it is recommended to avoid this. It gets even trickier if a compound model wishes to change multiple sub-values.
Panels
The panels model offers JPanel GUI components, aka Widgets, for use with the models. Currently there are the following panels:
Panel | Description |
---|---|
StringPanel | A panel allowing the user to enter an arbitrary text |
NumberPanel | Panel allowing the user to enter an arbitrary (decimal) number with a Text field. Spinners and sliders are not good to enter general numbers because we don't have a min/max or #decimal places |
SliderPanel | A panel allowing the user to pull a slider to enter a decimal number. The left side of the slider is the minimum, the right side the maximum value. |
CheckboxPanel | A panel allowing the user to change a boolean to true or false |
ComboBox | allows to make selections in SelectionModel |
MapPanel | showing a MapModel as table with 2 columns. The left column shows the keys, the right column shows the values. All fields are editable. Editors for the models in the MapModel are generated using the PanelFactory. MapPanel is therefore limited to built-in models |
CustomMapEditor | a map editor that can be used if the values in the map are custom Models (not built-in). The keys are assumed {@link BasicModel}. The keys are shown as a list. When a key is selected in this list, the function getEditor(BasicModel) is used to get the value editor. When add is clicked, addEntry() is called. |
ListViewPanel | Shows full scrollable view of list. User can select one item in the list and edit it. The list can NOT be changed, only the existing elements can be edited. Editors for the models in the ListModel are generated using the PanelFactory. This is therefore limited to built-in models. |
EditableListViewPanel | As a ListViewPanel, but with additional add and delete buttons. When add is clicked, the addItem function is called, which is to be implemented by the programmer. This gives maximum flexibility for creating new entries |
Usage, Example
Usage is very simple, basically the same as standard java panels but you always need to create a model. We look at the SliderPanel example:
RestrictedNumberModel model = new RestrictedNumberModel( new BigDecimal("1.2"), min, max, log); panel.add(new SliderPanel(model), BorderLayout.CENTER); panel.add(new NumberPanel(model), BorderLayout.SOUTH);
We left out the standard boilerplate to create the window. The RestrictedNumberModel is initialized with the number 1.2, and with the min (1.1) and max (1.2) set. Then we add a SliderPanel and a NumberPanel, both showing the same number. This allows the user to change the number either by dragging the slider or by typing the exact number.
The Multiplier Example shows how to create a compound model and also how to create a model with bidirectional dependencies between the values in the model. The point is that with bidirectional dependencies you need to detect which value the user is changing and then stick with the value he set, and adjust the other ones (while ignoring the change events from the other ones).
There are many more examples in mvc/src/test/java/tudelft/utilities/mvc/panels.
Thread safety, parallel editing
MVC2 is not thread safe. If different editors try to modify different components simultaneously, conflicts may arise. Thread safety was not considered in the design because normally GUIs are not multithreaded. However, some issues related to this can still arise. For instance a custom table cell editor may open, and it may stay open until you press enter, if you go editing something else and then later press enter, odd things may happen.