Peter Bucher - Mein Experiment, meine Spielereien, meine Welt...   ·   Stefan Falz   ·   Jürgen Gutsch   ·   Golo Roden   ·   ASP.NET Zone   ·   Microsoft ASP.NET
Willkommen bei ASP.NET Zone. Anmelden | Registrieren | Hilfe

Gastbeitrag: MVP oder MVVM? Was gehoert wohin?

Einleitung

Ein Kollege von mir hat sich intensiv mit den Designpatterns MVP und MVVM auseinandergesetzt und unter anderem auch mit mir diskutiert.
Da ich seine Gedanken sehr interessant fand, kam uns schlussendlich die Idee, diese als Gastbeitrag in meinem Blog zu veröffentlichen.
Der Hauptgedanke dieser Veröffentlichung ist Entwirrung und das Einholen von Feedback zu diesem Thema aus meiner Leserschaft.

In diesem Sinne wünsche ich euch viel Spass mit dem Gastbeitrag und wir freuen uns auf reichliches Feedback.

Gastbeitrag

Die Designpatterns MVP (Model - View - Presenter) und MVVM (Model - View - ViewModel) sind Designpatterns für die Strukturierung und Trennung von Daten, Ansicht und Presentationslogik.
Ich habe mich im Zusammenhang mit der Entwicklung eines Applikationsframework für Businessanwendungen damit beschäftigt und schaute dazu im Internet verschiedene Implementierungen und Blogeinträge an. Mein Verständnis hatte sich dabei nicht vergrössert, sondern erstmal mehr Verwirrung geschaffen:

  • Was ist der Unterschied zwischen einem Model und einem Entity?
  • Was ist ein Presenter?
  • Was ist Presentationslogik?
  • Was ist der Unterschied zwischen Presenter und ViewModel?

Ich versuche möglichst knapp meine Ansicht mitzuteilen und auch Beispiel-Code anzubieten. Im folgenden Beispiel verwende ich das MVP-Pattern:

Entity / Model:

Ein Entity enthält Daten wie Name, Adresse und Ort usw. z.B. eines Customers:

 


public class CustomerEntity
    {
        public int CustomerId { get; set; }

        public string Name { get; set; }

        public string Address { get; set; }

        public string Location { get; set; }
    }

 

Ein Model enthält wiederum ein oder mehrere Entities (z.B. ein zu editierender Customer oder ein Suchergebnis).

Das Model stellt die dazugehörenden Methoden, um Entities zu laden, zu suchen oder abzuspeichern zur Verfügung.
Zusätzlich darf im Model der Status des Views gespeichert werden, z.B. IsCustomerSelected, um gebunden mit der Eigenschaft IsEnabled eines Button zu unterscheiden, ob ein Customer im ListView der Ansicht selektiert wurde und somit editiert werden kann oder nicht.
Eigenschaften wie die eines Customers sollten meiner Meinung nach nicht im Model zur Verfügung gestellt werden, sondern getrennt in einem separaten Entity, obwohl dies in den meisten Artikel bezüglich MVP / MVVM genau anders dargestellt wird.
Im MVVM-Pattern wird von Microsoft empfohlen, die Daten sogar im ViewModel (Presenter) einzufügen; diese Mischung zwischen einerseits Daten und Presentationslogik steht die Möglichkeit entgegen, ableitbare Model-BasisKlassen für Daten und Basisklassen für Presentationslogik zu entwickeln.

In der Basisklasse für Modelle sollte INotifyPropertyChanged implementiert werden, damit die Ansicht, falls mit DataBinding gearbeitet, aktualisiert wird.

 


public class Model : INotifyPropertyChanged
    {
        public void OnPropertyChanged(string propertyName)
        {
            if (this.PropertyChanged != null)
            {
                this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;
    }

public class EditCustomerModel : Model
    {
        private CustomerEntity _customer;

        public void NewCustomer()
        {
            this.Customer = new CustomerEntity();
        }

        public void EditCustomer(int customerId)
        {
            // Code, um Customer Entity zu laden
            // this.Customer = ...
        }

        public CustomerEntity Customer
        {
            get { return _customer; }
            set
            {
                _customer = value;

                this.OnPropertyChanged("Customer");
            }
        }
    }

public class SearchCustomerModel : Model
    {
        private IEnumerable<CustomerEntity> _customers =
new List<CustomerEntity>();

        private CustomerEntity _selectedCustomer = null;

        public CustomerEntity SelectedCustomer
        {
            get { return _selectedCustomer; }
            set
            {
                _selectedCustomer = value;

                // Aktualisiert DataBindings für SelectedCustomer
                this.OnPropertyChanged("SelectedCustomer");

                // Aktualisiert die IsEnabled Eigenschaft des Buttons
                this.OnPropertyChanged("IsCustomerSelected");
            }
        }

        public IEnumerable<CustomerEntity> Customers
        {
            get { return _customers; }
            set
            {
                _customers = value;

                // OnPropertyChanged aktiviert das updaten der Liste in der Ansicht, falls DataBinding verwendet wurde
                this.OnPropertyChanged("Customers");
            }
        }

        public void LoadAllCustomers()
        {
            // Code, um alle CustomerEntities zu laden
            // this.Customers = ...
        }

        public void SearchCustomers(string text)
        {
            // Code, um CustomerEntities zu laden
        }

        public bool IsCustomerSelected
        {
            get
            {
                return this.SelectedCustomer != null ? true : false;
            }
        }
    }

 

 

View:

Die Views hier als Code bereitzustellen, sprengt den Rahmen dieses Artikels. Ich bitte hierzu die Vorstellungskraft des Lesers zu bemühen, um sich die Views für EditCustomerView und SearchCustomerView vorzustellen.
Das Databinding für ListView und SearchCustomerModel.Customers, sowie den Edit Button mit IsCustomerSelected zu verknüpfen, können wir uns vorstellen.
Im View ist eine Referenz auf den Presenter zu setzen, um auf die Methoden des Presenters zugreifen zu können.

 


public partial class SearchCustomerView : Page
    {
        private SearchCustomerPresenter Presenter { get; set; }

        public SearchCustomerView()
        {
            InitializeComponent();

            // Neue Presenter Instanz
            this.Presenter = new SearchCustomerPresenter();

            // Model binden mit DataContext des View für DataBinding
            this.DataContext = this.Presenter.Model;

        }

        private void searchButton_Click_1(object sender, RoutedEventArgs e)
        {
            this.Presenter.SearchCustomers(this.searchTextBox.Text);
        }

        private void loadAllCustomers_Click_1(object sender, RoutedEventArgs e)
        {
            this.Presenter.LoadAllCustomers();
        }
    }

 

Presenter / ViewModel:

Persönlich finde ich die Namensgebung Presenter besser als ViewModel; so kann besser zwischen Model-, View- und Presenterklassen unterschieden werden.
Im Presenter ist die Presentationslogik. Der Presenter delegiert meistens nur vom View zum Model.
Microsoft empfiehlt bezüglich MVVM das ViewModel (Presenter) als DataContext im View zu setzen - dies im Zusammenhang mit Commands, die sich im Presenter befinden und mit Steuerelementen des Views gebunden werden; in meinem Beispiel setze ich als DataContext das Model.

Ich meine, dass das MVVM Schema im Sinne von Microsoft hinterfragt werden muss, denn Microsoft trennt im MVVM-Schema nicht klar zwischen Model und Presenter oder besser ausgedrückt zwischen Daten und Presentation. Das Model klar vom ViewModel (Presenter) zu trennen, ist ja aber gerade die ursprüngliche Idee dieser Pattern MVP und MVVM.

Meiner Ansicht nach gehören Daten ins Model und nicht in die Presentationslogik: deshalb setze ich das Model als DataContext im View und nicht das ViewModel (Presenter).

In meinem Beispiel habe ich eine einfache Implementierung verwendet, ohne Kommunikation zwischen Presenter und View:

 


public class EditCustomerPresenter
    {
        public EditCustomerPresenter()
        {
            this.Model = new EditCustomerModel();
        }

        public void NewCustomer()
        {
            this.Model.NewCustomer();
        }

        public void EditCustomer(int customerId)
        {
            this.Model.EditCustomer(customerId);

            // Falls ohne DataBinding per xaml:
            // UpdateView();
        }

        public EditCustomerModel Model { get; set; }
    }

public class SearchCustomerPresenter
    {
        public SearchCustomerPresenter()
        {
            this.Model = new SearchCustomerModel();
        }

        public SearchCustomerModel Model { get; set; }

        public void SearchCustomers(string text)
        {
            this.Model.SearchCustomers(text);
        }

        public void LoadAllCustomers()
        {
            this.Model.LoadAllCustomers();
        }
    }

 

Um nun an einem ganz kleinen Beispiel zu veranschaulichen, was Presentationslogik ist, erweitere ich die SearchCustomers Methode etwas:

 


public void SearchCustomers(string text)
        {
            this.Model.SearchCustomers(text);
               
MessageBox.Show(String.Format("{0} Kunde(n) gefunden.", this.Model.Customers.Count()));
           
        }

 

Wo sonst sollte die MessageBox die Anzahl der Ergebnisse anzeigen, wenn nicht in der Presentationslogik des Presenters? Diese Info soll natürlich nur die Presenationslogik veranschaulichen; solche unnötigen Infos sollten in Businessapplikationen vermieden werden.

Auch erfahrene Entwickler brauchen etwas Zeit, wenn sie sich zum ersten Mal mit MVP und MVVM beschäftigen, um diese Patterns zu verstehen. Das beste Verständnis habe ich persönlich beim Implementieren eigener MVPs gewonnen und kann das nur weiterempfehlen.

Hier eine fragwürdige Implementierung des MVVM:

http://msdn.microsoft.com/de-de/magazine/dd419663.aspx

Hierzu einige Äusserungen:
Erstmal vorne weg: In diesem Artikel scheint ein Model gar nicht erst vorzukommen; die Daten wurden in das ViewModel, bzw. in den Presenter, also in die Presentationlogik hineingesetzt. Deshalb wurde dort auch die Schnittstelle INotifyPropertyChanged implementiert, die klar ins Model gehört.


Fazit:
 
Der MVP Pattern trennt klar Model und Presenter; der MVVM Pattern scheint diese Trennung nicht mehr anzustreben. Auch wenn wir uns vorstellen, Models zu entwickeln, die mit Hilfe von Generics standardmässig Eigenschaften zur Verfügung stellen, scheint mir die Trennung zwischen Model und Presenter im Sinne der Klassenhierarchie wichtig; So würden wir wohl Basisklassen einerseits für Daten-Modelle und andererseits für Presentationslogik erstellen und kaum beides in eine ViewModel-Klasse zwängen.
Wichtig ist für mich auch die Trennung zwischen Entity und Model.
Der MVVM Pattern scheint einige sinnvolle Eigenheiten des MVP Pattern über Bord zu schmeissen (z.B. klare Trennung zwischen Model und Presenter). Die aufwendige Verwendung von Commands sehe ich nicht als existentielle Erweiterung beim MVVM; ich sehe die MVP Implementierung als einfacher und klarer an.


Stefan Werdenberg, IntegralFramework

Veröffentlicht Mittwoch, 6. Februar 2013 20:14 von Peter Bucher

Kommentare

# re: Gastbeitrag: MVP oder MVVM? Was gehört wohin?

Hallo Peter,

im zweiten Teil des Artikels befindet sich ein Copy&Paste-Fehler. Das Thema Entiy/Model wiederholt sich.

Hallo Stefan,

vielen Dank für den Gastbeitrag in Peters Blog. Leider scheint mir, dass sich hier der Fehlerteufel, vielleicht sogar der Verständnis-Teufel eingeschlichen hat. Bitte siehe meinen Kommentar als konstruktive Kritik an und nicht als persönlichen Angriff.

Was du hier machst ist ein falscher Vergleich zwischen MVVM und MVP. Richtig ist, dass MVVM aus dem MVP entstanden ist, dennoch sind es zwei völlig unterschiedliche Pattern für unterschiedliche Zwecke. MVVM ist von Microsoft entwickelt und speziell für die Arbeit mit XAML-basierten Oberflächen geeignet. MVVM unterstützt das XAML-Databinding. Ein weiterer Vorteil: MVVM unterstützt uns um Testbare UI-Logik im ViewModel zu kapseln und um nicht testbare Logik in Code-Behind-Code einer XAML-Datei zu reduzieren oder ganz zu vermeiden.

Weiterhin sehen und verhalten sich die Patterns grundlegend. Schauen wir mal auf die Beziehungen der einzelnen Komponenten:

MVVM:

View => ViewModel => Model

View instanziiert und Bindet das ViewModel. Das ViewModel besorgt sich das Model und gibt die Daten oder seltener das Model selber an die View weiter.

Hier ist die View der aktive Part. Pro View gibt es üblicherweise ein ViewModel

Bestens geeignet für XAML basierte UI (WPF, Silverlight, WinRT und WindowsPhone)

MVP:

View <= Presenter => Model

Der Presenter instanziiert und hält eine oder mehrere Views. Der Presenter holt sich das Model und gibt die Daten an die View weiter

Hier ist der Presenter der aktive Part.

Geeignet für ASP.NET Webforms, Winforms

Interessant ist auch deine Definition von Model und Entity.

Aus meiner Sicht: Model == Entity == DTO == POCO

IMO entstammt der Begriff Entity aus dem direkten Bezum zu relationalen Datenbanken und ist im Umfeld vom Entity Framework gebräuchlich.

Model bezieht sich auf Datenmodell und kann / muss aber nicht das Abbild eines Datenbank-Schemas sein.

Sowohl Models als auch Entities können verschachtelt sein.

In meinen Fällen enthalten Models/Entities/DTOs/POCOs keine Logik. Die von Dir beschriebene Logik des Ladens oder Suchen eines Customers wird von einem ServiceLayer (oder BusinessLayer) übernommen. Die Models sind dabei wirklich nur reine Datentransfer-Objekte. In der Regel implementieren meine Models nicht INotifyPropertyChanged (hängt von der Art des Projektes ab) sondern deren Daten werden durch Eigenschafted des ViewModels oder verschachtelter ViewModels per Binding an die View gegeben.

Fazit: Keines der beiden Pattern ist besser oder schlechtet, sondern beide sind für ihre speziellen Zwecke absolut geeignet und sind somit gleichwertig zu behandeln und zu bewerten.

Du bist ja Kreuzlinger, wenn du möchtest können wir uns nächste Woche beim .NET-Stammtisch über das Thema intensiver unterhalten oder ich schreibe meine Sicht der Dinge in meinem Blog etwas genauer zusammen.

Viele Grüße

Jürgen

Donnerstag, 7. Februar 2013 08:40 by Jürgen Gutsch

# re: Gastbeitrag: MVP oder MVVM? Was gehört wohin?

Grundsätzlich danke ich für jedes Interesse und bin dankbar für jede Antwort. Als offener Mensch bin ich immer gewillt, Neues anzunehmen und das Bessere zu suchen und anzunehmen. Ein Anwort bezüglich des Kommentars von Jürgen Gutsch; ich kann und will nicht auf jeden Punkt eingehen, um den Umfang nicht zu sprengen.

Natürlich ist es ganz allgemein schwierig, offen für Neues, Anderes und Fremdes zu sein. Wer MVVM kennt und verwendet und zusätzlich von diesem Pattern überzeugt ist, wird vielleicht nicht sofort die Vorteile von MVP und die Nachteile von MVVM erkennen oder verwendet überhaupt die unterschiedlichen Patterns je nach Bedarf. Die Bedürfnisse mögen verschieden sein; Entwickler könnten z.B. Vorteile bezüglich Testen im MVVM wichtiger sein, als die klare Trennung zwischen Presenter und Model.

Die Frage nach dem Sinn des Models taucht immer wieder auf. Was ist das Model, was gehört dort hin? In der Praxis und bezüglich Basisklassen für Modelle, hat es sich sehr bewährt, den gesamten Datenteil in das Model zu setzen; im Model kann - was ich in meinem kurzen Beispiel nicht so ausgeführt habe - natürlich ein ServiceLayer oder eine DataFacade die Daten bereitstellen. Im MVP sollten diese aber nicht im Presenter gespeichert werden, um eine klare Trennung zwischen Daten und Presenter zu erfüllen. Dazu später mehr.

Bezüglich Model == Entity == DTO == POCO

Gerade diese Definition scheint mir eben nicht sinvoll, weil wir dann entgegen dem MVP Pattern ein View - ViewModel - ein Entity oder ein POCO Pattern hätten. In dem per Link erwähnten Beispiel fehlt das Model; so hätten wir also einen View-ViewModel Pattern oder nach Jürgen einen ServiceLayer - ViewModel - View Pattern. Der Vorteil das Model als Container für Entities, ein Entity oder Entitylisten ist im Sinne wiederverwendbarer Basisklassen für Modelle für verschiedene Bedürfnisse offentsichlich.

Natürlich ist der MVP auch für XAML basierte UI bestens geeignet, obwohl allenfalls bei der Testbarkeit Abstriche gemacht werden müssen.

Eine Einschränkung bezüglich MVVM sind "shared models", also Modelle, auf die von mehreren Presentern aus zugregriffen wird, z.B. bei Wizards - dies ist im MVP natürlich auch nur mit Hilfe von Composition umsetzbar. Folgende Szenarien sind mit MVVM und zwar dadurch, dass die Modelle nicht getrennt vom ViewModel bereitgestellt werden, nicht einfach möglich:

1) MDI; falls ein Model für mehrere Presenter eingesetzt wird.

2) Wizards, in dem ein Model für mehrere Ansichten bereitgestellt wird.

3) Verschiedene Ansichten für gleiche Daten (Modelle) (z.B. als Report, als Editierung und als Textansicht).

Dass Models mit dem MVVM zu reinen Datentransver-Objekte reduziert werden, und dabei ein aufgeblähtes ViewModel zu schaffen, sehe ich auch als Nachteil an; wie ich im Artikel mehrfach erwähnt habe und leider in Deiner Antwort keine Erwähnung findet, ist genau dieses Zusammenfügen im MVVM des Presenters und des Models, dass es verunmöglicht, Basisklassen für Modelle und für Presenter zu erstellen. In diesem Zusammenhang ist nochmals darauf hinzuweisen, dass meine Ansicht des Models im Bezug auf wiederverwendbare Basisklassen für Modelle für verschieden Bedürfnisse von Datenbereitstellen, ein enormer Vorteil ist: in einem der nächsten Artikel werde ich diese Möglichkeiten darstellen.

Weiter möchte ich schon vorgreifen, dass diese Trennung zwischen Model und Presenter bezüglich Workflows gleichfalls sehr wichtig ist. Wird nämlich ein Presenter zusätzlich von einer Activityklasse abgeleitet, ist die Trennung zwischen Model und Presenter entscheidend.

Grundsätzlich will ich klar sagen, dass der MVP Pattern gegenüber dem MVVM einige Vorteile aufweist und zwar vor allem in bestimmtem Verwendungszweck, dass aber jeder nach Bedürfnis selber entscheiden mag, welcher Pattern für sein Projekt am besten ist.

Viele Grüsse

Stefan Werdenberg  

Freitag, 8. Februar 2013 09:53 by Stefan Werdenberg

# re: Gastbeitrag: MVP oder MVVM? Was gehört wohin?

Hallo Stefan,

ok, die Namensgebung und das allgemeine Verständnis der zugehörigen Teile ist leider das reine Chaos...

Ich spreche lieber von Model-View-Presenter-Controller., wobei bei MVVM das ViewModel Presenter und Controller beinhaltet.

Aber bitte KEIN ONCLICK !!!

Korrekt angewendet implementiert man ICommand und bindet daran - der View enthält keinen Code! Dort ist nur Code der direkt mit der Anzeige wie z.B. Animationen zu tun hat.

Zur Vereinfachung der INotifyPropertyChanged Katastrophe habe ich eine VM-Basisklasse geschrieben.

http://fpfbaden.wordpress.com/2011/05/23/mvvm-dynamicviewmodel/

Damit ist auch die Kopiererei der Daten vom Model ins ViewModel nicht mehr nötig...

Gerne würde ich am .NET OpenSpace Süd in Karlsruhe darüber diskutieren :-)

Freitag, 8. Februar 2013 11:48 by Frank Pfattheicher

# re: Gastbeitrag: MVP oder MVVM? Was gehört wohin?

Hallo Frank

Danke für Deinen Kommentar.

Ich habe die verlinkte Seite besucht und mir den Code angeschaut; ich sehe das bezüglich dem ersten Teil Deines Artikels genauso, dass diese "Handarbeit" wie sie Microsoft vorschlägt unsinnig ist.  

Du schreibst unter "Mehrere Modelle"

"Im einfachsten Fall basiert ein ViewModel nur auf einem Model.

Aber schon eine einfache Beziehung erfordert die Bindung an mehrere Modelle (z.B. Person und Firma)."

Genau, aber dieser einfache Fall, von nur einem Entity, tritt -wie Du in der Praxis auch festgestellt hast -  jedoch selten ein. In der Praxis müssen meistens Modelle, so wie ich diese sehe, eben nicht nur ein Entity, sondern eine Entityliste oder mehrere verschiedene Entities enthalten.

Jürgen schreibt dazu (und Microsoft sieht das leider auch so):

"Aus meiner Sicht: Model == Entity == DTO == POCO"

Diese Sichtweise ist - worauf Du in Deinem Artikel eingehst - in der Praxis meistens nicht der Fall und kann deshalb auch nicht sinnvoll sein.

Meine Sichtweise:

Model != Entity/POCO

Ein Model (im MVP) oder ViewModel (MVVM) enthält nach meiner Auffasung eines oder mehrere Entities und oder Entitylisten. Zusätzliche können dort Methoden bereitgestellt werden, um Daten zu laden (per ServiceLayer oder DataFacade) oder als konkretes Beispiel die ein Total berechnen.

Weiter sprichst Du die Relation zwischen Entities an, z.B. Person - Firma.

In diesem Zusammenhang (Entities) würde ich Dir empfehlen einmal einen OR-Mapper wie das EntityFramework oder nHibernate anzuschauen - warhscheinlich verwendest Du eines der beiden bereits.

Diese OR-Mapper stellen den Datenzugriff und die dafür notwendigen Entities oder POCOs bereit, die Du dann mitsamt den Relationen in einem Model oder ViewModel für MVVM bereitstellen kannst.

Deine im Artikel vorgestellte Angehensweise per Indexer finde ich nicht gut, da so auch die Typisierung verloren geht.

Fazit:

Das ViewModel (MVVM) oder das Model (MVP) müssen - wie in der Praxis erkannt  - mehrere Entities bereitstellen können. Diese Entites oder POCOs stelle ich, im Zusammenhang mit MVP, in einem Model bereit. Weiter verwende ich Modellbasisklassen, die für bestimmte konkrete Anforderungen entwickelt wurden, wie in meinem Artikel als Beispiel vorgestellte EditCustomerModel oder das SearchCustomerModel.

In einem nächsten Artikel werde ich nochmals ausführlicher auf Modelle eingehen.

Gruss

Stefan

Freitag, 8. Februar 2013 21:36 by Stefan Werdenberg

# re: Gastbeitrag: MVP oder MVVM? Was gehört wohin?

Hallo Stefan,

ich bin ganz bei Dir:

"Das ViewModel (MVVM) ... mehrere Entities bereitstellen können."

Genau aus diesem Grund enthält meine Basisklasse die Methode AddModel()...

Beispiel:

 class MyViewModel : ActiveViewModel

 {

   public string MyProp { get; set; }

   public Customer Kunde { get; set; }

   [DependsOn("FirstName")]

   public int LenOfFirstName { get { return this["FirstName"].ToString().Length; } }

   public MyViewModel()

   {

     MyProp = "super";

     Wait = "warten";

     this["Test"] = "kuckuck";

     this["Toast"] = new { Scheiben = 1, Getoastet = false };

     Kunde = new Customer();

     AddModel(new Customer());

     AddModel("Neu_", new Customer());

     var person = new Customer {FirstName = "Person"};

     AddModel("Person", person);

     var timer = Observable.Interval(TimeSpan.FromSeconds(10)).ObserveOn(SynchronizationContext.Current);

     timer.Subscribe(_ => Wait += ".");

   }

     [ActionMethod(Shortcut = "F1")]

     public void OnAbout()

     {

         MessageBox.Show("Hallo");

     }

 }

Die ganzen Properties (und die der Modelle) stehen im Designer zur direkten Bindung zur Verfügung.

Auch die Implementierung der Kommandos wird durch ein Attribut "ActionMethod" verweinfacht - auch mit Designer support.

Ich glaub ich muss doch nochmal einen ausführlichen Blog schreiben - obwohl ich keine Zukunft für XAML sehe - Microsoft unterstützt .NET nur noch halbherzig :-(

Gruß Frank

Montag, 11. Februar 2013 15:56 by Frank Pfattheicher
Anonyme Kommentare sind nicht zugelassen