Ein einfaches Nachrichtensystem



  • Guten Tag liebe Mitprogrammierer!
    Nach längerer Abstinenz auf Grund von Internetnichtvorhandenseins nach Umzug erfreue ich euch mal wieder mit meinen Problemen!
    Und zwar habe ich im Zuge meines kleinen Widgetsystems ein Nachrichten"system" entwickelt. Allerdings hat sich nun herausgestellt, dass ich dieses auch in anderen Teilen meines Programm verwenden könnte/möchte (Es handelt sich um ein Messprogramm, das letztendlich einen Flussgraphen mit Events abarbeitet). Da also das Spektrum der Verwendung steigt, möchte ich es möglichst gut durchdacht haben, bevor ich anfange, im alten Widget-Code alles umzuschreiben und größere Teile meines Programms damit zu durchsetzen.
    Also fange ich nun einmal mit den Eckpunkten an, die ich bedacht hatte, als ich es damals geschrieben habe.

    - Möglichst wenige Handler-Funktionen, damit der Klassen-Namensraum von abgeleiteten Klassen (Nachrichtenempfängern) nicht verseucht wird.
    - Es soll dennoch eine komfortable Nachrichtenbehandlung möglich sein.
    - Die Nachrichten sollen durch einen Punkt in das Objekt geleitet werden, damit andere Objekte einfach zuhören können.
    - Man soll der Klassendeklaration auf einen Blick ansehen können, welche Nachrichten sie behandelt (evtl. nicht so wichtig?).
    - Es sollen Wege denkbar sein, diese Nachrichten über Sprachgrenzen hinaus weiterzuleiten (Lua, COM, ...)
    - Nachrichten sollen standardmäßig Bottom-Up verteilt werden. Das ewige weiterleiten an die Basisklasse müffelt. Wenn man es wirklich anders will, kann man zur Zeit OnMessage überschreiben, seine Behandlung machen und gegebenenfalls an die Basisklassen weiterleiten.

    Mit diesen Punkten im Kopf habe ich nun angefangen mit den zwei trivialen Klassen, auf die alles aufbaut.

    struct Message {
        Message( unsigned int id ) : id_(id) {}
    
        const unsigned int id_;
    };
    
    class MessageSink {
    public:
        virtual void OnMessage( Message* m ) = 0;
    };
    

    Jeder Nachrichtenempfänger leitet also von MessageSink ab und schon kann man ihm Nachrichten vom Typ Message schicken.

    Das ist jetzt natürlich noch nicht so prickelnd, weil es nur Nachrichten ohne Nutzdaten gibt. Darum soll man spezielle Nachrichten von Message ableiten und durch die gespeicherte ID in der Basisklasse soll der Empfänger wissen, wie er den Message-Zeiger umcasten darf. Also eigentlich alles so wie zB. bei Win32-Nachrichten.

    Hier eine Helfer-Klasse, damit die Definition und das Verwenden von solchen Nachrichten vereinfacht wird.

    template< typename T, const unsigned int id >
    struct MessageHelper : public Message
    {
        MessageHelper( ) : Message( id )
        {
        }
    
        void Send( MessageSink* target ) {
            target->OnMessage( this );
        }
    
        // For recipients which derived from ChainableMessageSink, it is common to have local sniffers and thus have
        // multiple conversions to MessageSink. This overload seems to be preferred by the compiler in such cases, so
        // that no error message regarding ambiguous conversions is generated. Todo: Check std.
        void Send( ChainableMessageSink* target ) {
            target->OnMessage( this );
        }
    
        static T* Decode( Message* m ) {
            //assert( m->id_ == id );
    	return static_cast< T* >( m );
        }
    
        static const int id_ = id;
    };
    

    Okay, jetzt kann man also seine speziellen Nachrichten mit Nutzlast von MessageHelper ableiten und die Versender/Empfänger davon haben es recht einfach etwas mit den entstehenden Message-Zeigern anzufangen.

    Von der komfortablen Verwendung der Nachrichten in einem Nachrichtenempfänger ist man natürlich noch ziemlich weit entfernt.

    Daher habe ich nun auf Basis von MessageSink die folgende Empfangsbasisklasse definiert:

    class ChainableMessageSink : public MessageSink {
    public:
        void OnMessage( Message* m )
        {
            // for each sink in sinks_ ......
        }
    
        typedef void RetType;
        //typedef ChainConnection RetType;
    
        RetType ChainAdd   ( MessageSink* s );
        RetType ChainRemove( MessageSink* s );
    
    private:
        // Eigentlich werden hier Verbindungsobjekte gespeichert, damit Quelle und Senke unabhängige Lebenszeiten voneinander
        // haben können.
        // std::vector< ChainConnection > conns_;
        std::vector< MessageSink* > sinks_;
    };
    

    Hier können sich also verschiedene "interne" (d.h. in der Klassenhierarchie von abgeleiteten Klassen) und externe MessageSinks registrieren um an Nachrichten an das Objekt beteiligt zu werden.

    Um das lokal auszunutzen, definiere ich weiterhin die folgende Klasse:

    template< typename T >
    class LocalSniffer : public MessageSink {
    public:
        LocalSniffer() {
            SubCast()->ChainAdd( this );
        }
    
        ~LocalSniffer() {
            SubCast()->ChainRemove( this );
        }
    
        T* SubCast() { 
             return static_cast<T*>(this);
        }
    };
    
    // Helfermakros...
    #define MSG_CASE( MessageType, MessageCall ) \
    	case MessageType::id_: \
    	{ \
    		MessageType *msg = MessageType::Decode( m ); \
    		MessageCall; \
    		break; \
    	}
    
    #define MSG_CASE_VOID( MessageType, MessageCall ) \
    	case MessageType::id_: \
    		MessageCall; \
    		break;
    

    Hiervon kann man nun Klassen ableiten die bestimmte Nachrichten oder Mengen von Nachrichten beim Objekt "abfangen" und in irgendeiner Weise verarbeiten. Wenn man diese Klassen nun als Basis für seine eigenen Klassen verwendet, so ergibt sich durch den Konstruktionprozess von Objekten automatisch die geforderte Bottom-Up-Reihenfolge bei der Nachrichtenverarbeitung (Weil Basisklassen zuerst konstruiert werden und sich somit zuerst bei der ChainableMessageSink registrieren).
    Ein Beispiel für einen solchen Sniffer mit den fiktiven Nachrichten TestMessage und TestMessage2 wäre nun:

    template< typename Subclass, template <typename T> class BaseClass = LocalSniffer >
    class TestMessageSniffer : public BaseClass< Subclass > {
        // Hier würden eigentlich Member-Funktions-Typedef's stehen und im Konstruktor entsprechende Zeiger mit Standard-Werten
        // akzeptiert (Hier: TwelveMessageType pTwelveMessage = &Subclass::OnTwelveMessage).
    
        // Wie kriege ich es hin, dass SubCast()->x() wirklich nur solche x aufruft, die direkt in Subclass vorhanden sind, nicht solchge, die durch
        // Ableitung erreichbar sind (Weil die Basisklasse diese Nachrichten auch behandelt).
        void OnMessage( Message *m ) {
            switch( m->id_ ) {
                MSG_CASE( TestMessage, msg->result = SubCast()->OnTwelveMessage( msg->param1, msg->param2 ) );
                MSG_CASE_VOID( TestMessage2, SubCast()->OnThirteenMessage() );
            }
        }
    };
    

    Wenn man sich nun also bei seinen Nachrichtenempfänger-Klassen an das ChainableMessageSink-LocalSniffer-Muster hält, sind so ziemlich alle eingangs beschriebenen Forderungen erfüllt.
    Aber es stellt sich mir natürlich die Frage, ob man das ganze nicht irgendwie besser, eleganter, flexibler, schöner, professioneller, (denkt euch hier weitere tolle Wörter) lösen könnte. Ich schätze die Lösung hat so ihre Probleme, deren Auftreten ich einfach noch nicht durchdacht habe. Ein wichtiger Haken ist ja im Moment zum Beispiel, dass diese Nachrichten-Verteilungs-Struktur pro Objekt gespeichert wird, wo doch die Struktur für alle Objekte der jeweiligen Klasse gleich bleibt.

    Habt ihr Gedanken dazu? Hinweise, Ratschläge, Vorschläge? Ich wäre euch auf jeden Fall sehr dankbar für alles, da es mir bestimmt hilft, mit besserem Gewissen auf diesem System weiterzuarbeiten oder ein anderes in Betracht zu ziehen.

    Vielste Grüße,
    Michael



  • Muss es denn etwas selbst geschriebenes sein? Ansonsten würde vielleicht boost.signals in Betracht kommen.



  • Hallo da! Danke, dass du dir die Zeit genommen hast, die Textwand durchzuarbeiten, hehe. boost::signal kommt in meiner Problemstellung einfach nicht in Frage. Bei 20 Nachrichten, die von einem Objekt ausgehen, müsste ich ja 20 Signale pro Objekt speichern. Außerdem sind bestimmte Nachrichten auch nur für bestimmte Objekte interessant (Tastendruck zB. nur für das aktive Objekt im Widget-System), manche für eine ganze Gruppe (Eine Gruppe wäre dann eine externe ChainableMessageSink sozusagen, durch die man alle interessierten Objekte erreicht). boost::signal finde ich eher für Dinge gut, wie zB. bei Buttons, wo es dann z.B. ein clicked-Signal gibt, also wo sich die Menge der "Nachrichten" in überschaubaren Grenzen hält.

    Viele Grüße,
    Michael



  • Decimad schrieb:

    Hallo da! Danke, dass du dir die Zeit genommen hast, die Textwand durchzuarbeiten, hehe. boost::signal kommt in meiner Problemstellung einfach nicht in Frage. Bei 20 Nachrichten, die von einem Objekt ausgehen, müsste ich ja 20 Signale pro Objekt speichern. Außerdem sind bestimmte Nachrichten auch nur für bestimmte Objekte interessant (Tastendruck zB. nur für das aktive Objekt im Widget-System), manche für eine ganze Gruppe (Eine Gruppe wäre dann eine externe ChainableMessageSink sozusagen, durch die man alle interessierten Objekte erreicht). boost::signal finde ich eher für Dinge gut, wie zB. bei Buttons, wo es dann z.B. ein clicked-Signal gibt, also wo sich die Menge der "Nachrichten" in überschaubaren Grenzen hält.

    Viele Grüße,
    Michael

    Hmm, Du kannst aber auch Signale erstellen, die nur einen bestimmten Scope überleben. Du musst sie nicht im Objekt speichern. Und empfangen werden Signale ja nur von verbundenen Slots. Vielleicht verstehe ich aber das Problem auch noch nicht so ganz.

    {
      boost::signals::scoped_connection c = sig.connect(ShortLived());
      sig(); // will call ShortLived function object
    }
    sig(); // ShortLived function object no longer connected to sig
    

    Wenn ein SIgnal also nur für einen Slot interessant ist, dann verbindest Du es nur mit dem Interessenten. Du kannst auch Slots blocken, so dass sie, solange sie im geblockten Zustand sind, keine Signale annehmen.

    Um nochmal auf Deine Liste zurückzukommen:

    Decimad schrieb:

    1. Möglichst wenige Handler-Funktionen, damit der Klassen-Namensraum von abgeleiteten Klassen (Nachrichtenempfängern) nicht verseucht wird.
    2. Es soll dennoch eine komfortable Nachrichtenbehandlung möglich sein.
    3. Die Nachrichten sollen durch einen Punkt in das Objekt geleitet werden, damit andere Objekte einfach zuhören können.
    4. Man soll der Klassendeklaration auf einen Blick ansehen können, welche Nachrichten sie behandelt (evtl. nicht so wichtig?).
    5. Es sollen Wege denkbar sein, diese Nachrichten über Sprachgrenzen hinaus weiterzuleiten (Lua, COM, ...)
    6. Nachrichten sollen standardmäßig Bottom-Up verteilt werden. Das ewige weiterleiten an die Basisklasse müffelt. Wenn man es wirklich anders will, kann man zur Zeit OnMessage überschreiben, seine Behandlung machen und gegebenenfalls an die Basisklassen weiterleiten.

    Zu 1.: Da ist boost.signals besser, da jede Memberfunktion als Signalempfänger genutzt werden kann, und Du nicht von irgendetwas erben musst.
    Zu 2.: boost.signals 😉
    Zu 3.: boost.signals macht sowas einfach
    Zu 4.: Da helfen entweder sprechende Namen, oder Kommentare
    Zu 5.: Da brauchst Du so oder so einen Empfänger, welcher die entsprechende Umsetzung macht.
    Zu 6.: Da bist Du mit boost.signals deutlich freier, Da Du keine Basisklasse benötigst, und ein Signal beliebig (und auch auf unterschiedliche Arten) an Empfänger weiterleiten kannst.



  • Hallo da!
    Ich verstehe dein Enthusiasmus für boost::signal und ich habe die auch bei bestimmten Szenarien gerne verwendet (das ist jetzt übertrieben, ich mochte mich nie an den bind/_1 kram für Memberfunktionen gewöhnen, den einem die boost::-varianten nie abnahmen, deshalb habe ich meine eigenen Callback-Funktions-Templates geschrieben).

    Im Prinzip weiß aber das Signal nicht, wen genau es adressiert, wenn es einen gewissen Slot "aufruft". Man kann zwar sogar die Reihenfolge bestimmen/sortieren, aber einen Slot einem bestimmten Objekt zuordnen oder alle Slots eines gewissen Objekts zu finden und dann nur diejenigen aufzurufen wird kaum so einfach gehen.

    Was ist, wenn bestimmte Nachrichten eben nicht an alle versendet werden sollen, die sich zwar potentiell für den Nachrichtentyp interessieren, sondern nur ein Interessent gemeint ist?
    Ein Beispiel: Ziemlich viele Widgets interessieren sich für einen Tastendruck (oder auch Bewegung, Größenänderung, Aktivierung...), aber nur das vom System als aktiv bestimmte Widget soll wirklich benachrichtigt werden.
    All solche Events müssten also als signal pro Widget implementiert werden (Namensraum-verseuchung, Speicherverschwendung!). Dann könnten natürlich auch wieder andere Widgets (wie in meinem System) bei an ein bestimmtes Widget gerichtete Nachrichten mitlauschen.

    Jetzt könnte man natürlich die Ereignistypen zerlegen in die genannten Klassen und diese unterschiedlich behandeln, aber dann ist mindestens Punkt 3 verletzt und die Übersetzung der Ereignisse über Sprachgrenzen dürfte auch nicht mehr so einheitlich werden).

    Vielleicht verstehe aber auch ich dich einfach gar nicht. Könntest du etwas pseudo-code für beispielsweise ein WidgetMgr/Widget-System schreiben, dass ich deinen Vorschlag besser überblicken kann?

    Viele Grüße,
    Michael

    PS: Ach verdammt, muss jetzt hier vom Rechner weg, wahrscheinlich komme ich erst morgen wieder dazu, hier zu lesen -.-



  • Decimad schrieb:

    Was ist, wenn bestimmte Nachrichten eben nicht an alle versendet werden sollen, die sich zwar potentiell für den Nachrichtentyp interessieren, sondern nur ein Interessent gemeint ist?
    Ein Beispiel: Ziemlich viele Widgets interessieren sich für einen Tastendruck (oder auch Bewegung, Größenänderung, Aktivierung...), aber nur das vom System als aktiv bestimmte Widget soll wirklich benachrichtigt werden.
    All solche Events müssten also als signal pro Widget implementiert werden (Namensraum-verseuchung, Speicherverschwendung!). Dann könnten natürlich auch wieder andere Widgets (wie in meinem System) bei an ein bestimmtes Widget gerichtete Nachrichten mitlauschen.

    typedef boost::signal<void (WidgetMessageType)> WidgetSignalType;
    
    void SomeSignalSource::SendSignal(WidgetMessageType const & messsage)
    {
        WidgetSignalType signal;
        //abstrakt. hier muss gebunden werden ;)
        boost::signals::scoped_connection c = signal.connect(WidgetManager::getInstance().getActiveWidget().getSlot());
        signal(message);
       //hier sterben das Signal und die Verbindung wieder. Nur das aktive Widget hat die Nachricht bekommen.
    }
    


  • Nachtrag:

    Die Message könnte bei boost.signals natürlich auch so etwas sein, wie Deine abstrakte Basisklasse + Message-Helper.

    Dabei gibst Du aber Typsicherheit auf. T im Empfänger muss ja T im Sender entsprechen, sonst macht es *bumm*. Der assert lässt Dich das immerhin merken.
    Du hast dann also nur noch einen Slot, aber darin müssen dann die verschiedenen Messages ausgepackt werden. Das wird also eine Orgie aus switch(message.id) und static_cast<T*>(message) (Decode muss ja für das passende T aufgerufen werden).
    Anstatt vieler kleiner, typsicherer Funktionen bzw. Slots hast Du dann also eine dicke unsichere Funktion, welche das gleiche machen würde. Ob das gerade besser ist, sei mal dahin gestellt.



  • Guten Abend da! Man hat mir kurz den WLAN-Notebook geliehen, daher kann ich mich fix melden.
    Dein Beispiel hätte nun ja eigentlich auch keinen Unterschied mehr zu einem einfachen Methodenaufruf, wenn ich das richtig sehe? Man verlöre ja sozusagen die Möglichkeit, Nachrichten an ein Fenster "mitzulauschen" und im Falle von der absoluten Typsicherheit müsste man auch alle nötigen Slots in jedem Widget zur Verfügung haben, selbst wenn es sich um beispielsweise Tastatureingaben nicht kümmert. Zudem könnte es ja mehrere Slots für ein Event in einem Objekt geben. In deiner typunsicheren Variante bliebe natürlich nur noch die Sache mit dem "mitlauschen" als "Mangel".
    Dieses Mitlauschen ohne Einwirkung/Kenntnisnahme des belauschten finde ich aber in einigen Situationen echt praktisch (zb. beim Anheften von Widgets und ähnlichem).

    Ich weiß jetzt nicht ob ich wirklich gut erklärt habe, wie ich mein System derzeit verwende. Die switch/case Orgien finden eigentlich nur in den Sniffer-Klassen statt, die ich zu gewissen Verbundmengen von Nachrichten erstelle (z.B. alle Keyboard-Nachrichten, Maus-Nachrichten, ...). Ein Widget sieht dann zB. so aus:

    class SampleWidget : public Widget, 
        private KeyboardSniffer<SampleWidget>,
        private MouseSniffer<SampleWidget>
    {
        // ...
    
    public:
       void OnKeyDown( ... );
       void OnMouseMove( ... );
    }
    

    Das heißt, ich füge durch Erstellen von neuen Widgets keine neuen möglichen Fehlerursachen mehr her. Wenn der Sniffer korrekt funktioniert, dann ist alles, was ich in meinem Code von der ganzen Sache mitbekomme zumindest dann typsicher.

    Verstehe mich auch bitte nicht falsch, ich versperre mich hier nicht aktiv anderen/besseren Ansichten (ansonsten hätte ich mich hier wohl auch nicht gemeldet, hehe, und ich bin dir dankbar für die viele schöne Zeit, deine Beispiele zB. brennen sich zur Wiederverwendung ebenfalls gerade in mein Hirn ein^^). Es ist nur so, dass ich boost::signal beim Entwurf des Widget-Systems schon in Betracht gezogen hatte, es dann aber nicht als durchweg anwendbar erachtete und mich dann auf die Suche nach einer Lösung begeben habe, die mir einheitlich anwendbar erschien.

    Vielen vielen Dank ersteinmal und noch einen schönen Abend,
    Michael



  • Ich würde gerne mal ein einfaches Anwendungsbeispiel sehen, wie Du Dir die Verwendung Deines Frameworks vorstellst.
    Irgendwie ein Sender und zwei Empfänger, wobei ein Empfänger auf zwei unterschiedliche Nachrichten reagieren soll. Vor allem das Behandeln der Nachrichten im Empfänger in Abhängigkeit vom Nachrichtentypen interessiert mich.

    Wo soll denn Mitlauschen von Nachrichten geschehen? Doch bestimmt in den jeweiligen Empfängern, oder?

    Verstehe mich auch bitte nicht falsch, ich versperre mich hier nicht aktiv anderen/besseren Ansichten (ansonsten hätte ich mich hier wohl auch nicht gemeldet, hehe, und ich bin dir dankbar für die viele schöne Zeit, deine Beispiele zB. brennen sich zur Wiederverwendung ebenfalls gerade in mein Hirn ein^^).

    PS: Ich fühle mich nicht angegriffen, oder so. Ich bin mir nur ziemlich sicher, dass Du Dir unnötig viel Mühe machst. Viele Widgetsysteme benutzen einen Signal-Slot-Mechanismus, der dem von boost.signals sehr ähnlich ist. Für sowas ist das ganze eigentlich genau richtig.



  • Moin moin!

    Also Beispielsweise sieht derzeit meine Umsetzung eines Widgets, das einen Knoten meines Graphen repräsentiert (Sagt man Controller? Mit Nomenklatur habe ich so meine Probleme^^), so aus:

    class NodeWidget : public widgets::FrameWidget, public NodeObserver
    	, visual::geometry_sniffer< NodeWidget >
    	, visual::activation_sniffer< NodeWidget >
    	, visual::geometry_sniffer< NodeWidget, visual::remote_multisniffer >
    {
    public:
    	NodeWidget( GraphControl* view, NodePtr node );
    	~NodeWidget();
    
    // Interface
    public:
    	InputWidget* GetInput ( const std::wstring& name );
    	OutputWidget* GetOutput( const std::wstring& name );
    
    	NodePtr Node() const;
    	std::wstring Name() const;
    
    // State managment
    public:
    	const detail::NodeWidget_properties& Properties() { return props_; }
    
    	void EnterWiring();
    	void LeaveWiring();
    
    // Sniffer handlers
    public:
    	void OnMove( const visual::point& oldPos, const visual::point& newPos );
    	void OnSize( const visual::size& oldPos, const visual::size& newPos );
    	void OnXputMove( const visual::point& oldPos, const visual::point& newPos );
    	void OnXputSize( const visual::size& oldPos, const visual::size& newPos );
    	void OnActivate();
    	void OnDeactivate();
    
    // Node Observer
    public:
    	void NewInput( Input* i );
    	void LostInput( Input* i );
    	void NewOutput( Output* o );
    	void LostOutput( Output* o );
    
       // ...
    };
    

    Erstmal behandelt es Größen/Positions/Aktivierungs-Nachrichten für sich selbst und lauscht auch auf Größenveränderungen seine Eingangs- und Ausgangswidgets (d.h. wenn diese die Größe verändern, wird das Layout neu berechnet, dass das Gesamtkonstrukt noch schön aussieht). Die unter Sniffer Handlers aufgeführten Methoden werden dann bei entsprechenden Nachrichten aufgerufen (Von den Sniffern).

    Wenn jemand eine Nachricht an dieses Widget schicken will, dann einfach so:

    Widget *pWidget = pNodeWidget;
    SomeMessage some();
    some.Send( pWidget );
    // Bearbeitung von eventuellen Ausgabe-Membern (Rückgabewerten) in some.
    

    Für Events, die gebroadcastet werden, kann ich mir ein Event von ChainableMessageSink ableiten (Also eigentlich nur einer ChainableMessageSink einen etwas prägnanteren Namen wie Event oder so geben), bei dem sich interessenten registrieren (ChainAdd) können und die Nachricht wird dann nur an dieses "Event" geschickt und damit automatisch an alle interessenten geschickt (Praktisch äquivalent zu einem signal, außer diese Typunsicherheit zwischen versendung und dem sniffer).

    Hier sieht man zum Beispiel, dass ich mir NodeObserver eigentlich schenken könnte, weil das genau so über das Nachrichtensystem zu implementieren wäre (Und ich dann sozusagen mich nur einmal darum zu kümmern hätte, wenn ich Nachrichten auch über Sprachgrenzen zulassen möchte, weil alle Events und Nachrichten usw. gleichförmig verteilt werden)!

    Die etwas anderen Namen (Groß/Kleinschreibung und Unterstriche) begründen sich dadurch, dass ich zwischen erster Implementierung und jetzt meinen Geschmack in Sachen Benennung geändert habe, nur falls das etwas komisch anmutet.

    Einen schönsten Tag ersteinmal!
    Michael



  • Puh, ich durchdringe Dein Codebeispiel zugegebenermaßen nicht wirklich.

    Wenn Deine Lösung so gut funktioniert, und Deine Anforderungen durch Dein Framework erfüllt werden, dann würde ich das aber so lassen. Ich nehme mal an, dass schon einiges an Code da ist, was auf dem Framework aufbaut, oder? Dann macht es nur Sinn, was daran zu ändern, wenn es irgendwelche gravierenden Mängel an Deiner Lösung gibt.



  • Hurra, ich habe kurz Internet.
    Wollte mich nur fix melden und nochmal danke sagen, ich werde das erstmal so weitermachen, ich kann es ja bei Problemen das nächste Mal einfach besser machen, wenn ich mehr Erfahrung mit diesem System gesammelt habe!

    Viele Grüße,
    Michael


Anmelden zum Antworten