Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ap/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@ target_sources(ap
target_link_libraries(ap PRIVATE gtk4-settings)

add_subdirectory(mvc)
# add_subdirectory(mvvm)
add_subdirectory(mvvm)
18 changes: 9 additions & 9 deletions src/ap/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,13 @@ View → Controller → Model
### 3. GTK4
- [Refer](https://docs.gtk.org/gtk4/getting_started.html)

### 4. Examples
### 4.1. simple_ap
- Cos:
- Quick, Simple
- Pos:
- Dependency: e.g. what happen when we delete Gtk::Label m_labelMonitorA;
- Scalability:
- Reusability:
### 4. Trade-offs: MVC vs MVVM

### 4.2. mvc_ap
| Aspect | MVC | MVVM |
|---------------------|---------------------------------------------------------------------|----------------------------------------------------------------------|
| **Complexity** | Lower — **Controller** is a thin pass-through | Slightly higher — **ViewModel** adds an extra layer |
| **Coupling** | Views know both **Controller** and **Model** (e.g., for initial data) | **Views** know only the **ViewModel** |
| **Testability** | Controller is testable, but **Views** are still tied to **Model** for reads | **ViewModel** is fully testable without GTK; Views are pure UI |
| **Scalability** | Adding fields requires updating **Model**, **Controller**, and all **Views** | Adding fields requires updating **Model** and **ViewModel**; Views update bindings only |
| **Observer wiring** | Manual — Container wires each **View** to the **Model** | Self-contained — **Views** register via **ViewModel**; container stays clean|
| **UI logic leakage**| Risk - Views may call `model_->getData()` directly | Eliminated - Views use `viewModel_->getCurrentText()` only |
1 change: 1 addition & 0 deletions src/ap/mvc/IObserver.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once

#include <string>
class IObserver {
public:
virtual ~IObserver() = default;
Expand Down
2 changes: 1 addition & 1 deletion src/ap/mvc/model/SharedData.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ void SharedData::setData(const std::string& data) {
}

void SharedData::notifyObservers() {
for (auto o : observers_) {
for (auto* o : observers_) {
o->onDataChanged(this->data_);
}
}
Expand Down
4 changes: 3 additions & 1 deletion src/ap/mvc/mvc_ap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "view/DisplayWidget.h"
#include "view/EditorWidget.h"

namespace mvc {
class ContainerWindow : public Gtk::Window {
public:
ContainerWindow();
Expand Down Expand Up @@ -67,8 +68,9 @@ ContainerWindow::ContainerWindow()
mainLayout_.append(*editorView_); // Add bottom row
set_child(mainLayout_);
}
} // namespace mvc

int main(int argc, char* argv[]) {
auto app = Gtk::Application::create("org.gtkmm.example.singlemvc");
return app->make_window_and_run<ContainerWindow>(argc, argv);
return app->make_window_and_run<mvc::ContainerWindow>(argc, argv);
}
12 changes: 12 additions & 0 deletions src/ap/mvvm/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
add_executable(mvvm_ap)

target_sources(mvvm_ap
PRIVATE
mvvm_ap.cpp
model/SharedData.cpp
viewmodel/SharedDataVM.cpp
view/EditorWidget.cpp
view/DisplayWidget.cpp
)

target_link_libraries(mvvm_ap PRIVATE gtk4-settings)
8 changes: 8 additions & 0 deletions src/ap/mvvm/IObserver.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#pragma once

#include <string>
class IObserver {
public:
virtual ~IObserver() = default;
virtual void onDataChanged(const std::string& newData) = 0;
};
22 changes: 22 additions & 0 deletions src/ap/mvvm/model/SharedData.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include "SharedData.h"

mvvm::SharedData::SharedData() : data_{"Initial Data"} {}

void mvvm::SharedData::setData(const std::string& data) {
this->data_ = data;
notifyObservers();
}

void mvvm::SharedData::notifyObservers() {
for (auto* o : observers_) {
o->onDataChanged(this->data_);
}
}
void mvvm::SharedData::addObserver(IObserver* obs) {
if (obs != nullptr)
observers_.push_back(obs);
}

std::string mvvm::SharedData::getData() const {
return data_;
}
22 changes: 22 additions & 0 deletions src/ap/mvvm/model/SharedData.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#pragma once
#include <string>
#include <vector>
#include "../IObserver.h"

namespace mvvm {
class SharedData {
public:
SharedData();

void setData(const std::string& data);
std::string getData() const;

void addObserver(IObserver* obs);

private:
void notifyObservers();

std::string data_;
std::vector<IObserver*> observers_;
};
} // namespace mvvm
71 changes: 71 additions & 0 deletions src/ap/mvvm/mvvm_ap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#include <gtkmm.h>
#include <memory>
#include "model/SharedData.h"
#include "view/DisplayWidget.h"
#include "view/EditorWidget.h"
#include "viewmodel/SharedDataVM.h"

namespace mvvm {
/**
* @brief ContainerWindow wires up the MVVM triad:
* Key differences from MVC's ContainerWindow:
* 1. No Controller is created.
* 2. No manual addObserver() calls, each View self-registers with the ViewModel during construction.
* 3. Views receive a shared_ptr<SharedDataVM>, never a Model pointer.
*/
class ContainerWindow : public Gtk::Window {
public:
ContainerWindow();

private:
// Main Layout
Gtk::Box mainLayout_;
Gtk::Box topRowLayout_; // Horizontal arrangement (2 Displays side-by-side)

// Model & ViewModel are shared
// View hold a shared_ptr to the ViewModel
std::shared_ptr<SharedData> dataModel_;
std::shared_ptr<SharedDataVM> viewModel_;

// Views
std::unique_ptr<EditorWidget> editorView_;
std::unique_ptr<DisplayWidget> displayViewLeft_;
std::unique_ptr<DisplayWidget> displayViewRight_;
};

ContainerWindow::ContainerWindow()
: mainLayout_(Gtk::Orientation::VERTICAL),
topRowLayout_(Gtk::Orientation::HORIZONTAL) {
set_title("MVVM Integrated Demo");
set_default_size(600, 400);

// Step 1 – construct the Model.
dataModel_ = std::make_shared<SharedData>();

// Step 2 – construct the ViewModel; it subscribes to the Model internally.
viewModel_ = std::make_shared<SharedDataVM>(dataModel_);

// Step 3 – construct Views, passing only the ViewModel.
editorView_ = std::make_unique<EditorWidget>(viewModel_);
displayViewLeft_ = std::make_unique<DisplayWidget>("ZONE 2: MONITOR A (Blue)",
"blue", viewModel_);
displayViewRight_ = std::make_unique<DisplayWidget>("ZONE 3: MONITOR B (Red)",
"red", viewModel_);

// Layout, unchanged from MVC
displayViewLeft_->set_hexpand(true);
displayViewRight_->set_hexpand(true);
topRowLayout_.append(*displayViewLeft_);
topRowLayout_.append(*displayViewRight_);

editorView_->set_vexpand(false);
mainLayout_.append(topRowLayout_);
mainLayout_.append(*editorView_);
set_child(mainLayout_);
}
} // namespace mvvm

int main(int argc, char* argv[]) {
auto app = Gtk::Application::create("org.gtkmm.example.singlemvvm");
return app->make_window_and_run<mvvm::ContainerWindow>(argc, argv);
}
33 changes: 33 additions & 0 deletions src/ap/mvvm/view/DisplayWidget.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#include "DisplayWidget.h"

mvvm::DisplayWidget::DisplayWidget(const std::string& title, std::string color,
std::shared_ptr<SharedDataVM> vm)
: Gtk::Box(Gtk::Orientation::VERTICAL),
color_(std::move(color)),
innerBox_(Gtk::Orientation::VERTICAL),
view_model_(std::move(vm)) {
frame_.set_label(title);
frame_.set_margin(10);

// get from the ViewModel's current state
updateLabel(view_model_->getCurrentText());

innerBox_.append(labelData_);
innerBox_.set_margin(20);

frame_.set_child(innerBox_);
this->append(frame_);

// self-register: now the VM will push future update to this View
view_model_->addObserver(this);
}

void mvvm::DisplayWidget::updateLabel(const std::string& text) {
std::string markup = "<span foreground='" + color_ +
"' size='x-large' weight='bold'>" + text + "</span>";
labelData_.set_markup(markup);
}

void mvvm::DisplayWidget::onDataChanged(const std::string& newData) {
updateLabel(newData);
}
24 changes: 24 additions & 0 deletions src/ap/mvvm/view/DisplayWidget.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#include <gtkmm.h>
#include <memory>
#include "../IObserver.h"
#include "../viewmodel/SharedDataVM.h"

namespace mvvm {
class DisplayWidget : public Gtk::Box, public IObserver {
public:
DisplayWidget(const std::string& title, std::string color,
std::shared_ptr<SharedDataVM> vm);

void onDataChanged(const std::string& newData) override;

private:
void updateLabel(const std::string& text);

std::string color_;
Gtk::Frame frame_;
Gtk::Box innerBox_;
Gtk::Label labelData_;

std::shared_ptr<SharedDataVM> view_model_;
};
} // namespace mvvm
37 changes: 37 additions & 0 deletions src/ap/mvvm/view/EditorWidget.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#include "EditorWidget.h"

#include <utility>

mvvm::EditorWidget::EditorWidget(std::shared_ptr<SharedDataVM> vm)
: Gtk::Box(Gtk::Orientation::VERTICAL),
innerBox_(Gtk::Orientation::VERTICAL),
view_model_(std::move(vm)) {
frame_.set_label("ZONE 1: EDITOR (Input View)");
frame_.set_margin(10);

labelTitle_.set_text("Enter new data:");
entry_.set_text(view_model_->getCurrentText());
button_.set_label("Broadcast Update");

innerBox_.append(labelTitle_);
innerBox_.append(entry_);
innerBox_.append(button_);
innerBox_.set_margin(15);
innerBox_.set_spacing(10);

frame_.set_child(innerBox_);
this->append(frame_);

// MVVM binding: user action -> ViewModel command
button_.signal_clicked().connect(
[this]() { view_model_->submitText(entry_.get_text()); });

// Self-register
view_model_->addObserver(this);
}

void mvvm::EditorWidget::onDataChanged(const std::string& newData) {
if (entry_.get_text() != Glib::ustring(newData)) {
entry_.set_text(newData);
}
}
23 changes: 23 additions & 0 deletions src/ap/mvvm/view/EditorWidget.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#pragma once
#include <gtkmm.h>
#include <memory>
#include "../IObserver.h"
#include "../viewmodel/SharedDataVM.h"

namespace mvvm {
class EditorWidget : public Gtk::Box, public IObserver {
public:
explicit EditorWidget(std::shared_ptr<SharedDataVM> vm);

void onDataChanged(const std::string& newData) override;

private:
Gtk::Frame frame_;
Gtk::Box innerBox_;
Gtk::Label labelTitle_;
Gtk::Entry entry_;
Gtk::Button button_;

std::shared_ptr<SharedDataVM> view_model_;
};
} // namespace mvvm
32 changes: 32 additions & 0 deletions src/ap/mvvm/viewmodel/SharedDataVM.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#include "SharedDataVM.h"

mvvm::SharedDataVM::SharedDataVM(std::shared_ptr<SharedData> model)
: model_(std::move(model)) {
model_->addObserver(this);
}

void mvvm::SharedDataVM::submitText(const std::string& text) {
if (text.empty())
return;

model_->setData(text);
}

std::string mvvm::SharedDataVM::getCurrentText() const {
return model_->getData();
}

void mvvm::SharedDataVM::addObserver(IObserver* obs) {
if (obs != nullptr)
view_observers_.push_back(obs);
}

void mvvm::SharedDataVM::onDataChanged(const std::string& newData) {
notifyObservers(newData);
}

void mvvm::SharedDataVM::notifyObservers(const std::string& data) {
for (auto* obs : view_observers_) {
obs->onDataChanged(data);
}
}
Loading
Loading