Wie kann ich den Windows Forms-Designer von Visual Studio 2008 dazu bringen, ein Formular zu rendern, das eine abstrakte Basisklasse implementiert?

98

Ich habe ein Problem mit geerbten Steuerelementen in Windows Forms festgestellt und benötige einige Ratschläge dazu.

Ich verwende eine Basisklasse für Elemente in einer Liste (selbst erstellte GUI-Liste aus einem Bedienfeld) und einige geerbte Steuerelemente für jeden Datentyp, der der Liste hinzugefügt werden könnte.

Es gab kein Problem damit, aber ich fand jetzt heraus, dass es richtig wäre, das Basissteuerelement zu einer abstrakten Klasse zu machen, da es Methoden enthält, die in allen geerbten Steuerelementen implementiert werden müssen, die vom Code innerhalb des aufgerufen werden Basissteuerung, darf und kann aber nicht in der Basisklasse implementiert werden.

Wenn ich das Basissteuerelement als abstrakt markiere, lehnt der Visual Studio 2008 Designer das Laden des Fensters ab.

Gibt es eine Möglichkeit, den Designer mit dem abstrakten Basissteuerelement zum Laufen zu bringen?

Oliver Friedrich
quelle

Antworten:

97

Ich wusste, dass es einen Weg geben musste, dies zu tun (und ich fand einen Weg, dies sauber zu tun). Shengs Lösung ist genau das, was ich mir als vorübergehende Problemumgehung ausgedacht habe, aber nachdem ein Freund darauf hingewiesen hat, dass die FormKlasse letztendlich von einer abstractKlasse geerbt wurde , sollten wir in der Lage sein, dies zu erreichen. Wenn sie es können, können wir es schaffen.

Wir sind von diesem Code zum Problem übergegangen

Form1 : Form

Problem

public class Form1 : BaseForm
...
public abstract class BaseForm : Form

Hier kam die erste Frage ins Spiel. Wie bereits erwähnt, wies ein Freund darauf hin, dass System.Windows.Forms.Formeine abstrakte Basisklasse implementiert wird. Wir konnten finden ...

Beweis einer besseren Lösung

Aus diesem Grund wussten wir, dass es dem Designer möglich war, eine Klasse anzuzeigen, die eine abstrakte Basisklasse implementierte. Es konnte einfach keine Designerklasse angezeigt werden, die sofort eine abstrakte Basisklasse implementierte. Dazwischen mussten maximal 5 sein, aber wir haben 1 Abstraktionsebene getestet und zunächst diese Lösung gefunden.

Erste Lösung

public class Form1 : MiddleClass
...
public class MiddleClass : BaseForm
... 
public abstract class BaseForm : Form
... 

Dies funktioniert tatsächlich und der Designer macht es gut, das Problem ist gelöst ... außer dass Sie eine zusätzliche Vererbungsstufe in Ihrer Produktionsanwendung haben, die nur aufgrund einer Unzulänglichkeit im Winforms-Designer erforderlich war!

Dies ist keine 100% todsichere Lösung, aber sie ist ziemlich gut. Grundsätzlich verwenden Sie #if DEBUG, um die raffinierte Lösung zu finden.

Raffinierte Lösung

Form1.cs

#if DEBUG
public class Form1 : MiddleClass
#else 
public class Form1 : BaseForm
#endif
...

MiddleClass.cs

public class MiddleClass : BaseForm
... 

BaseForm.cs

public abstract class BaseForm : Form
... 

Dies verwendet nur die in "Erstlösung" beschriebene Lösung, wenn sie sich im Debug-Modus befindet. Die Idee ist, dass Sie den Produktionsmodus niemals über einen Debug-Build freigeben und immer im Debug-Modus entwerfen.

Der Designer wird immer mit dem im aktuellen Modus erstellten Code ausgeführt, sodass Sie den Designer nicht im Release-Modus verwenden können. Solange Sie jedoch im Debug-Modus entwerfen und den im Release-Modus erstellten Code freigeben, können Sie loslegen.

Die einzige todsichere Lösung wäre, wenn Sie den Entwurfsmodus über eine Präprozessor-Direktive testen können.

riechen
quelle
3
Haben Ihr Formular und die abstrakte Basisklasse einen Konstruktor ohne Argumente? Denn das ist alles, was wir hinzufügen mussten, damit der Designer ein Formular zeigt, das von einem abstrakten Formular geerbt wurde.
Nr.
Hat super funktioniert! Ich nehme an, ich werde nur die Änderungen vornehmen, die ich an den verschiedenen Klassen vorgenommen habe, die die abstrakte implementieren, und dann die temporäre Mittelklasse wieder entfernen. Wenn ich später jemals weitere Änderungen vornehmen muss, kann ich sie wieder hinzufügen. Die Problemumgehung hat tatsächlich funktioniert. Vielen Dank!
Neminem
1
Ihre Lösung funktioniert hervorragend. Ich kann einfach nicht glauben, dass Visual Studio erfordert, dass Sie durch solche Rahmen springen, um etwas so Gemeinsames zu tun.
RB Davidson
1
Wenn ich jedoch eine Mittelklasse verwende, die keine abstrakte Klasse ist, muss derjenige, der die Mittelklasse erbt, die abstrakte Methode nicht mehr implementieren. Dies macht den eigentlichen Zweck der Verwendung der abstrakten Klasse zunichte ... Wie kann dies behoben werden?
Darius
1
@ ti034 Ich konnte keine Problemumgehung finden. Ich mache einfach, dass die angeblich abstrakten Funktionen aus der Mittelklasse einige Standardwerte haben, die mich leicht daran erinnern können, sie zu überschreiben, ohne dass der Compiler einen Fehler auslösen muss. Wenn die angeblich abstrakte Methode beispielsweise darin besteht, den Titel der Seite zurückzugeben, wird eine Zeichenfolge "Bitte ändern Sie den Titel" zurückgegeben.
Darius
74

@smelch, Es gibt eine bessere Lösung, ohne ein mittleres Steuerelement erstellen zu müssen, selbst für das Debuggen.

Was wir wollen

Definieren wir zunächst die letzte Klasse und die abstrakte Basisklasse.

public class MyControl : AbstractControl
...
public abstract class AbstractControl : UserControl // Also works for Form
...

Jetzt brauchen wir nur noch einen Beschreibungsanbieter .

public class AbstractControlDescriptionProvider<TAbstract, TBase> : TypeDescriptionProvider
{
    public AbstractControlDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(TAbstract)))
    {
    }

    public override Type GetReflectionType(Type objectType, object instance)
    {
        if (objectType == typeof(TAbstract))
            return typeof(TBase);

        return base.GetReflectionType(objectType, instance);
    }

    public override object CreateInstance(IServiceProvider provider, Type objectType, Type[] argTypes, object[] args)
    {
        if (objectType == typeof(TAbstract))
            objectType = typeof(TBase);

        return base.CreateInstance(provider, objectType, argTypes, args);
    }
}

Schließlich wenden wir einfach ein TypeDescriptionProvider-Attribut auf das Abastract-Steuerelement an.

[TypeDescriptionProvider(typeof(AbstractControlDescriptionProvider<AbstractControl, UserControl>))]
public abstract class AbstractControl : UserControl
...

Und das ist es. Keine mittlere Steuerung erforderlich.

Und die Provider-Klasse kann in derselben Lösung auf so viele abstrakte Basen angewendet werden, wie wir möchten.

* EDIT * In der app.config wird auch Folgendes benötigt

<appSettings>
    <add key="EnableOptimizedDesignerReloading" value="false" />
</appSettings>

Danke @ user3057544 für den Vorschlag.


jucardi
quelle
1
Dies funktionierte auch für mich, dass ich CF 3.5 benutze, gibt es keineTypeDescriptionProvider
Adrian Botor
4
Konnte dies in VS 2010 nicht zum Laufen bringen, obwohl Smelch's funktioniert hat. Weiß jemand warum?
RobC
5
@RobC Designer ist aus irgendeinem Grund etwas mürrisch. Ich stellte fest, dass ich nach der Implementierung dieses Fixes die Lösung bereinigen, VS2010 schließen und neu starten und neu erstellen musste. dann würde es mich die Unterklasse entwerfen lassen.
Oblivious Sage
3
Es ist erwähnenswert, dass visuelle Elemente, die im Designer für die abstrakte Klasse hinzugefügt wurden, beim Entwerfen der Unterklassen nicht verfügbar sind , da dieser Fix eine Instanz der Basisklasse für die abstrakte Klasse ersetzt .
Oblivious Sage
1
Dies funktionierte für mich, aber ich musste VS 2013 nach dem Erstellen des Projekts zuerst neu starten. @ObliviousSage - Danke für das Heads-up; Zumindest in meinem aktuellen Fall ist dies kein Problem, aber es ist trotzdem ein gutes Problem, auf das man achten muss.
InteXX
10

@ Melch, danke für die hilfreiche Antwort, da ich kürzlich auf dasselbe Problem gestoßen bin.

Im Folgenden finden Sie eine geringfügige Änderung an Ihrem Beitrag, um Kompilierungswarnungen zu vermeiden (indem Sie die Basisklasse in die #if DEBUGVorprozessor-Direktive aufnehmen):

public class Form1
#if DEBUG  
 : MiddleClass 
#else  
 : BaseForm 
#endif 
Dave Clemmer
quelle
5

Ich hatte ein ähnliches Problem, fand aber einen Weg, Dinge umzugestalten, um eine Schnittstelle anstelle einer abstrakten Basisklasse zu verwenden:

interface Base {....}

public class MyUserControl<T> : UserControl, Base
     where T : /constraint/
{ ... }

Dies gilt möglicherweise nicht für jede Situation, führt jedoch nach Möglichkeit zu einer saubereren Lösung als die bedingte Kompilierung.

Jan Hettich
quelle
1
Könnten Sie ein etwas vollständigeres Codebeispiel bereitstellen? Ich versuche Ihr Design besser zu verstehen und werde es auch in VB übersetzen. Vielen Dank.
InteXX
Ich weiß, dass dies alt ist, aber ich fand, dass dies die am wenigsten hackige Lösung war. Da ich immer noch wollte, dass meine Schnittstelle an das UserControl gebunden ist, fügte ich UserControlder Schnittstelle eine Eigenschaft hinzu und verwies darauf, wann immer ich direkt darauf zugreifen musste. In meinen Schnittstellenimplementierungen erweitere ich UserControl und setze die UserControlEigenschaft aufthis
chanban
3

Ich verwende die Lösung in dieser Antwort auf eine andere Frage, die diesen Artikel verlinkt . Der Artikel empfiehlt die Verwendung einer benutzerdefinierten TypeDescriptionProviderund konkreten Implementierung der abstrakten Klasse. Der Designer fragt den benutzerdefinierten Anbieter, welche Typen verwendet werden sollen, und Ihr Code kann die konkrete Klasse zurückgeben, sodass der Designer zufrieden ist, während Sie die vollständige Kontrolle darüber haben, wie die abstrakte Klasse als konkrete Klasse angezeigt wird.

Update: Ich habe ein dokumentiertes Codebeispiel in meine Antwort auf diese andere Frage aufgenommen. Der Code dort funktioniert, aber manchmal muss ich einen Bereinigungs- / Erstellungszyklus durchlaufen, wie in meiner Antwort angegeben, damit er funktioniert.

Carl G.
quelle
3

Ich habe einige Tipps für Leute, die sagen, dass das TypeDescriptionProvidervon Juan Carlos Diaz nicht funktioniert und die bedingte Zusammenstellung auch nicht mag:

Zunächst müssen Sie möglicherweise Visual Studio neu starten, damit die Änderungen in Ihrem Code im Formular-Designer funktionieren (ich musste, die einfache Neuerstellung hat nicht funktioniert - oder nicht jedes Mal).

Ich werde meine Lösung dieses Problems für den Fall der abstrakten Basisform vorstellen. Angenommen, Sie haben eine BaseFormKlasse und möchten, dass darauf basierende Formulare gestaltbar sind (dies wird der Fall sein Form1). Das TypeDescriptionProvidervon Juan Carlos Diaz vorgestellte hat auch bei mir nicht funktioniert. So habe ich es zum Laufen gebracht, indem ich es mit der MiddleClass-Lösung (per Smelch) verbunden habe, aber ohne das#if DEBUG bedingte Kompilieren und mit einigen Korrekturen:

[TypeDescriptionProvider(typeof(AbstractControlDescriptionProvider<BaseForm, BaseFormMiddle2>))]   // BaseFormMiddle2 explained below
public abstract class BaseForm : Form
{
    public BaseForm()
    {
        InitializeComponent();
    }

    public abstract void SomeAbstractMethod();
}


public class Form1 : BaseForm   // Form1 is the form to be designed. As you see it's clean and you do NOTHING special here (only the the normal abstract method(s) implementation!). The developer of such form(s) doesn't have to know anything about the abstract base form problem. He just writes his form as usual.
{
    public Form1()
    {
        InitializeComponent();
    }

    public override void SomeAbstractMethod()
    {
        // implementation of BaseForm's abstract method
    }
}

Beachten Sie das Attribut in der BaseForm-Klasse. Dann müssen Sie nur noch die TypeDescriptionProviderund zwei Mittelklassen deklarieren , aber keine Sorge, sie sind für den Entwickler von Form1 unsichtbar und irrelevant . Der erste implementiert die abstrakten Elemente (und macht die Basisklasse nicht abstrakt). Der zweite ist leer - der VS-Formular-Designer muss nur funktionieren. Dann weisen Sie die zweite Mittelschicht auf die TypeDescriptionProvidervon BaseForm. Keine bedingte Kompilierung.

Ich hatte noch zwei Probleme:

  • Problem 1: Nach dem Ändern von Form1 in Designer (oder einem Code) wurde der Fehler erneut ausgegeben (beim erneuten Öffnen in Designer).
  • Problem 2: Die Steuerelemente von BaseForm wurden falsch platziert, als die Größe von Form1 im Designer geändert und das Formular geschlossen und im Formular-Designer erneut geöffnet wurde.

Das erste Problem (Sie haben es möglicherweise nicht, weil es mich in meinem Projekt an wenigen anderen Stellen verfolgt und normalerweise die Ausnahme "Typ X kann nicht in Typ X konvertiert werden" hervorruft). Ich löste es in der TypeDescriptionProviderdurch die Typnamen zu vergleichen (Fullname) anstelle der Arten zu vergleichen (siehe unten).

Das zweite Problem. Ich weiß nicht genau, warum die Steuerelemente des Basisformulars in der Form1-Klasse nicht entworfen werden können und ihre Positionen nach dem Ändern der Größe verloren gehen, aber ich habe es umgangen (keine gute Lösung - wenn Sie es besser wissen, schreiben Sie bitte). Ich verschiebe die Schaltflächen der BaseForm einfach manuell (die sich in der unteren rechten Ecke befinden sollten) an ihre korrekten Positionen in einer Methode, die asynchron vom Ladeereignis der BaseForm aufgerufen wird: BeginInvoke(new Action(CorrectLayout));Meine Basisklasse hat nur die Schaltflächen "OK" und "Abbrechen" Fall ist einfach.

class BaseFormMiddle1 : BaseForm
{
    protected BaseFormMiddle1()
    {
    }

    public override void SomeAbstractMethod()
    {
        throw new NotImplementedException();  // this method will never be called in design mode anyway
    }
}


class BaseFormMiddle2 : BaseFormMiddle1  // empty class, just to make the VS designer working
{
}

Und hier haben Sie die leicht modifizierte Version von TypeDescriptionProvider:

public class AbstractControlDescriptionProvider<TAbstract, TBase> : TypeDescriptionProvider
{
    public AbstractControlDescriptionProvider()
        : base(TypeDescriptor.GetProvider(typeof(TAbstract)))
    {
    }

    public override Type GetReflectionType(Type objectType, object instance)
    {
        if (objectType.FullName == typeof(TAbstract).FullName)  // corrected condition here (original condition was incorrectly giving false in my case sometimes)
            return typeof(TBase);

        return base.GetReflectionType(objectType, instance);
    }

    public override object CreateInstance(IServiceProvider provider, Type objectType, Type[] argTypes, object[] args)
    {
        if (objectType.FullName == typeof(TAbstract).FullName)  // corrected condition here (original condition was incorrectly giving false in my case sometimes)
            objectType = typeof(TBase);

        return base.CreateInstance(provider, objectType, argTypes, args);
    }
}

Und das ist es!

Sie müssen den zukünftigen Entwicklern von Formularen, die auf Ihrer BaseForm basieren, nichts erklären, und sie müssen keine Tricks ausführen, um ihre Formulare zu entwerfen! Ich denke, es ist die sauberste Lösung, die es geben kann (mit Ausnahme der Neupositionierung der Steuerelemente).

Noch ein Tipp:

Wenn sich der Designer aus irgendeinem Grund immer noch weigert, für Sie zu arbeiten, können Sie jederzeit den einfachen Trick ausführen, indem Sie das public class Form1 : BaseFormin public class Form1 : BaseFormMiddle1(oder BaseFormMiddle2) in der Codedatei ändern , es im VS-Formular-Designer bearbeiten und dann wieder zurück ändern. Ich bevorzuge diesen Trick gegenüber der bedingten Kompilierung, da es weniger wahrscheinlich ist, dass die falsche Version vergessen und veröffentlicht wird .

PW
quelle
1
Dies löste das Problem, das ich mit Juans Lösung in VS 2013 hatte. Beim Neustart von VS werden die Steuerelemente jetzt konsistent geladen.
Luke Merrett
3

Ich habe einen Tipp für Juan Carlos Diaz Lösung. Es funktioniert gut für mich, war aber ein Problem damit. Wenn ich VS starte und Designer eingebe, funktioniert alles einwandfrei. Nachdem Sie die Lösung ausgeführt haben, stoppen und beenden Sie sie und versuchen Sie dann, Designer einzugeben. Die Ausnahme wird immer wieder angezeigt, bis VS neu gestartet wird. Aber ich habe die Lösung dafür gefunden - alles, was Sie tun müssen, ist unten zu Ihrer app.config hinzuzufügen

  <appSettings>
   <add key="EnableOptimizedDesignerReloading" value="false" />
  </appSettings>
user3057544
quelle
2

Da die abstrakte Klasse public abstract class BaseForm: Formeinen Fehler ausgibt und die Verwendung des Designers vermeidet, habe ich virtuelle Mitglieder verwendet. Anstatt abstrakte Methoden zu deklarieren, habe ich im Grunde genommen virtuelle Methoden mit dem geringstmöglichen Text deklariert. Folgendes habe ich getan:

public class DataForm : Form {
    protected virtual void displayFields() {}
}

public partial class Form1 : DataForm {
    protected override void displayFields() { /* Do the stuff needed for Form1. */ }
    ...
}

public partial class Form2 : DataForm {
    protected override void displayFields() { /* Do the stuff needed for Form2. */ }
    ...
}

/* Do this for all classes that inherit from DataForm. */

Da DataFormes sich bei dem abstrakten Mitglied um eine abstrakte Klasse handeln sollte displayFields, "fälsche" ich dieses Verhalten mit virtuellen Mitgliedern, um die Abstraktion zu vermeiden. Der Designer beschwert sich nicht mehr und alles funktioniert gut für mich.

Es ist eine besser lesbare Problemumgehung, aber da es nicht abstrakt ist, muss ich sicherstellen, dass alle untergeordneten Klassen von DataFormimplementiert sind displayFields. Seien Sie daher vorsichtig, wenn Sie diese Technik verwenden.

Gabriel L.
quelle
Damit bin ich gegangen. Ich habe gerade NotImplementedException in die Basisklasse geworfen, um den Fehler offensichtlich zu machen, wenn er vergessen wird.
Shaun Rowan
1

Der Windows Forms Designer erstellt eine Instanz der Basisklasse Ihres Formulars / Steuerelements und wendet das Analyseergebnis von an InitializeComponent. Aus diesem Grund können Sie das vom Projektassistenten erstellte Formular entwerfen, ohne das Projekt zu erstellen. Aufgrund dieses Verhaltens können Sie auch kein Steuerelement entwerfen, das von einer abstrakten Klasse abgeleitet ist.

Sie können diese abstrakten Methoden implementieren und eine Ausnahme auslösen, wenn sie nicht im Designer ausgeführt wird. Der vom Steuerelement abgeleitete Programmierer muss eine Implementierung bereitstellen, die Ihre Basisklassenimplementierung nicht aufruft. Andernfalls würde das Programm abstürzen.

Sheng Jiang 蒋 晟
quelle
Schade, aber so wird es noch gemacht. Ich hoffte auf einen richtigen Weg, dies zu tun.
Oliver Friedrich
Es gibt einen besseren Weg, siehe Smelchs Antwort
Allen Rice
-1

Sie können das abstractSchlüsselwort nur bedingt kompilieren, ohne eine separate Klasse einzufügen:

#if DEBUG
  // Visual Studio 2008 designer complains when a form inherits from an 
  // abstract base class
  public class BaseForm: Form {
#else
  // For production do it the *RIGHT* way.
  public abstract class BaseForm: Form {
#endif

    // Body of BaseForm goes here
  }

Dies funktioniert, sofern BaseFormkeine abstrakten Methoden vorhanden sind (das abstractSchlüsselwort verhindert daher nur die Laufzeitinstanziierung der Klasse).

Peter Gluck
quelle