GUI - model code organization
In the time of writing the core OpenCPN codebase consists of more than 300000 lines. A code base this large requires some kind of organization.
The traditional way to organize GUI applications like OpenCPN is the Model - View - Controller (MVC) pattern. However, for various reasons OpenCPN does not follow this. Instead, a late push has been done to split the code into two pieces named Model and Gui where Model indeed corresponds to the MVC "Model" while Gui corresponds to the MVC "View" and "Controller" parts.
The overall picture thus becomes:
┌──────────────────────────────────┐ │ Gui (View + Controller) │ └──────────────────────────────────┘
┌──────────────────────────────────┐ │ Model │ └──────────────────────────────────┘
In the traditional layered software approach this means:
-
Code in the GUI can call any code, be it in Gui or Model.
-
Code in the Model can only call the Model; it can not call the GUI
Violating these rules typically leads to linker errors when linking the tests and/or opencpn-cmd targets.
Typical Model parts are communication drivers, databases like routes and configuration data, various datatypes, plugin loading, etc. Having these in a model part means that they can be updated, tested and used without paying attention to the Gui as long as the interface is kept stable. This considerably simplifies code maintenance.
Definition of the Gui and Model parts
At the time of writing, the Model is defined by the variable MODEL_SRC in the top level CMakeLists.txt. Remaining source file makes up the Gui. Work to make this more visible is likely to be done, for example by creating separate subdirectories like src/gui and src/model instead of a single src/ directory.
Model → Gui communication.
From time to time Model code still needs to invoke the Gui. One example could be a communication driver which needs some user data which should be retrieved using a dialog box. Another example could be that the same driver needs to send a message to all plugins using the BroadcastToAllPlugins() method which currently resides in the Gui. There are three possible ways to handle this.
-
Refactor the code so that the called method is moved to a Model component.
-
Send an asynchronous signal to the Gui without any feedback.
-
Use a callback.
Of these, moving the code to the Model is generally a good thing if feasible. However, things like a dialog just cannot be moved, it will always have Gui dependencies.
Otherwise, sending a signal is the simplest solution. It should be used when possible. The basic limitation is that this is an asynchronous fire-and-forget approach, the model will not get anything back from the Gui.
If sending a signal does not fit the bill, a callback should be used. This is synchronous, and the Model can get a return value from the Gui
Using signals
Sending signals is done using an
.
This is declared in the observable_evtvar.h header file.EventVar
The Model part defines and signals to the EventVar. The declaration is done in the header:
#include "observable_evtvar.h" class ModelClass { public: /** Notified on SomeChange with a strinng available in GetString(). */ EventVar some_change;
Note the comment, an
without a comment is hard to track down.EventVar
In the implementation file:
void ModelClass::SomeMethod() { .... some_change;.Notify("this happened"); ... }
There are many overloads for
which could be used to carry not only
strings but also pointers, int/bool, etc.Notify()
This is basically it from the Model perspective. One more more Gui component could listen to SomeChange, but this does not affect ModelClass.
The Gui performs the listening. First, in the header declare an ObsListener
#include "model_class.h"
class GuiClassn { ... private: ObsListener some_change_listener; }
And then, typically in the constructor, set up the listening in the implementation .cpp file:
void GuiClass::GuiClass(ModelClass& model_class) { auto action = [&](ObservedEvt ev) { wxString s = ev.GetString(); do something useful } some_change_listener.Init(model_class.some_change, action);
Here, GuiClass obviously needs to have access to an instance of ModelClass to be able to listen to it. In our case this is often a global variable, but this varies and could be more or less complicated.
Using a callback
To use a callback, the model part defines it in a header, where there also is a accessor for the Gui to set it. The example assumes the function takes a string argument and returns a bool. This is just an example, the signature could be anything.
#include <functional> ... class ModelClass { public: ModelClass::ModelClass() ; void SetCallback(std::function<bool(const std::string&)> cb) { m_callback = cb; } private: std::function<bool(const std::string&)> m_callback; }
In the implementation .cpp file we first ensure that m_callback always have a defined, default value which basically does nothing without crashing
ModelClass::ModelClass() : m_callback([](const std::string) { return false; }) { ... }
It is now possible to invoke the callback and get the result from the
Gui (assuming that
has been called):SetCallback()
ModelClass::SomeFunction() { ... bool result = m_callback("foo"); ... }
In the Gui the callback is set, typically using a lambda in the constructor like
#include "model_class.h" ... GuiClass::GuiClass(ModelClass& model_class) { ... model_class.SetCallback([&](const std::string& s) { do something with s; return true or false; });
And that’s it. The model can now effectively call a function in the Gui without any knowledge of it. This upholds the basic promise that the model is not linked to the Gui in any way.