Skip to content

Commit 3973eed

Browse files
committed
Add an MVVM example
1 parent 44bf515 commit 3973eed

15 files changed

Lines changed: 323 additions & 11 deletions

src/ap/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,4 @@ target_sources(ap
1818
target_link_libraries(ap PRIVATE gtk4-settings)
1919

2020
add_subdirectory(mvc)
21-
# add_subdirectory(mvvm)
21+
add_subdirectory(mvvm)

src/ap/README.md

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,13 @@ View → Controller → Model
4545
### 3. GTK4
4646
- [Refer](https://docs.gtk.org/gtk4/getting_started.html)
4747

48-
### 4. Examples
49-
### 4.1. simple_ap
50-
- Cos:
51-
- Quick, Simple
52-
- Pos:
53-
- Dependency: e.g. what happen when we delete Gtk::Label m_labelMonitorA;
54-
- Scalability:
55-
- Reusability:
48+
### 4. Trade-offs: MVC vs MVVM
5649

57-
### 4.2. mvc_ap
50+
| Aspect | MVC | MVVM |
51+
|---------------------|---------------------------------------------------------------------|----------------------------------------------------------------------|
52+
| **Complexity** | Lower — **Controller** is a thin pass-through | Slightly higher — **ViewModel** adds an extra layer |
53+
| **Coupling** | Views know both **Controller** and **Model** (e.g., for initial data) | **Views** know only the **ViewModel** |
54+
| **Testability** | Controller is testable, but **Views** are still tied to **Model** for reads | **ViewModel** is fully testable without GTK; Views are pure UI |
55+
| **Scalability** | Adding fields requires updating **Model**, **Controller**, and all **Views** | Adding fields requires updating **Model** and **ViewModel**; Views update bindings only |
56+
| **Observer wiring** | Manual — Container wires each **View** to the **Model** | Self-contained — **Views** register via **ViewModel**; container stays clean|
57+
| **UI logic leakage**| Risk - Views may call `model_->getData()` directly | Eliminated - Views use `viewModel_->getCurrentText()` only |

src/ap/mvc/IObserver.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#pragma once
22

3+
#include <string>
34
class IObserver {
45
public:
56
virtual ~IObserver() = default;

src/ap/mvc/model/SharedData.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ void SharedData::setData(const std::string& data) {
99
}
1010

1111
void SharedData::notifyObservers() {
12-
for (auto o : observers_) {
12+
for (auto *o : observers_) {
1313
o->onDataChanged(this->data_);
1414
}
1515
}

src/ap/mvvm/CMakeLists.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
add_executable(mvvm_ap)
2+
3+
target_sources(mvvm_ap
4+
PRIVATE
5+
mvvm_ap.cpp
6+
model/SharedData.cpp
7+
viewmodel/SharedDataVM.cpp
8+
view/EditorWidget.cpp
9+
view/DisplayWidget.cpp
10+
)
11+
12+
target_link_libraries(mvvm_ap PRIVATE gtk4-settings)

src/ap/mvvm/IObserver.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#pragma once
2+
3+
#include <string>
4+
class IObserver {
5+
public:
6+
virtual ~IObserver() = default;
7+
virtual void onDataChanged(const std::string& newData) = 0;
8+
};

src/ap/mvvm/model/SharedData.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
#include "SharedData.h"
2+
3+
SharedData::SharedData() : data_{"Initial Data"} {}
4+
5+
void SharedData::setData(const std::string& data) {
6+
this->data_ = data;
7+
notifyObservers();
8+
}
9+
10+
void SharedData::notifyObservers() {
11+
for (auto* o : observers_) {
12+
o->onDataChanged(this->data_);
13+
}
14+
}
15+
void SharedData::addObserver(IObserver* obs) {
16+
if (obs != nullptr)
17+
observers_.push_back(obs);
18+
}
19+
20+
std::string SharedData::getData() const {
21+
return data_;
22+
}

src/ap/mvvm/model/SharedData.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
#pragma once
2+
#include <string>
3+
#include <vector>
4+
#include "../IObserver.h"
5+
6+
class SharedData {
7+
public:
8+
SharedData();
9+
10+
void setData(const std::string& data);
11+
std::string getData() const;
12+
13+
void addObserver(IObserver* obs);
14+
15+
private:
16+
void notifyObservers();
17+
18+
std::string data_;
19+
std::vector<IObserver*> observers_;
20+
};

src/ap/mvvm/mvvm_ap.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#include <gtkmm.h>
2+
#include <memory>
3+
#include "model/SharedData.h"
4+
#include "view/DisplayWidget.h"
5+
#include "view/EditorWidget.h"
6+
#include "viewmodel/SharedDataVM.h"
7+
8+
/**
9+
* @brief ContainerWindow wires up the MVVM triad:
10+
* Key differences from MVC's ContainerWindow:
11+
* 1. No Controller is created.
12+
* 2. No manual addObserver() calls, each View self-registers with the ViewModel during construction.
13+
* 3. Views receive a shared_ptr<SharedDataViewModel>, never a Model pointer.
14+
*/
15+
class ContainerWindow : public Gtk::Window {
16+
public:
17+
ContainerWindow();
18+
19+
private:
20+
// Main Layout
21+
Gtk::Box mainLayout_;
22+
Gtk::Box topRowLayout_; // Horizontal arrangement (2 Displays side-by-side)
23+
24+
// Model & ViewModel are shared
25+
// View hold a shared_ptr to the ViewModel
26+
std::shared_ptr<SharedData> dataModel_;
27+
std::shared_ptr<SharedDataVM> viewModel_;
28+
29+
// Views
30+
std::unique_ptr<EditorWidget> editorView_;
31+
std::unique_ptr<DisplayWidget> displayViewLeft_;
32+
std::unique_ptr<DisplayWidget> displayViewRight_;
33+
};
34+
35+
ContainerWindow::ContainerWindow()
36+
: mainLayout_(Gtk::Orientation::VERTICAL),
37+
topRowLayout_(Gtk::Orientation::HORIZONTAL) {
38+
set_title("MVC Integrated Demo");
39+
set_default_size(600, 400);
40+
41+
// Step 1 – construct the Model.
42+
dataModel_ = std::make_shared<SharedData>();
43+
44+
// Step 2 – construct the ViewModel; it subscribes to the Model internally.
45+
viewModel_ = std::make_shared<SharedDataVM>(dataModel_);
46+
47+
// Step 3 – construct Views, passing only the ViewModel.
48+
editorView_ = std::make_unique<EditorWidget>(viewModel_);
49+
displayViewLeft_ = std::make_unique<DisplayWidget>("ZONE 2: MONITOR A (Blue)",
50+
"blue", viewModel_);
51+
displayViewRight_ = std::make_unique<DisplayWidget>("ZONE 3: MONITOR B (Red)",
52+
"red", viewModel_);
53+
54+
dataModel_->addObserver(editorView_.get());
55+
dataModel_->addObserver(displayViewLeft_.get());
56+
dataModel_->addObserver(displayViewRight_.get());
57+
58+
// Layout, unchanged from MVC
59+
displayViewLeft_->set_hexpand(true);
60+
displayViewRight_->set_hexpand(true);
61+
topRowLayout_.append(*displayViewLeft_);
62+
topRowLayout_.append(*displayViewRight_);
63+
64+
editorView_->set_vexpand(false);
65+
mainLayout_.append(topRowLayout_);
66+
mainLayout_.append(*editorView_);
67+
set_child(mainLayout_);
68+
}
69+
70+
int main(int argc, char* argv[]) {
71+
auto app = Gtk::Application::create("org.gtkmm.example.singlemvvm");
72+
return app->make_window_and_run<ContainerWindow>(argc, argv);
73+
}

src/ap/mvvm/view/DisplayWidget.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
#include "DisplayWidget.h"
2+
3+
DisplayWidget::DisplayWidget(const std::string& title, const std::string& color,
4+
std::shared_ptr<SharedDataVM> vm)
5+
: Gtk::Box(Gtk::Orientation::VERTICAL),
6+
color_(color),
7+
innerBox_(Gtk::Orientation::VERTICAL),
8+
view_model_(std::move(vm)) {
9+
frame_.set_label(title);
10+
frame_.set_margin(10);
11+
12+
// get from the ViewModel's current state
13+
updateLabel(view_model_->getCurrentText());
14+
15+
innerBox_.append(labelData_);
16+
innerBox_.set_margin(20);
17+
18+
frame_.set_child(innerBox_);
19+
this->append(frame_);
20+
21+
// self-register: now the VM will push future update to this View
22+
view_model_->addObserver(this);
23+
}
24+
25+
void DisplayWidget::updateLabel(const std::string& text) {
26+
std::string markup = "<span foreground='" + color_ +
27+
"' size='x-large' weight='bold'>" + text + "</span>";
28+
labelData_.set_markup(markup);
29+
}
30+
31+
void DisplayWidget::onDataChanged(const std::string& newData) {
32+
updateLabel(newData);
33+
}

0 commit comments

Comments
 (0)