MVC olé!
-
Hallo da!
Hattet ihr, während ihr euer Projekt nach dem MVC (oder nur MV)- Muster aufgebaut habt auch schonmal Situationen, in denen ihr am liebsten auf eine C-Schnittstelle zurückgerudert wärt? Ich hab halt hier so mein Modell (gleichzeitig Controller, bzw. es hat friend controller für das undo-system) mit seinen Observern und in diesem speziellen Fall hat das Modell hierarchisch angeordnete Minimal-Teilmodelle.
Also um es auf einen Punkt zu bringen, ich habe hier eine graphische Kante zwischen Knoten, welche verästeln kann um mehrere Ziele zu erreichen. Jeder Teilast wäre ja nun an sich auch als Modell zu sehen und man könnte ihm eine Observer-Liste zuordnen. Allerdings hab ich da so den Eindruck, dass der Verwaltungsaufwand nicht mehr angemessen ist. Also bin ich im Moment zurückgerudert und habe im Modell der Kante jede Menge Methoden nach dem Motto:
class EdgeElement; class EdgeObserver { virtual void OnSplitElement( EdgeElement* oldElement, Element* newElement ) = 0; virtual void OnJoinElements( EdgeElement* a, EdgeElement* b ) = 0; // ... }; class Edge : mvc::Model< EdgeObserver, mvc::StorageTraits::Xml > { public: Edge( Output* src ); ~Edge(); Output* GetSource() const; std::vector< Input* > GetTargets() const; // Convenience (Input and Output provide the means already) std::wstring GetSourceId() const; // Convenience (Input and Output provide the means already) std::wstring GetSourceName() const; // as in name of output in source, together they form id/outputs/name // Convenience (Input and Output provide the means already) NodePtr GetSourceNode() const; std::vector< NodePtr > GetTargetNodes() const; // .... public: // Splits a given element at given point, adds a new element as single child, moves all children from the original element to the new one. void SplitElement( EdgeElement* e, unsigned int index ); // Joins two consecutive elements. Adds all children of the second element to the first element. void JoinElements( EdgeElement* e1, EdgeElement* e2 ); // Adds a new Element void AddElement( EdgeElement* parent, EdgeElement* child ); // Removes an element and all its children. void RemoveElement( EdgeElement* e ); // Moves a point in an EdgeElement. void MovePoint( EdgeElement* e, unsigned int index, vis::Point newPos ); // Adds a point at given index. void AddPoint( EdgeElement* e, unsigned int index, vis::Point point ); // Removes a point at given index. Must not be the only point. void RemovePoint( EdgeElement* e, unsigned int index ); private: friend class XNSplitElement; void xnSplitElement( EdgeElement* e, EdgeElement *newelem, unsigned int index ); friend class XNJoinElements; void xnJoinElements( EdgeElement* e1, EdgeElement* e2 ); //void xnUndoJoinElements( EdgeElement* e1, EdgeElement* oldChild ); friend class XNAddElement; void xnAddElement( EdgeElement* paramt, EdgeElement* child ); friend class XNRemoveElement; void xnRemoveElement( EdgeElement* e ); friend class XNMovePoint; vis::Point xnMovePoint( EdgeElement* e, unsigned int index, vis::Point newPos ); friend class XNAddPoint; void xnAddPoint( EdgeElement* e, unsigned int index, vis::Point pos ); friend class XNRemovePoint; vis::Point xnRemovePoint( EdgeElement* e, unsigned int index ); // .... }Diese XN*-Klassen sind "Aktionen" auf die Daten für das Undo-System, welche dann mit den gespeicherten Parametern die xn*-Methoden aufrufen, um die Aktion letztlich auszuführen, falls ihr euch wundert.
EdgeElements haben praktisch keine öffentlichen Methoden um sie zu verändern, das muss alles über die Edge-Klasse geschehen (ansonsten konnte man das Modell ja verändern, ohne dass es und alle Observer es mitbekämen)
Was man jetzt aber sieht, ist, dass ich ein riesiges hässliches Interface zum Umgang mit den EdgeElements in der Klasse Edge habe (das halt so aussieht wie ein C-Interface). Würdet ihr die Kosten in Kauf nehmen, EdgeElemente als Teilmodelle erster Klasse ( class EdgeElement : public mvc::Model< EdgeElementObserver, ... > ), obwohl dann schon allein die Verwaltungsdaten für die Liste der Observer fast größer sind, als die paar popeligen Pointer und Punkte als Nutzdaten darin?
Hier der Screenshot, was daraus letztlich werden soll:
http://img408.imageshack.us/i/edgeshot.png/Viele Grüße,
DeciEdit: Ich habe mal alles, was hier eigentlich schon damals hingehört hätte, hier eingefügt.
-
Könntest du dein Problem noch ein wenig weiter erörtern? So ganz konnte ich dem nicht folgen. Was meinst du damit, dass die "Kante" sich verästeln kann? Meinst du damit, dass es immer einen Ausgangspunkt gibt, über welchen man mehrere Endpunkte erreichen können soll und du willst nun diese Wege als Modell haben? Wieso dann nicht im Modell den Ausgangspunkt speichern und alle Endpunkte?
Und was meinst du mit dem "auf eine C-Schnittstelle zurückrudern"? Man kann MVC auch mit C umsetzen.
Grüssli
-
Hallo Dravere,
das will ich gerne tun!
Also man kann einen Ausgang meiner "Knoten" im Datenflussgraph mit beliebig vielen Eingängen von Folgeknoten verbinden. Die Verbindung "Quelle, n Ziele" repräsentiert bei mir die Klasse Edge. Allerdings hat sich auch einen grafischen Zustand, so dass im View die Kante halt wie ein Baum von Linienzügen ausästeln kann und der Benutzer sich die einzelnen Abschnitte auch zurechtziehen kann usw..
Elemente dieses "Baums" werden in meinem Fall mit EdgeElement bezeichnet. Das ist praktisch ein Linienzug mit Vater und n Kindern oder Ziel (Eingang eines Folgeknotens). Aber wie ich oben vielleicht nicht so klar beschrieb, hatte ich irgendwie Bedenken jedem kleinen EdgeElement ein "MVC"-Interface zu verpassen. Also habe ich die Veränderung (graphisch und verbindungstechnisch) durch Methoden in der beinhaltenden Edge-Klasse umgesetzt. Und diese Methoden fühlen sich ja nun aber irgendwie wie der alte C-Weg von Objektorientierung an (zumindest sah das damals so aus bei mir^^).Viele Grüße,
Deci
-
*hopp*
Man könnte die Frage auch so ausdrücken: Wie setzt ihr MVC bei "Kleinst"-Teilmodellen um?
Viele Grüße,
Deci
-
Ich habe es ehrlich gesagt immer noch nicht so recht verstanden. Die Klasse
Edgerepräsentiert die Verbindung von einer Quelle mit n Zielen. Die KlasseEdgeElementrepräsentiert einen Linienzug mit einem Vater und n Kindern oder Zielen. Ist das nicht dasselbe?
Zudem wundere ich mich ein wenig ab den Bezeichnern. Du hast Edge wegen der Übersetzung von Kante genommen, nehme ich an. Doch irgendwie ist es für mich gar keine Kante, welche du da beschreibst. Das ist doch eher eine Verbindung und dafür würde ich eher Connection oder allenfalls Circuit nehmen. Und meinst du womöglich mit EdgeElement einen Knoten? Also womöglich Node?Im übrigen, vielleicht auch ohne auf dein Problem explizit einzugehen, dir ist schon klar, dass das MVC Modell keine 1:1:1 Beziehung für M:V:C vorschreibt? Eine View darf Daten aus mehreren Modellen verarbeiten. Zudem ist auch nirgends genau definiert, wie ein Modell eigentlich aufgebaut sein soll. Ein Modell muss nicht unbedingt einfach nur eine Liste von Objekten eines Typs sein. Ein einzelnes Modell darf durchaus auch eine hierarchische Struktur repräsentieren.
Bei MVC geht es eigentlich hauptsächlich um die Trennung der Aufgaben:
1. Datenhaltung
2. Logik
3. AnzeigeEs stellt eine Richtlinie dar und erhöht die Wartbarkeit und meistens auch die Wiederverwendbarkeit.
Grüssli
-
Du hast vollkommen Recht, wenn du sagst, dass Edge kein wirklich treffender Begriff für das ist, was die Klasse Edge hier eigentlich repräsentiert. Graphentheorethisch repräsentiert sie hier ja mehrere Kanten - nämlich alle, die von einem Knoten (oder besser gesagt von einem Ausgang eines Knotens) entspringen. Der Name rührt noch daher, dass ich zuvor wirklich jede Verbindung als einzelnes Objekt ausgeführt habe, allerdings zu dem Schluss kam, dass das nicht zweckdienlich war, weil der graphische Aspekt eben auch ein wichtiger Teil der Anwendung (und damit des Zustands des Modells) ist und in diesem Fall diese Kanten als ein (vom Benutzer veränderbaren) Linien-Baum dargestellt werden sollen.
Mit EdgeElement, meine ich wirklich nur einen Teil des graphischen Baums, der dem Benutzer gegenüber eine 'Edge' repräsentiert. Zum Beispiel ein Linienzug bis zu einem Punkt X, an dem wieder y Linienzüge entspringen, welche dann hierarchisch irgendwann an einem Eingang von einem Knoten enden.
Diese EdegeElements sind nun wirklich der Quell all meiner gedanklichen Probleme. Sie enthalten praktisch null Daten, daher bin ich nicht verleitet sie als hierarchisches teilmodell von der Klasse Edge zu behandeln. Natürlich sollte ich auch darauf eingehen, dass all meine Modelle immer auch ein Container von Observern sind (zB. sind alle Views Observer ihrere designierten Modelle). Wenn sie also echte Teilmodelle sein sollten, würde ich für die 3 Punkte, die ein EdgeElement beispielsweise enthält, wieder eine Liste von Observern bereitstellen (mit entsprechend gespiegeltem Aufwand auf der View-Seite, um die Korrespondenzen zu verwalten).
Also bin ich dazu übergangen, die EdgeElements als integralen Bestandteil von Edge zu behandeln. Allerdings muss dann ja auch die Observer-Schnittstelle all die möglichen Veränderungen an die verbundenen Observer weiterleiten, daher müssen meiner derzeitigen Meinung nach die Methoden zur Veränderung der EdgeElements in der Klasse Edge enthalten sein (sie ist ja fortan das umfassende Modell und Observer/Views haben nur hier die Chance, Veränderungen am Modell mitzubekommen).Dein dritter Absatz betrifft mein spezielles Problem gar nicht so sehr, aber beschreibt natürlich gut die Gründe, aus denen ich in meiner Anwendung MVC einsetze. Es bietet einfach eine super Möglichkeit, Ansichten auf dieselben Daten zu synchronisieren und die Logik zur Veränderung dieser zu zentralisieren.
Viele Grüße,
Deci
-
mh ... vielleicht sowas ... allerdings erst M und V ... noch kein C dabei
braucht (wenig) NokiaQt zum visualisieren ...Ich finde hier fehlt noch ein [spoiler|[/spoiler] tag um code zu verstecken der so lang ist

Result: http://img405.imageshack.us/i/visve.jpg/ Größe ist direkt abhängig von der Anzahl ausgehender Kanten, Farben wird eine pro Node gemerkt.
#include <QApplication> #include <QLabel> #include <QImage> #include <QPixmap> #include <QColor> #include <QPainter> #include <QPen> #include <QBrush> #include <QDebug> #include <QMap> #include <QtGlobal> #include <QTime> #include <cassert> #include <vector> #include <map> #include <set> namespace MVC { namespace Model { class Node { static unsigned int maxID; std::set<Node*> m_connectsTo; unsigned int m_ID; public: Node() : m_ID(maxID++) {} unsigned int id() { return m_ID; } void conntectTo(Node *n) { m_connectsTo.insert(n); } const std::set<Node*> & connections() const { return m_connectsTo; } }; // class Node unsigned int Node::maxID = 0; class Graph { std::set<Node*> m_created; public: Graph() { for(int i=0; i<90; ++i) createNode(); // connect each node with 0..9 others, maybe even with itself - multiples do not occure due to std::set std::set<Node*>::iterator it; for(it = m_created.begin(); it != m_created.end(); ++it) { int concount = qrand() % 3 * 4; // 0,4,8 for (int j=0;j<concount;++j) { std::set<Node*>::iterator itit = m_created.begin(); std::advance(itit,qrand()%m_created.size()); connect(*it,*itit); } } } ~Graph() { std::set<Node*>::iterator it = m_created.begin(); for (it = m_created.begin(); it != m_created.end();) { delete *it; m_created.erase(it++); } m_created.clear(); } int nodeCount() { return m_created.size(); } Node * nodeAt(unsigned int i) { assert(i < m_created.size()); std::set<Node*>::iterator it = m_created.begin(); std::advance(it,i); return *it; } Node * createNode() { Node * n = new Node(); m_created.insert(n); return n; } void connect(Node *from, Node *to) { assert( m_created.find(from) != m_created.end() && m_created.find(to) != m_created.end()); from->conntectTo(to); } }; // class Graph }; // namespace Model namespace View { class NodeV { Model::Node * m_data; unsigned int m_x; unsigned int m_y; unsigned int m_radius; public: NodeV(Model::Node * n) : m_data(n), m_x(n->id()*100%1000+100), m_y(n->id()*100/1000*100+100), m_radius(std::min(45u, static_cast<unsigned int>(5+5*m_data->connections().size()))) { } NodeV(const NodeV & other) : m_data(other.m_data), m_x(other.m_x), m_y(other.m_y), m_radius(other.m_radius) { } NodeV& operator=(const NodeV &other) { m_data = other.m_data; m_x = other.m_x; m_y = other.m_y; m_radius = other.m_radius; return *this;} unsigned x() { return m_x;} unsigned y() { return m_y;} unsigned r() { return m_radius;} }; // class NodeV class GraphV { Model::Graph * m_data; std::map<unsigned int, NodeV*> m_nodeViews; double m_scale; QMap<NodeV*,int> cols; void line(NodeV* from, NodeV* to) { if (! cols.contains(from)) cols.insert(from,qrand()%360); QColor c = QColor::fromHsv(cols.value(from),255,255,155); QPainter p(&im); QPen pen; pen.setColor(c); pen.setStyle(Qt::SolidLine); pen.setWidthF(from->r()/10.0); p.setPen(pen); p.setBrush(Qt::NoBrush); p.drawLine(from->x(),from->y(),to->x(),to->y()); } void node(NodeV* node) { if (! cols.contains(node)) cols.insert(node,qrand()%360); QColor c = QColor::fromHsv(cols.value(node),255,255,255); QPainter p(&im); QBrush brush; brush.setColor(c); brush.setStyle(Qt::SolidPattern); p.setBrush(brush); p.setPen(Qt::NoPen); qDebug() << node->r(); p.drawEllipse(node->x()-node->r(),node->y()-node->r(),2*node->r(),2*node->r()); } void draw() { // create views on first run static bool first = true; { first = false; for (int i=0; i< m_data->nodeCount(); ++i) { m_nodeViews[static_cast<unsigned int>(m_data->nodeAt(i)->id())] = new NodeV(m_data->nodeAt(i)); } } // draw { // all lines for (int i=0; i< m_data->nodeCount(); ++i) { Model::Node * from = m_data->nodeAt(i); unsigned int fromID = m_data->nodeAt(i)->id(); std::set<Model::Node*> conns = from->connections(); std::set<Model::Node*>::const_iterator it; for(it=conns.begin(); it!=conns.end(); ++it) { Model::Node * to = *it; unsigned int toID = to->id(); line(m_nodeViews[fromID], m_nodeViews[toID]); } } std::map<unsigned int, NodeV*>::iterator itit; // all nodes for (itit = m_nodeViews.begin(); itit != m_nodeViews.end(); ++itit) { node(itit->second); } } } public: GraphV(Model::Graph * g) : m_data(g), m_scale(1.0) { } ~GraphV() { std::map<unsigned int, NodeV*>::iterator it; for (it = m_nodeViews.begin(); it != m_nodeViews.end();) { delete it->second; m_nodeViews.erase(it++); } m_nodeViews.clear(); } void update() { im = QImage(1100,1000,QImage::Format_ARGB32); im.fill(0xFFFFFFFF); draw(); } QImage im; }; // class GraphV }; // namespace View }; // namespace MVC int main(int argc, char **argv) { qsrand(QTime(0,0,0,0).msecsTo(QTime::currentTime())); MVC::Model::Graph g; MVC::View::GraphV gv(&g); gv.update(); QApplication app(argc,argv); QLabel l; l.setAlignment(Qt::AlignCenter); l.resize(1100,1100); l.setPixmap(QPixmap::fromImage(gv.im.scaled(900,900,Qt::KeepAspectRatio, Qt::SmoothTransformation))); l.show(); app.exec(); }
-
Hallo padreigh,
danke dass du dir die Zeit genommen hast! Dein Beispiel zeigt in etwa (aber nicht 1:1), wie ich es früher mal umgesetzt habe. Damals war auch jede Verbindung von ausgang zu einem eingang separat und als bezier-kurve ausgeführt, hatte somit keinen komplexen graphischen Zustand. Aber dieses Zusammenlegen der Kanten inklusive geometrischen Zustand, welches ich nun wegen des schöneren Aussehens und der Handhabbarkeit einführen möchte, sorgt eben dafür, dass sich das alles nicht mehr so schön umsetzen lässt, wie ich mir das irgendwie vorstelle. Ich wünschte, ich würde hier noch meinen alten Screenshot finden, auf dem ich euch fix in der Grafik zeigen könnte, welche Klasse was repräsentiert. *weiterstöber*
-
http://img408.imageshack.us/i/edgeshot.png/
Hier ein Link zu einem sehr alten Screenshot, als ich mal experimentell, aber auf andere Art und Weise, Kanten graphisch umgesetzt habe. Aber dort lässt sich gut sehen, was ich mit EdgeElements meine. Diese EdgeElements soll der Benutzer halt nach seinem Gusto verschieben/verändern können usw.
-
So, ich habe jetzt den Eingangspost mal auf den neuesten Stand gebracht!
-
So, nachdem ich jetzt lange drüber nachgedacht habe, komme ich zu dem Schluss, dass das ganz gut so ist. Allerdings hab ich jetzt noch ein Problem. Und zwar möchte ich dem Benutzer natürlich erlauben, alles in der graphischen Ansicht zu "draggen". Ich denke mal, da ist ein ganz guter Kompromiss, wenn nur der aktive View die ganze Sache animiert, aber das Modell erst gegen Ende der Drag-Operation verändert wird. Mir würde jetzt folgende Implementierung vorschweben:
1. Der View erstellt sich zu beginn einer Drag-Operation ein lokales "Klonmodell".
2. Er schickt alle Standard-Veränderungen während des draggens an einen lokalen Undo-Stack
3. Der Undo-Stack wird minimiert.
4. Der Undo-Stack wird auf das echte Modell "übersetzt"
5. Das lokale Modell wird wieder zerstört.
6. Der übersetzte Undo-Stack wird auf das echte Modell losgelassen und der View synchronisiert dann die alten lokalen Daten entsprechend dem, was er über die Observer-Schnittstelle mitbekommt.Das sollte so ja eigentlich funktionieren. Allerdings würde ich jetzt Ad-Hoc dem Modell eine Klon-Methode verpassen, um das zu erreichen. Sollte ich mir Gedanken darüber machen, dass ich das Modell sozusagen für seine Views erweitere, wo das doch eigentlich total getrennte Dinge sein sollten?
Viele Grüße,
Deci
-
Hier fehlt eindeutig ein heulendes keiner-mag-mich-smiley
