Bestes Design für Windows-Formulare mit gemeinsamen Funktionen

20

In der Vergangenheit habe ich Vererbung verwendet, um die Erweiterung von Windows-Formularen in meiner Anwendung zu ermöglichen. Wenn alle meine Formulare über gemeinsame Steuerelemente, Grafiken und Funktionen verfügen würden, würde ich ein Basisformular erstellen, das die gemeinsamen Steuerelemente und Funktionen implementiert, und dann zulassen, dass andere Steuerelemente von diesem Basisformular erben. Ich habe jedoch ein paar Probleme mit diesem Design gestoßen.

  1. Steuerelemente können sich jeweils nur in einem Container befinden, daher sind statische Steuerelemente schwierig. Beispiel: Angenommen, Sie hatten ein Basisformular namens BaseForm, das eine TreeView enthielt, die Sie geschützt und statisch machen, damit alle anderen (abgeleiteten) Instanzen dieser Klasse dieselbe TreeView ändern und anzeigen können. Dies funktioniert nicht für mehrere Klassen, die von BaseForm erben, da sich TreeView jeweils nur in einem Container befinden kann. Es wäre wahrscheinlich auf dem letzten Formular initialisiert. Obwohl jede Instanz das Steuerelement bearbeiten konnte, wurde es jeweils nur in einer Instanz angezeigt. Natürlich gibt es Workarounds, aber sie sind alle hässlich. (Dies scheint mir ein wirklich schlechtes Design zu sein. Warum können nicht mehrere Container Zeiger auf dasselbe Objekt speichern? Wie auch immer, es ist, was es ist.)

  2. Status zwischen Formularen, dh Schaltflächenstatus, Bezeichnungstext usw. Ich muss globale Variablen für den Status beim Laden verwenden und ihn zurücksetzen.

  3. Dies wird vom Visual Studio-Designer nicht sehr gut unterstützt.

Gibt es ein besseres und dennoch leicht zu wartendes Design? Oder ist Formularvererbung immer noch der beste Ansatz?

Update Ich wechselte von MVC zu MVP zum Observer-Pattern zum Event-Pattern. Hier ist, was ich im Moment denke, bitte Kritik:

Meine BaseForm-Klasse enthält nur die Steuerelemente und Ereignisse, die mit diesen Steuerelementen verbunden sind. Alle Ereignisse, für deren Verarbeitung eine beliebige Logik erforderlich ist, werden sofort an die BaseFormPresenter-Klasse übergeben. Diese Klasse verarbeitet die Daten von der Benutzeroberfläche, führt alle logischen Operationen aus und aktualisiert dann das BaseFormModel. Das Modell stellt Ereignisse, die bei Statusänderungen ausgelöst werden, der Presenter-Klasse zur Verfügung, die es abonniert (oder beobachtet). Wenn der Präsentator die Ereignisbenachrichtigung erhält, führt er eine beliebige Logik aus, und der Präsentator ändert die Ansicht entsprechend.

Es gibt nur eine von jeder Model-Klasse im Speicher, aber möglicherweise gibt es viele Instanzen der BaseForm und daher des BaseFormPresenter. Dies würde mein Problem der Synchronisierung jeder Instanz von BaseForm mit demselben Datenmodell lösen.

Fragen:

Auf welcher Ebene sollten Inhalte wie die zuletzt gedrückte Schaltfläche gespeichert werden, damit ich sie für den Benutzer (wie in einem CSS-Menü) zwischen Formularen hervorheben kann?

Bitte kritisieren Sie diesen Entwurf. Danke für Ihre Hilfe!

Jonathan Henson
quelle
Ich verstehe nicht, warum Sie gezwungen sind, globale Variablen zu verwenden, aber wenn ja, dann muss es mit Sicherheit einen besseren Ansatz geben. Vielleicht Fabriken, um gemeinsame Komponenten mit Komposition statt Vererbung zu schaffen?
Stijn
Ihr gesamtes Design ist fehlerhaft. Wenn Sie bereits verstehen, was Sie tun möchten, wird von Visual Studio nicht unterstützt, sollte Ihnen dies etwas mitteilen.
Ramhound
1
@Ramhound Es wird von Visual Studio unterstützt, nur nicht gut. So fordert Microsoft Sie auf, dies zu tun. Ich finde es nur ein Schmerz in der A. msdn.microsoft.com/en-us/library/aa983613(v=vs.71).aspx Wie auch immer , wenn Sie eine bessere Idee haben, ich bin alle Augen.
Jonathan Henson
@stijn Ich nehme an, ich könnte eine Methode in jeder Basisform haben, die die Steuerelementzustände in eine andere Instanz lädt. dh LoadStatesToNewInstance (BaseForm-Instanz). Ich könnte es jederzeit nennen, wenn ich eine neue Form dieses Basistyps zeigen möchte. form_I_am_about_to_hide.LoadStatesToNewInstance (this);
Jonathan Henson
Ich bin kein .NET-Entwickler. Können Sie Punkt 1 näher erläutern? Ich könnte eine Lösung haben
Imran Omar Bukhsh

Antworten:

6
  1. Ich weiß nicht, warum Sie eine statische Kontrolle benötigen. Vielleicht weißt du etwas, was ich nicht weiß. Ich habe viel visuelle Vererbung verwendet, aber ich habe noch nie gesehen, dass statische Steuerelemente erforderlich sind. Wenn Sie ein gemeinsames Strukturansichts-Steuerelement haben, lassen Sie jede Formularinstanz eine eigene Instanz des Steuerelements haben und geben Sie eine einzelne Instanz der an die Strukturansichten gebundenen Daten frei .

  2. Das Teilen des Kontrollstatus (im Gegensatz zu Daten) zwischen Formularen ist ebenfalls eine ungewöhnliche Anforderung. Sind Sie sicher, dass FormB über den Status der Schaltflächen in FormA Bescheid wissen muss? Berücksichtigen Sie die MVP- oder MVC-Designs. Stellen Sie sich jedes Formular als eine dumme "Ansicht" vor, die nichts über die anderen Ansichten oder sogar die Anwendung selbst weiß. Überwachen Sie jede Ansicht mit einem intelligenten Presenter / Controller. Wenn es sinnvoll ist, kann ein Präsentator mehrere Ansichten überwachen. Ordnen Sie jeder Ansicht ein Statusobjekt zu. Wenn Sie einen Status haben, der zwischen Ansichten geteilt werden muss, lassen Sie den / die Präsentator (en) dies vermitteln (und prüfen Sie die Datenbindung - siehe unten).

  3. Einverstanden, Visual Studio wird Ihnen Kopfschmerzen bereiten. Wenn Sie die Vererbung von Formularen oder Benutzerkontrollen in Betracht ziehen, müssen Sie die Vorteile sorgfältig gegen die potenziellen (und wahrscheinlichen) Kosten des Ringens mit den frustrierenden Macken und Einschränkungen des Formularentwicklers abwägen. Ich schlage vor, die Vererbung von Formularen auf ein Minimum zu beschränken - verwenden Sie sie nur, wenn die Auszahlung hoch ist. Denken Sie daran, dass Sie als Alternative zur Unterklasse ein gemeinsames "Basis" -Formular erstellen und es einfach einmal für jedes potenzielle "Kind" instanziieren und dann sofort anpassen können. Dies ist sinnvoll, wenn die Unterschiede zwischen den einzelnen Versionen des Formulars im Vergleich zu den gemeinsamen Aspekten geringfügig sind. (IOW: komplexe Grundform, nur geringfügig komplexere Kinderformen)

Verwenden Sie Benutzersteuerelemente, um zu verhindern, dass die Entwicklung der Benutzeroberfläche erheblich dupliziert wird. Berücksichtigen Sie die Benutzersteuerungsvererbung, wenden Sie jedoch dieselben Überlegungen an wie bei der Formularvererbung.

Ich denke, der wichtigste Rat, den ich anbieten kann, ist, dass ich Sie nachdrücklich dazu ermutige, damit zu beginnen, wenn Sie derzeit keine Form des Ansichts- / Controller-Musters verwenden. Es zwingt Sie, die Vorteile von Loose-Couping und Layer-Separation kennen und schätzen zu lernen.

Antwort auf Ihr Update

Welche Ebene soll wie das zuletzt gedrückte Zeug speichern, damit ich es für den Benutzer hervorheben kann ...

Sie können den Status zwischen Ansichten teilen, ähnlich wie Sie den Status zwischen einem Präsentator und seiner Ansicht teilen würden. Erstellen Sie eine spezielle Klasse SharedViewState. Der Einfachheit halber können Sie es zu einem Singleton machen oder es im Hauptpräsentator instanziieren und von dort aus an alle Ansichten (über deren Präsentatoren) übergeben. Wenn der Status mit Steuerelementen verknüpft ist, verwenden Sie nach Möglichkeit die Datenbindung. Die meisten ControlEigenschaften können datengebunden sein. Beispielsweise könnte die BackColor-Eigenschaft eines Buttons an eine Eigenschaft Ihrer SharedViewState-Klasse gebunden sein. Wenn Sie diese Bindung für alle Formulare mit identischen Schaltflächen ausführen, können Sie Button1 in allen Formularen durch einfaches Festlegen hervorheben SharedViewState.Button1BackColor = someColor.

Wenn Sie nicht mit WinForms-Datenbindung vertraut sind, klicken Sie auf MSDN und lesen Sie etwas. Es ist nicht schwer. Erfahren Sie mehr über INotifyPropertyChangedund Sie sind auf halbem Weg da.

Hier ist eine typische Implementierung einer viewstate-Klasse mit der Button1BackColor-Eigenschaft als Beispiel:

public class SharedViewState : INotifyPropertyChanged
{
    // boilerplate INotifyPropertyChanged stuff
    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

    // example of a property for data-binding
    private Color button1BackColor;
    public Color Button1BackColor
    {
        get { return button1BackColor; }
        set
        {
            if (value != button1BackColor)
            {
                button1BackColor = value;
                NotifyPropertyChanged("Button1BackColor");
            }
        }
    }
}
Igby Largeman
quelle
Vielen Dank für Ihre nachdenkliche Antwort. Kennen Sie einen guten Verweis auf das View / Controller-Pattern?
Jonathan Henson
1
Ich erinnere mich, dass ich dachte, dass dies ein ziemlich gutes Intro war: codebetter.com/jeremymiller/2007/05/22/…
Igby Largeman
Bitte beachten Sie mein Update.
Jonathan Henson
@ Jonathan Henson: Antwort aktualisiert.
Igby Largeman
5

Ich verwalte eine neue WinForms / WPF-Anwendung, die auf dem MVVM-Muster und den Update-Steuerelementen basiert . Es begann als WinForms, dann erstellte ich eine WPF-Version, weil die Marketingbedeutung einer Benutzeroberfläche gut aussieht. Ich dachte, es wäre eine interessante Designbeschränkung, eine Anwendung beizubehalten, die zwei völlig unterschiedliche UI-Technologien mit demselben Backend-Code (Ansichtsmodelle und Modelle) unterstützt, und ich muss sagen, dass ich mit diesem Ansatz ziemlich zufrieden bin.

Wenn ich in meiner App Funktionen für Teile der Benutzeroberfläche freigeben möchte, verwende ich benutzerdefinierte Steuerelemente oder Benutzersteuerelemente (Klassen, die von WPF UserControl oder WinForms UserControl abgeleitet sind). In der WinForms-Version befindet sich ein UserControl in einem anderen UserControl in einer abgeleiteten Klasse von TabPage, die sich schließlich in einem Registersteuerelement im Hauptformular befindet. In der WPF-Version sind UserControls tatsächlich eine Ebene tiefer verschachtelt. Mit benutzerdefinierten Steuerelementen können Sie auf einfache Weise eine neue Benutzeroberfläche aus zuvor erstellten Benutzersteuerelementen erstellen.

Da ich das MVVM-Muster verwende, stecke ich so viel Programmlogik wie möglich in das ViewModel (einschließlich des Präsentations- / Navigationsmodells ) oder das Modell (je nachdem, ob der Code mit der Benutzeroberfläche zusammenhängt oder nicht), aber da das gleiche ViewModel Wird sowohl von einer WinForms-Ansicht als auch von einer WPF-Ansicht verwendet, darf das ViewModel keinen Code enthalten, der für WinForms oder WPF entwickelt wurde oder der direkt mit der Benutzeroberfläche interagiert. Ein solcher Code MUSS in die Ansicht eingehen.

Auch im MVVM-Muster sollten UI-Objekte nicht miteinander interagieren! Stattdessen interagieren sie mit den Ansichtsmodellen. Beispielsweise würde eine TextBox eine nahegelegene ListBox nicht fragen, welches Element ausgewählt ist. Stattdessen speichert das Listenfeld einen Verweis auf das aktuell ausgewählte Element irgendwo in der Ansichtsmodellebene, und das Textfeld fragt die Ansichtsmodellebene ab, um herauszufinden, was gerade ausgewählt ist. Dies löst Ihr Problem, wenn Sie herausfinden möchten, welche Schaltfläche in einem Formular in einem anderen Formular gedrückt wurde. Sie teilen einfach ein Navigationsmodell-Objekt (das Teil der Ansichtsmodell-Ebene der Anwendung ist) zwischen den beiden Formularen und fügen eine Eigenschaft in das Objekt ein, die angibt, welche Schaltfläche gedrückt wurde.

WinForms selbst unterstützt das MVVM-Muster nicht sehr gut, aber Update Controls bietet seinen eigenen einzigartigen Ansatz, MVVM als Bibliothek, die auf WinForms aufbaut.

Meiner Meinung nach funktioniert dieser Ansatz zur Programmgestaltung sehr gut und ich plane, ihn für zukünftige Projekte zu verwenden. Die Gründe, warum dies so gut funktioniert, sind (1) dass Update Controls Abhängigkeiten automatisch verwaltet, und (2) dass es normalerweise sehr klar ist, wie Sie Ihren Code strukturieren sollten: Der gesamte Code, der mit UI-Objekten interagiert, gehört in die Ansicht, der gesamte Code, der ist UI-bezogen, aber keine Interaktion mit UI-Objekten erforderlich, gehört zum ViewModel. Sehr oft werden Sie Ihren Code in zwei Teile aufteilen, einen Teil für die Ansicht und einen anderen Teil für das ViewModel. Ich hatte einige Probleme beim Entwerfen von Kontextmenüs in meinem System, aber irgendwann hatte ich auch ein Design dafür.

Ich schwärme auch von Update Controls in meinem Blog . Dieser Ansatz ist jedoch gewöhnungsbedürftig und in umfangreichen Apps (z. B. wenn Ihre Listboxen Tausende von Elementen enthalten) können aufgrund der aktuellen Einschränkungen des automatischen Abhängigkeitsmanagements Leistungsprobleme auftreten.

Qwertie
quelle
3

Ich werde dies beantworten, obwohl Sie bereits eine Antwort akzeptiert haben.

Wie andere bereits ausgeführt haben, verstehe ich nicht, warum Sie irgendetwas Statisches verwenden mussten. Das hört sich so an, als hätten Sie etwas sehr Falsches getan.

Wie auch immer, ich hatte das gleiche Problem wie Sie: In meiner WinForms-Anwendung habe ich mehrere Formulare, die einige Funktionen und einige Steuerelemente gemeinsam haben. Außerdem stammen alle meine Formulare bereits von einem Basisformular (nennen wir es "MyForm"), das Funktionen auf Framework-Ebene hinzufügt (unabhängig von der Anwendung). Der Formulardesigner von Visual Studio unterstützt angeblich das Bearbeiten von Formularen, die von anderen Formularen erben, jedoch in Üben Sie es nur, solange Ihre Formulare nichts weiter tun als "Hallo Welt! - OK - Abbrechen".

Am Ende habe ich Folgendes getan: Ich behalte meine allgemeine Basisklasse "MyForm" bei, die sehr komplex ist, und leite alle Formulare meiner Bewerbung daraus ab. Da ich in diesen Formularen absolut nichts mache, hat der VS Forms Designer keine Probleme, sie zu bearbeiten. Diese Formulare bestehen ausschließlich aus dem Code, den der Formular-Designer für sie generiert. Dann habe ich eine separate parallele Hierarchie von Objekten, die ich "Surrogate" nenne und die alle anwendungsspezifischen Funktionen enthalten, wie z. B. das Initialisieren der Steuerelemente, das Behandeln von vom Formular und den Steuerelementen generierten Ereignissen usw. Es gibt eine Eins-zu-Eins-Beziehung Entsprechung zwischen Ersatzklassen und Dialogen in meiner Anwendung: Es gibt eine Basisersatzklasse, die "MyForm" entspricht, dann wird eine andere Ersatzklasse abgeleitet, die "MyApplicationForm" entspricht.

Jeder Ersatz akzeptiert als Konstruktionszeitparameter einen bestimmten Formulartyp und hängt sich an ihn an, indem er sich für seine Ereignisse registriert. Es delegiert auch an base, bis hin zu "MySurrogate", das ein "MyForm" akzeptiert. Dieser Ersatz wird beim "Disposed" -Ereignis des Formulars registriert. Wenn das Formular zerstört wird, ruft der Ersatz ein überschreibbares Element für sich selbst auf, sodass er und alle seine Nachkommen eine Bereinigung durchführen können. (Abmeldung von Veranstaltungen usw.)

Bisher hat es gut funktioniert.

Mike Nakis
quelle