Monday, April 18, 2016

MVC and Subject-Observer pattern in C++ & QT

Leave a Comment

Disclaimer:

As the first answers have duly noted, using MVC in the current example case is overkill. The goal of the question is to understand the underlying concepts, with a simple example, to be able to use them in a bigger program where more complex data (arrays, objects) is modified.


I am trying to implement the MVC pattern in C++ & QT, similar to the question here:

Other MVC Questions

The program has 2 line edits:

  • mHexLineEdit
  • mDecLineEdit

3 buttons

  • mConvertToHexButton
  • mConvertoDecButton
  • mClearButton

and just modifies strings.

enter image description here

The difference with the other question is that I am trying to implement the Subject/Observer pattern to update the View once the Model is changed.

Model.h

#ifndef MODEL_H #define MODEL_H  #include <QString> #include <Subject>  class Model : virtual public Subject { public:     Model();     ~Model();     void convertDecToHex(QString iDec);     void convertHexToDec(QString iHex);     void clear();      QString getDecValue() {return mDecValue;}     QString getHexValue() {return mHexValue;} private:     QString mDecValue;     QString mHexValue; }; #endif // MODEL_H 

Model.cpp

#include "Model.h"  Model::Model():mDecValue(""),mHexValue(""){} Model::~Model(){}  void Model::convertDecToHex(QString iDec) {     mHexValue = iDec + "Hex";      notify("HexValue"); }  void Model::convertHexToDec(QString iHex) {     mDecValue = iHex + "Dec";      notify("DecValue"); }  void Model::clear() {   mHexValue = "";   mDecValue = "";    notify("AllValues"); } 

View.h

#ifndef VIEW_H #define VIEW_H  #include <QtGui/QMainWindow> #include "ui_View.h" #include <Observer>  class Controller; class Model; class View : public QMainWindow, public Observer {     Q_OBJECT  public:     View(QWidget *parent = 0, Qt::WFlags flags = 0);     ~View();     void setController(VController* iController);     void setModel(VModel* iModel);     QString getDecValue();     QString getHexValue(); public slots:     void ConvertToDecButtonClicked();     void ConvertToHexButtonClicked();     void ClearButtonClicked(); private:      virtual void update(Subject* iChangedSubject, std::string iNotification);      Ui::ViewClass ui;      Controller*  mController;     Model*        mModel; };  #endif // VIEW_H 

View.cpp

#include "View.h" #include "Model.h" #include "Controller.h" #include <QSignalMapper>  VWorld::VWorld(QWidget *parent, Qt::WFlags flags) : QMainWindow(parent, flags) {     ui.setupUi(this);      connect(ui.mConvertToHexButton,SIGNAL(clicked(bool)),this,SLOT(ConvertToHexButtonClicked()));     connect(ui.mConvertToDecButton,SIGNAL(clicked(bool)),this,SLOT(ConvertToDecButtonClicked()));     connect(ui.mClearButton,SIGNAL(clicked(bool)),this,SLOT(ClearButtonClicked())); }  View::~View(){}  void View::setController(Controller* iController) {     mController = iController;      //connect(ui.mConvertToHexButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnConvertToHexButtonClicked(this)));     //connect(ui.mConvertToDecButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnConvertToDecButtonClicked(this)));     //connect(ui.mClearButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnClearButtonClicked(this))); }  void View::setModel(Model* iModel) {     mModel = iModel;      mModel->attach(this); }  QString View::getDecValue() {     return ui.mDecLineEdit->text(); }  QString View::getHexValue() {     return ui.mHexLineEdit->text(); }  void View::ConvertToHexButtonClicked() {     mController->OnConvertToHexButtonClicked(this); }  void View::ConvertToDecButtonClicked() {     mController->OnConvertToDecButtonClicked(this); }  void VWorld::ClearButtonClicked()  {     mController->OnClearButtonClicked(this); }  void View::update(Subject* iChangedSubject, std::string     iNotification) {     if(iNotification.compare("DecValue") == 0)     {         ui.mDecLineEdit->setText(mModel->getDecValue());     }     else if(iNotification.compare("HexValue") == 0)     {         ui.mHexLineEdit->setText(mModel->getHexValue());     }     else if(iNotification.compare("AllValues") == 0)     {         ui.mDecLineEdit->setText(mModel->getDecValue());         ui.mHexLineEdit->setText(mModel->getHexValue());     }     else     {         //Unknown notification;     } } 

Controller.h

#ifndef CONTROLLER_H #define CONTROLLER_H  //Forward Declaration class Model; class View;  class Controller  { public:     Controller(Model* iModel);     virtual ~Controller();     void OnConvertToDecButtonClicked(View* iView);     void OnConvertToHexButtonClicked(View* iView);     void OnClearButtonClicked(View* iView); private:     Model* mModel; }; #endif // CONTROLLER_H 

Controller.cpp

#include "Controller.h" #include "Model.h" #include "View.h"  Controller::Controller(Model* iModel):mModel(iModel){}  Controller::~Controller(){}  void Controller::OnConvertToDecButtonClicked(View* iView)  {   QString wHexValue = iView->getHexValue();    mModel->convertHexToDec(wHexValue); }  void Controller::OnConvertToHexButtonClicked(View* iView)  {   QString wDecValue = iView->getDecValue();    mModel->convertDecToHex(wDecValue); }  void Controller::OnClearButtonClicked(View* iView)  {   mModel->clear(); } 

main.cpp

#include "View.h" #include "Model.h" #include "Controller.h" #include <QtGui/QApplication>  int main(int argc, char *argv[]) {     QApplication a(argc, argv);     Model wModel;     View wView;     Controller wCtrl(&wModel);     wView.setController(&wCtrl);     wView.setModel(&wModel);     wView.show();     return a.exec(); } 

I can post the Subject/Observer files later if they become relevant.

Asides from general comments, can someone please answer these questions:

1) Would it be better to connect the buttons signals to the Controller slots directly (like in the portion commented out in View::setController)? The Controller needs to know which View called so it can use the proper information from the View doesn't it? This would mean either:

a) Reimplement a QSignalMapper or

b) Upgrade to Qt5 and VS2012 in order to connect directly with lambdas (C++11);

2) What is optimal way to know what has changed when update is called by the Model ? Is it a switch/looping through all possibilities, a predefined map... ?

3) Also, should I pass the necessary info through the update function, or let View check the required values the of Model once it gets notified ?

In the second case the View needs to access the Model data...


EDIT:

In particular in the case where there is a lot of data modified. In example, there is a load button and a whole object/array is modified. Passing a copy to the View through the signal/slot mechanism would be time consuming.

From ddriver's answer

Now, it would be a different matter if you have a traditional "list of items" model and your view is a list/tree/table, but your case is one of a single form.


4) Should the View need to have a reference to the Model ? since it only acts with on controller? (View::setModel())

If not, how does it register itself as an observer to the Model ?

4 Answers

Answers 1

You are overthinking something practically trivial. You are also over-engineering.

Yes, it is always a good idea to abstract logic from UI, but in the case of your particular example, the extra abstraction layer of data is not necessary, mainly because you do not have different data sets, you only have two values which are really part of the logic and do not merit a data abstraction layer.

Now, it would be a different matter if you have a traditional "list of items" model and your view is a list/tree/table, but your case is one of a single form.

In your case, the proper design would be a Converter class which includes your current model data, controller and conversion logic, and a ConverterUI class, which is essentially your view form. You save on boilerplate code and component interconnect.

That being said, you are free to go through unnecessary lengths and for the overkill.

1 - you emit the modification data from the view to controller connection, so it will always come from the appropriate view, the controller is not concerned with which view it is, how many views there might be, or if there is a view at all. QSignalMapper is an option, but it is rather limited - it only supports a single parameter and only a few parameter types. I myself honestly prefer one line slots, they are more flexible and not all that hard to write, plus they are reusable code, which sometimes comes handy. Lambdas are a new a cool feature, and using those will sure make you look cooler, but in your particular case they won't make that much of a difference, and lambdas alone do not merit switching to Qt5. That being said, there are a whole lot more reasons to update to Qt5 besides lambdas.

2 - signals and slots, you know what you are editing, so you only update that

3 - passing the values through the signals is more elegant, and it doesn't require your controller to keep a reference to the view(s) and managing which view it is, as discussed in 1

4 - as it is obvious from the MVC diagram, the view has a reference to the model for reading only. So if you want a "by the book" MVC, that's what you need.

enter image description here

I have improved (somewhat, still untested) on the previous example, now there is Data which is just a POD, the Model which maintains a data set, the Controller which iterates the underlying Model data set and reads and writes data, the View which is tied to a controller, and the App which brings together a model and two independent controllers for it and two independent views. There is limited functionality - you can go to the next available data set entry, modify or remove, there is no adding or reordering in this example, you could implement those as an exercise. Changes will propagate back down to the model, and thus be reflect in every controller and associated view. You can have multiple different views tied to a single controller. The model of a controller is currently fixed, but if you want to change it, you must go through a procedure similar to setting the controller for a view - that is, disconnect the old one before connecting to the new one, although if you are deleting the old one, it will automatically disconnect.

struct Data {     QString d1, d2; };  class Model : public QObject {     Q_OBJECT     QVector<Data> dataSet;   public:     Model() {       dataSet << Data{"John", "Doe"} << Data{"Jane", "Doe"} << Data{"Clark", "Kent"} << Data{"Rick", "Sanchez"};     }     int size() const { return dataSet.size(); }   public slots:     QString getd1(int i) const { return i > -1 && i < dataSet.size() ? dataSet[i].d1 : ""; }     QString getd2(int i) const { return i > -1 && i < dataSet.size() ? dataSet[i].d2 : ""; }     void setd1(int i, const QString & d) {       if (i > -1 && i < dataSet.size()) {         if (dataSet[i].d1 != d) {           dataSet[i].d1 = d;           emit d1Changed(i);         }       }     }     void setd2(int i, const QString & d) {       if (i > -1 && i < dataSet.size()) {         if (dataSet[i].d2 != d) {           dataSet[i].d2 = d;           emit d2Changed(i);         }       }     }     void remove(int i) {       if (i > -1 && i < dataSet.size()) {         removing(i);         dataSet.remove(i);         removed();       }     }   signals:     void removing(int);     void removed();     void d1Changed(int);     void d2Changed(int); };  class Controller : public QObject {     Q_OBJECT     Model * data;     int index;     bool shifting;   public:     Controller(Model * _m) : data(_m), index(-1), shifting(false) {       connect(data, SIGNAL(d1Changed(int)), this, SLOT(ond1Changed(int)));       connect(data, SIGNAL(d2Changed(int)), this, SLOT(ond2Changed(int)));       connect(data, SIGNAL(removing(int)), this, SLOT(onRemoving(int)));       connect(data, SIGNAL(removed()), this, SLOT(onRemoved()));       if (data->size()){         index = 0;         dataChanged();       }     }   public slots:     QString getd1() const { return data->getd1(index); }     QString getd2() const { return data->getd2(index); }     void setd1(const QString & d) { data->setd1(index, d); }     void setd2(const QString & d) { data->setd2(index, d); }     void remove() { data->remove(index); }   private slots:     void onRemoving(int i) { if (i <= index) shifting = true; }     void onRemoved() {       if (shifting) {         shifting = false;         if ((index > 0) || (index && !data->size())) --index;         dataChanged();       }     }     void ond1Changed(int i) { if (i == index) d1Changed(); }     void ond2Changed(int i) { if (i == index) d2Changed(); }     void fetchNext() {       if (data->size()) {         index = (index + 1) % data->size();         dataChanged();       }     }   signals:     void dataChanged();     void d1Changed();     void d2Changed(); };  class View : public QWidget {     Q_OBJECT     Controller * c;     QLineEdit * l1, * l2;     QPushButton * b1, * b2, * bnext, * bremove;   public:     View(Controller * _c) : c(nullptr) {       QVBoxLayout * l = new QVBoxLayout;       setLayout(l);       l->addWidget(l1 = new QLineEdit(this));       l->addWidget(b1 = new QPushButton("set", this));       connect(b1, SIGNAL(clicked(bool)), this, SLOT(setd1()));       l->addWidget(l2 = new QLineEdit(this));       l->addWidget(b2 = new QPushButton("set", this));       connect(b2, SIGNAL(clicked(bool)), this, SLOT(setd2()));       l->addWidget(bnext = new QPushButton("next", this));       l->addWidget(bremove = new QPushButton("remove", this));       setController(_c);     }     void setController(Controller * _c) {       if (_c != c) {         if (c) {           disconnect(c, SIGNAL(d1Changed()), this, SLOT(updateL1()));           disconnect(c, SIGNAL(d2Changed()), this, SLOT(updateL2()));           disconnect(c, SIGNAL(dataChanged()), this, SLOT(updateForm()));           disconnect(bnext, SIGNAL(clicked(bool)), c, SLOT(fetchNext()));           disconnect(bremove, SIGNAL(clicked(bool)), c, SLOT(remove()));           c = nullptr;         }         c = _c;         if (c) {           connect(c, SIGNAL(d1Changed()), this, SLOT(updateL1()));           connect(c, SIGNAL(d2Changed()), this, SLOT(updateL2()));           connect(c, SIGNAL(dataChanged()), this, SLOT(updateForm()));           connect(bnext, SIGNAL(clicked(bool)), c, SLOT(fetchNext()));           connect(bremove, SIGNAL(clicked(bool)), c, SLOT(remove()));         }       }       updateForm();     }   public slots:     void updateL1() { l1->setText(c ? c->getd1() : ""); }     void updateL2() { l2->setText(c ? c->getd2() : ""); }     void updateForm() {       updateL1();       updateL2();     }     void setd1() { c->setd1(l1->text()); }     void setd2() { c->setd2(l2->text()); } };  class App : public QWidget {     Q_OBJECT     Model m;     Controller c1, c2;   public:     App() : c1(&m), c2(&m) {       QVBoxLayout * l = new QVBoxLayout;       setLayout(l);                 l->addWidget(new View(&c1));                 l->addWidget(new View(&c2));     } }; 

Answers 2

I am going to answer this in the context of Passive-View and Model-View-Presenter

Model View Presenter

which (see Wikipedia)

is a derivation of the model–view–controller (MVC) architectural pattern, and is used mostly for building user interfaces.

The Model:

Changes to the Model / Subject must be observable. Most of the Subject / Observer details are handled of the signal / slot mechanism, so for this simple use case it is sufficient to make the Model observable by giving it a signal. Because the online compilers don't support Qt, I will use boost::signals2 and std::string.

class Model { public:      Model(  )     {     }      void setValue( int value )     {         value_ = value;         sigValueChanged( value_ );     }      void clear()     {         value_ = boost::none;         sigValueChanged( value_ );     }      boost::optional<int> value() const     {         return value_;     }      boost::signals2::signal< void( boost::optional<int> ) > sigValueChanged;  private:      boost::optional<int> value_; }; 

The Presenter:

Here the Presenter is Observer, not the View. The Presenters job is to translate the integral value of the model into a textual representation for display. Here we really have two controller, one for the decimal notation and one for the hexadecimal notation. Although possibly over-designed for this simple case, we create an abstact base class for the Presenter.

class AbstractPresenter { public:      AbstractPresenter()         : model_( nullptr )         , view_( nullptr )     {     }      void setModel( Model& model )     {         model_ = &model;         model.sigValueChanged.connect( [this]( int value ){             _modelChanged( value ); } );     }      void setView( TextView& view )     {         view_ = &view;     }      void editChanged( std::string const& hex )     {         _editChanged( hex );     }  private:      virtual void _editChanged( std::string const& ) = 0;     virtual void _modelChanged( boost::optional<int> ) = 0;  protected:      Model *model_;     TextView  *view_; }; 

and an implementation for the decimal Presenter

class DecPresenter     : public AbstractPresenter {     void _editChanged( std::string const& dec ) override     {         int value;         std::istringstream( dec ) >> value;          model_->setValue( value );     }      void _modelChanged( boost::optional<int> value ) override     {         std::string text;          if( value )         {                         text = std::to_string( *value );;         }          view_->setEdit( text );     } }; 

and an implementation for the hexadecimal case.

class HexPresenter     : public AbstractPresenter {     void _editChanged( std::string const& hex ) override     {         int value;         std::istringstream( hex ) >> std::hex >> value;          model_->setValue( value );     }      void _modelChanged( boost::optional<int> value ) override     {         std::string text;          if( value )         {             std::stringstream stream;             stream << std::hex << *value;              text = stream.str();         }          view_->setEdit( text );     } }; 

And lastly an aggregated Presenter

class Presenter { public:      Presenter()         : model_( nullptr )     {     }      void setModel( Model& model )     {         model_ = &model;         hexPresenter.setModel( model );         decPresenter.setModel( model );         }      void setView( View& view )     {         hexPresenter.setView( view.hexView );         decPresenter.setView( view.decView );         }      HexPresenter hexPresenter;     DecPresenter decPresenter;          void clear()     {         model_->clear();     }  private:      Model * model_; }; 

The View:

The views only job displays a text value, so we can use the same View for both cases.

class TextView { public:      TextView( std::string which )         : which_( which )     {     }      void setPresenter( AbstractPresenter& presenter )     {         presenter_ = &presenter;     }      void setEdit( std::string const& string )     {         std::cout << which_ << " : " << string << "\n";     }  private:      AbstractPresenter* presenter_;     std::string which_; }; 

And the aggregated view.

class View { public:      View()         : hexView( "hex" )         , decView( "dec" )     {     }      TextView hexView;     TextView decView; }; 

In a Qt application, each view would have a pointer to the corresponding label and it would set the text of the label.

    void setEdit( std::string const& string )     {         label->setText( QSting::fromStdString( string ) );     } 

In this context we can also answer Question 1.

1) Would it be better to connect the buttons signals to the Controller slots directly (like in the portion commented out in View::setController)?

Since we want a "Passive-View" without logic it is perfectly ok to connect directly to the controller if the controls parameters fit. If you have to convert, say a std::string to a QString, you can create a local slot that does the conversion and passes the value on or use lambda for the job in Qt5.

The Controller needs to know which View called so it can use the proper information from the View doesn't it?

No it does not. If it needs to do different things there should either be separate presenters or a presenter with separate methods for each case.

2) What is optimal way to know what has changed when update is called by the Model ? Is it a switch/looping through all possibilities, a predefined map... ?

The optimal way is for the model to tell the observer what changed. This can be done with different signals or with an event that contains the information. In this case there is only one value, so there is no difference.

3) Also, should I pass the necessary info through the update function, or let View check the required values the of Model once it gets notified ?

Pass the information to avoid redundant change calculations in the presenter.

4) Should the View need to have a reference to the Model ?

No, at least not in MVP.

The Pieces can be put together like this:

int main() {     Model model;     Presenter presenter;     View view;      presenter.setModel( model );     presenter.setView( view );      view.decView.setPresenter( presenter.decPresenter );     view.hexView.setPresenter( presenter.hexPresenter );      // simulate some button presses      presenter.hexPresenter.editChanged( "42" );     presenter.clear();     presenter.decPresenter.editChanged( "42" ); } 

which creates the following output

hex : 42 dec : 66 hex : dec : hex : 2a dec : 42 

Live on Coliru

Answers 3

1) Would it be better to connect the buttons signals to the Controller slots directly (like in the portion commented out in View::setController)?

Yes, since the slots you are calling just one-liners.

The Controller needs to know which View called so it can use the proper information from the View doesn't it?

Not necessarily. You shouldn't pass this in your signals. You should pass the data that has changed. For example, in the controller class you can have a slot called void SetDecValueTo(int) or void SetDecValueTo(QString) and just call it from your view instead of passing this.

This would mean either:

a) Reimplement a QSignalMapper or

b) Upgrade to Qt5 and VS2012 in order to connect directly with lambdas (C++11);

As above, you don't really need this. But in general, lambdas are the way of the future.

2) What is optimal way to know what has changed when update is called by the Model ? Is it a switch/looping through all possibilities, a predefined map... ?

Pass the relevant data in your signals/slots. For example, in your model you can have a signal void DecValueChanged(int) and void HexValueChanged(int). You connect these to your view's slots void UpdateDecValue(int) and void UpdateHexValue(int).

3) Also, should I pass the necessary info through the update function, or let View check the required values the of Model once it gets notified ?

See above paragraph.

In the second case the View needs access the Model data...

4) Should the View need to have a reference to the Model ? since it only acts with on controller? (View::setModel())

If not, how does it register itself as an observer to the Model ?

In this particular case it doesn't need to have ref to the model. You can either do all the connections in main() or do it in the view and just not keep the ref to the model.

Lastly, since there is not a lot of controlling that needs to be done, you can forego the controller class and implement its functionality in the view, as is frequently done in Qt. See Model/View Programming.

Answers 4

There is a significant variations of different MVC architectures, especially how the components should collaborate. I prefer an approach where the model and the view are independent from each other as well as from the controller. The benefit of this design is that you can easily reuse the model with another view, or vice-versa; reusing a view with another model.

This is the MVC component collaboration that Apple suggests: https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

Apples MVC

So how does it work?

The View. The view can be considered to just be a dummy. It doesn't now anything about the outside world and can only output representation of information which will be decided by the model.

The Model. This is the main component, it is your software. It manages the data, logic and rules of the application.

The Controller. The controller's responsibility is to make sure that the model and view understand each other.

Think of the view as your body, the model as your brain (who you are) and the controller would be the electrical signals from and to your brain.

Example

I currently don't have a compiler handy, so can't test this, but I'll try it when I get to work tomorrow. The important part to notice is that both the view and model are independent from each other and the controller.

Model

class PersonModel : public QObject {     Q_OBJECT      QString m_sFirstname;     QString m_sLastname;  public:     Model() : m_sFirstname(""), m_sLastname("")     {}     ~Model();      void setFirstname(const QString & sFirstname)     {         m_sFirstname = sFirstname;         emit firstnameChanged(sFirstname);     }      void setLastname(const QString & sLastname)     {         m_sLastname = sLastname;         emit lastnameChanged(sLastname);     }  signals:     void firstnameChanged(const QString &);     void lastnameChanged(const QString &); }; 

View

class PersonView : public QWidget {     Q_OBJECT      QLabel * m_pFirstnameLabel; // should be unique_ptrs     QLabel * m_pLastnameLabel;  //  public:     PersonView() :         m_pFirstnameLabel(new QLabel),         m_pLastnameLabel(new QLabel)     {         auto m_pMainLayout = new QHBoxLayout;         m_pMainLayout->addWidget(m_pFirstnameLabel);         m_pMainLayout->addWidget(m_pLastnameLabel);         setLayout(m_pMainLayout);     }      ~PersonView()     {         delete m_pFirstnameLabel;         delete m_pLastnameLabel;     }  public slots:     void setFirstnameText(const QString & sFirstname)     {         m_pFirstnameLabel->setText(sFirstname);     }      void setLastnameText(const QString & sLastname)     {         m_pLastnameLabel->setText(sLastname);     } }; 

Controller

class PersonController : public QObject {     Q_OBJECT      PersonView * m_pPersonView;     // better off as unique ptr     PersonModel * m_pPersonModel;  public:     PersonController() :         m_pPersonView(new PersonView),         m_pPersonModel(new PersonModel)     {         connect(m_pPersonModel, &PersonModel::firstnameChanged, m_pPersonView, &PersonView::setFirstnameText);         connect(m_pPersonModel, &PersonModel::lastnameChanged, m_pPersonView, &PersonView::setLastnameText);          m_pPersonModel->setFirstname("John");         m_pPersonModel->setLastname("Doe");          m_pPersonView->show();      }      ~PersonController()     {         delete m_pPersonView;         delete m_pPersonModel;     } }; 

Notes

  • In a bigger project there will probably be more than just one MVC. In that case the communication would be through the controllers.

  • It's also possible to add additional models and views, using one controller. Multiple views can for example be used to show a set of data in different ways.

  • As I mentioned in the beginning there are other varations of MVC architectures. One which for example ddriver suggest as an answer.

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment