Version 47 (modified by wouter, 9 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
StringPanelA panel allowing the user to enter an arbitrary text
NumberPanelPanel 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
SliderPanelA 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.
CheckboxPanelA panel allowing the user to change a boolean to true or false
OnOffButtonA button allowing the user to toggle a boolean between true and false
ComboBoxallows to make selections in SelectionModel
MapPanelshowing 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
CustomMapEditora 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.
ListViewPanelShows 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.
EditableListViewPanelAs 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.

model and immutable object

Usually the base objects are immutable, while the model that edits the objects is by definition mutable. One approach would be to create an adapter that takes the base model and maps an adapter around it. But for more complex base objects, the adapter tends to become unreadable very quick.

Instead we recommend to make a XModel for every base object X. The XModel then contains a set of YModels for all Y that are in X. Also the XModel attaches listeners to these YModels so that it can notify its listeners about changes in submodels. This way, XModel can deliver the proper YModel for contained objects without extra adapter code. XModel can also contain a getX() that returns a base object X from the current settings in the XModel.

Note: See TracWiki for help on using the wiki.