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

Denial of responsibility:

As the first answers have been duly noted, using MVC in the current example case is redundant. The task of the question is to understand the basic concepts using a simple example in order to be able to use them in a larger program, where more complex data (arrays, objects) are modified.


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

Other MVC issues

The program has 2 lines of editing:

  • mHexLineEdit
  • mDecLineEdit

3 buttons

  • mConvertToHexButton
  • mConvertoDecButton
  • mClearButton

and just changes the lines.

enter image description here

The difference with another question is that I am trying to implement a Subject / Observer template to update the view after changing the Model.

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 publish Subject / Observer files later if they become relevant.

In general terms, someone can answer these questions:

1) Would it be better to directly connect the button signals to the controller slots (as in the part commented in View::setController )? The controller needs to know which view is being called so that it can use the correct information from the view, right? That would mean:

a) Override QSignalMapper or

b) Switch to Qt5 and VS2012 to connect directly to lambdas (C ++ 11) ;

2) What is the best way to find out what has changed when a model is called by an update? Is this a switch / cycle of all features, a predefined map ...?

3) In addition, should I transmit the necessary information through the update function or allow View to check the required model values ​​after receiving it?

In the second case, View must access the Model ... data.


EDIT:

In particular, when there is a lot of modified data. The example has a download button, and the entire object / array changes. Transferring a copy to a presentation via a signal / slot mechanism will take a long time.

From ddriver answer

Now it would be different if you had a traditional “list of items” model and your view is a list / tree / table, but your case is one of one form.


4) If the View should have a link to the model? since it only acts on the controller? (View :: setModel ())

If not, how is he registered as an observer in the Model?

+5
source share
4 answers

You oversubscribed something almost trivial. You are also excessive.

Yes, it is always useful to distract logic from the user interface, but in the case of your specific example, an additional level of data abstraction is not needed, mainly because you do not have different data sets, you only have two values ​​that really are part of the logic and do not deserve level of data abstraction.

Now it would be different if you had a traditional “list of items” model and your view is a list / tree / table, but your case is one of one form.

In your case, the correct design would be the Converter class, which includes the current model data, the controller and the conversion logic, and the ConverterUI class, which is essentially your form of presentation. You save the template code and component connection.

Having said that, you are free to go through excessive length for excess.

1 - you give these changes from the view to the connection with the controller, therefore it will always be displayed in the corresponding view, the controller does not have any relation to what kind, how many views can be, or if there is a look at all. QSignalMapper is an option, but it is quite limited - it supports only one parameter and only a few types of parameters. I myself honestly prefer one linear slot, they are more flexible and not everything that is difficult to write, plus they are reusable code, which is sometimes useful. Lambdas is a great new feature, and using these functions will make you look cooler, but in your particular case they will not make that much difference, and only lambdas does not deserve the transition to Qt5. However, there are many more reasons to upgrade Qt5 other than lambdas.

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

3 - the transmission of values ​​through signals is more elegant and does not require your controller to maintain a link to the view and control which view, as described in 1

4 - as seen from the MVC diagram, the view has a read-only reference. Therefore, if you want MVC “by the book”, then what you need.

enter image description here

In the previous example, I improved (somewhat, still unverified), now there is Data , which is just a regular structure, you definitely do not want this to be a QObject if you have many of them, since QObject is a huge amount of memory, Model , which supports the data set, Controller , which iterates the basic data set Model and reads and writes data, View , which is attached to the controller and App , which combines the model and two independent controllers for it and two independent views. There is limited functionality - you can go to the next available record of the data set, change or delete, in this example there is no addition or reordering, you can implement them as an exercise. Changes will propagate back to the model and, thus, reflect in each controller and corresponding representation. You can have several different views tied to one controller. The controller model is currently fixed, but if you want to change it, you must go through a procedure similar to setting the controller for presentation, that is, disconnect the old one before connecting to the new one, although if you delete the old one, it will automatically shut down.

 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)); } }; 
+3
source

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

Model View Presenter

which (see Wikipedia)

This is the output of the model-view-controller (MVC) architectural template and is mainly used to create user interfaces.

Model :

Changes in the model / subject should be observable. Most Subject / Observer parts are processed by the signal / slot mechanism, so for this simple use it is enough to make the model observable by giving it a signal that emits a value. Since online compilers do not 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_; }; 

Lead :

Here the Leader is the Observer, not the View. The task of Teachers is to convert the integral value of the model into a textual representation for display. Here we have two controllers: one for decimal notation and one for hexadecimal notation. Although this simple case may be too elaborate, we are creating the abstact base class for 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 implementation for 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 implementation for the hex 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 finally, the 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_; }; 

View :

The view-only task 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 will have a pointer to a corresponding label, and it will set the label text.

  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 button signals to the Controller slots directly (as in the part commented out in View :: setController)?

Since we need a “passive view” without logic, it’s quite normal to connect directly to the controller if the control parameters are suitable. If you need to convert, say std :: string in QString, you can create a local slot that performs the conversion and passes the value or uses a lambda for the job in Qt5.

The controller needs to know which view is being called, so it can use the right information from the view, right?

No no. If he needs to do different things, then there must be either separate speakers or a moderator with separate methods for each case.

2) What is the best way to find out what has changed when a model is called by an update? Is this a switch / cycle of all features, a predefined map ...?

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

3) In addition, should I transmit the necessary information through the update function or allow View to check the required model values ​​after receiving it?

Pass information to avoid calculating redundant changes in the presenter.

4) If the View should have a link to the model?

No, at least not in MVP.

Parts can be assembled as follows:

 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 produces the following output

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

Live on coliru

+3
source

1) Would it be better to connect the button signals to the Controller slots directly (as in the part commented out in View :: setController)?

Yes, since you only call single-line slots.

The controller needs to know which view is being called, so it can use the right information from the view, right?

Not necessary. You should not pass this into your signals. You must transfer data that has been changed. For example, in a 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 .

That would mean:

a) Repeat the implementation of QSignalMapper or

b) Switch to Qt5 and VS2012 to connect directly to lambdas (C ++ 11);

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

2) What is the best way to find out what has changed when a model is called by an update? Is this a switch / cycle of all features, a predefined map ...?

Transfer the relevant data to your signals / slots. For example, in your model, you can have the signal void DecValueChanged(int) and void HexValueChanged(int) . You connect them to your viewing slots void UpdateDecValue(int) and void UpdateHexValue(int) .

3) In addition, should I transmit the necessary information through the update function or allow View to check the required model values ​​after receiving it?

See paragraph above.

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

4) If the View should have a link to the model? since it only acts on the controller? (View :: setModel ())

If not, how is he registered as an observer in the Model?

In this particular case, he does not need to have a reference to the model. You can either make all the connections in main() , or do it in the view, and just don’t specify a link to the model.

Finally, since there is not much control over what needs to be done, you can abandon the controller class and implement its functions in the view, as is often done in Qt. See Model / View Programming .

0
source

There are significant differences in the various MVC architectures, especially in how the components should interact. I prefer the approach when the model and view are independent both from each other and from the controller. The advantage of this design is that you can easily reuse a model with a different look or vice versa; reusing the view with a different model.

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

MVC Apples

So how does it work?

View. Representation can be considered just a mannequin. It has nothing to do with the outside world and can only display information that will be determined by the model.

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

Controller. The responsibility of the dispatcher is to ensure that the model and view understand each other.

Think about how your body, model, how your brain (who you are), and the controller are electrical signals from your brain and to your brain.

Example

I don’t currently have a compiler, so I can’t check it, but I will try when I get to work tomorrow. It is important to note that both the view and the model are independent of 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 larger project, there will likely be more than one MVC. In this case, communication will be through controllers.

  • You can also add additional models and views using a single controller. For example, multiple views can be used to display a dataset in different ways.

  • As I mentioned at the beginning, there are other variations of the MVC architecture. One that, for example, ddriver, offers as an answer.

0
source

Source: https://habr.com/ru/post/1246490/


All Articles