diff --git a/src/ap/CMakeLists.txt b/src/ap/CMakeLists.txt index 7e9ae84..c6e1842 100644 --- a/src/ap/CMakeLists.txt +++ b/src/ap/CMakeLists.txt @@ -18,4 +18,4 @@ target_sources(ap target_link_libraries(ap PRIVATE gtk4-settings) add_subdirectory(mvc) -# add_subdirectory(mvvm) \ No newline at end of file +add_subdirectory(mvvm) \ No newline at end of file diff --git a/src/ap/README.md b/src/ap/README.md index 22aaebd..f757537 100644 --- a/src/ap/README.md +++ b/src/ap/README.md @@ -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 \ No newline at end of file +| 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 | \ No newline at end of file diff --git a/src/ap/mvc/IObserver.h b/src/ap/mvc/IObserver.h index 60c9baf..15ced7e 100644 --- a/src/ap/mvc/IObserver.h +++ b/src/ap/mvc/IObserver.h @@ -1,5 +1,6 @@ #pragma once +#include class IObserver { public: virtual ~IObserver() = default; diff --git a/src/ap/mvc/model/SharedData.cpp b/src/ap/mvc/model/SharedData.cpp index 0f44a39..853a0c4 100644 --- a/src/ap/mvc/model/SharedData.cpp +++ b/src/ap/mvc/model/SharedData.cpp @@ -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_); } } diff --git a/src/ap/mvc/mvc_ap.cpp b/src/ap/mvc/mvc_ap.cpp index e6cb20c..4da5d0c 100644 --- a/src/ap/mvc/mvc_ap.cpp +++ b/src/ap/mvc/mvc_ap.cpp @@ -5,6 +5,7 @@ #include "view/DisplayWidget.h" #include "view/EditorWidget.h" +namespace mvc { class ContainerWindow : public Gtk::Window { public: ContainerWindow(); @@ -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(argc, argv); + return app->make_window_and_run(argc, argv); } \ No newline at end of file diff --git a/src/ap/mvvm/CMakeLists.txt b/src/ap/mvvm/CMakeLists.txt new file mode 100644 index 0000000..2e68a5e --- /dev/null +++ b/src/ap/mvvm/CMakeLists.txt @@ -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) \ No newline at end of file diff --git a/src/ap/mvvm/IObserver.h b/src/ap/mvvm/IObserver.h new file mode 100644 index 0000000..15ced7e --- /dev/null +++ b/src/ap/mvvm/IObserver.h @@ -0,0 +1,8 @@ +#pragma once + +#include +class IObserver { + public: + virtual ~IObserver() = default; + virtual void onDataChanged(const std::string& newData) = 0; +}; \ No newline at end of file diff --git a/src/ap/mvvm/model/SharedData.cpp b/src/ap/mvvm/model/SharedData.cpp new file mode 100644 index 0000000..36a9554 --- /dev/null +++ b/src/ap/mvvm/model/SharedData.cpp @@ -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_; +} \ No newline at end of file diff --git a/src/ap/mvvm/model/SharedData.h b/src/ap/mvvm/model/SharedData.h new file mode 100644 index 0000000..603cdd5 --- /dev/null +++ b/src/ap/mvvm/model/SharedData.h @@ -0,0 +1,22 @@ +#pragma once +#include +#include +#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 observers_; +}; +} // namespace mvvm \ No newline at end of file diff --git a/src/ap/mvvm/mvvm_ap.cpp b/src/ap/mvvm/mvvm_ap.cpp new file mode 100644 index 0000000..ba03f62 --- /dev/null +++ b/src/ap/mvvm/mvvm_ap.cpp @@ -0,0 +1,71 @@ +#include +#include +#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, 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 dataModel_; + std::shared_ptr viewModel_; + + // Views + std::unique_ptr editorView_; + std::unique_ptr displayViewLeft_; + std::unique_ptr 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(); + + // Step 2 – construct the ViewModel; it subscribes to the Model internally. + viewModel_ = std::make_shared(dataModel_); + + // Step 3 – construct Views, passing only the ViewModel. + editorView_ = std::make_unique(viewModel_); + displayViewLeft_ = std::make_unique("ZONE 2: MONITOR A (Blue)", + "blue", viewModel_); + displayViewRight_ = std::make_unique("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(argc, argv); +} \ No newline at end of file diff --git a/src/ap/mvvm/view/DisplayWidget.cpp b/src/ap/mvvm/view/DisplayWidget.cpp new file mode 100644 index 0000000..d18ad9b --- /dev/null +++ b/src/ap/mvvm/view/DisplayWidget.cpp @@ -0,0 +1,33 @@ +#include "DisplayWidget.h" + +mvvm::DisplayWidget::DisplayWidget(const std::string& title, std::string color, + std::shared_ptr 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 = "" + text + ""; + labelData_.set_markup(markup); +} + +void mvvm::DisplayWidget::onDataChanged(const std::string& newData) { + updateLabel(newData); +} \ No newline at end of file diff --git a/src/ap/mvvm/view/DisplayWidget.h b/src/ap/mvvm/view/DisplayWidget.h new file mode 100644 index 0000000..33864fb --- /dev/null +++ b/src/ap/mvvm/view/DisplayWidget.h @@ -0,0 +1,24 @@ +#include +#include +#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 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 view_model_; +}; +} // namespace mvvm \ No newline at end of file diff --git a/src/ap/mvvm/view/EditorWidget.cpp b/src/ap/mvvm/view/EditorWidget.cpp new file mode 100644 index 0000000..7607262 --- /dev/null +++ b/src/ap/mvvm/view/EditorWidget.cpp @@ -0,0 +1,37 @@ +#include "EditorWidget.h" + +#include + +mvvm::EditorWidget::EditorWidget(std::shared_ptr 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); + } +} \ No newline at end of file diff --git a/src/ap/mvvm/view/EditorWidget.h b/src/ap/mvvm/view/EditorWidget.h new file mode 100644 index 0000000..e9d9c5f --- /dev/null +++ b/src/ap/mvvm/view/EditorWidget.h @@ -0,0 +1,23 @@ +#pragma once +#include +#include +#include "../IObserver.h" +#include "../viewmodel/SharedDataVM.h" + +namespace mvvm { +class EditorWidget : public Gtk::Box, public IObserver { + public: + explicit EditorWidget(std::shared_ptr 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 view_model_; +}; +} // namespace mvvm \ No newline at end of file diff --git a/src/ap/mvvm/viewmodel/SharedDataVM.cpp b/src/ap/mvvm/viewmodel/SharedDataVM.cpp new file mode 100644 index 0000000..2e4e8db --- /dev/null +++ b/src/ap/mvvm/viewmodel/SharedDataVM.cpp @@ -0,0 +1,32 @@ +#include "SharedDataVM.h" + +mvvm::SharedDataVM::SharedDataVM(std::shared_ptr 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); + } +} \ No newline at end of file diff --git a/src/ap/mvvm/viewmodel/SharedDataVM.h b/src/ap/mvvm/viewmodel/SharedDataVM.h new file mode 100644 index 0000000..44ac352 --- /dev/null +++ b/src/ap/mvvm/viewmodel/SharedDataVM.h @@ -0,0 +1,33 @@ +#pragma once +#include +#include +#include "../IObserver.h" +#include "../model/SharedData.h" + +namespace mvvm { +/** + * @brief The single mediator between the Model and all Views + * + * Responsibilities: + * - Subscribes to the Model + * - Exposes a command that Views call on user actions + * - Maintains its own observer list so View subscribe to the ViewModel instead of the Model. View never touch Model + */ +class SharedDataVM : public IObserver { + public: + explicit SharedDataVM(std::shared_ptr model); + + void submitText(const std::string& text); + + std::string getCurrentText() const; + + void addObserver(IObserver* obs); + + private: + void onDataChanged(const std::string& newData) override; + void notifyObservers(const std::string& data); + + std::shared_ptr model_; // SharedData model + std::vector view_observers_; // Views +}; +} // namespace mvvm \ No newline at end of file