Comment puis-je demander au concepteur Windows Forms de Visual Studio 2008 de restituer un formulaire qui implémente une classe de base abstraite?


98

J'ai rencontré un problème avec les contrôles hérités dans Windows Forms et j'ai besoin de conseils à ce sujet.

J'utilise une classe de base pour les éléments d'une liste (liste d'interface graphique faite par moi-même composée d'un panneau) et certains contrôles hérités qui sont pour chaque type de données qui pourraient être ajoutés à la liste.

Il n'y avait aucun problème avec cela, mais j'ai maintenant découvert qu'il serait juste de faire du contrôle de base une classe abstraite, car il a des méthodes, qui doivent être implémentées dans tous les contrôles hérités, appelés à partir du code à l'intérieur du base-control, mais ne doit pas et ne peut pas être implémenté dans la classe de base.

Lorsque je marque le contrôle de base comme abstrait, le concepteur Visual Studio 2008 refuse de charger la fenêtre.

Existe-t-il un moyen de faire travailler le concepteur avec le résumé créé par le contrôle de base?

Réponses:


97

JE SAVAIS qu'il devait y avoir un moyen de le faire (et j'ai trouvé un moyen de le faire proprement). La solution de Sheng est exactement ce que j'ai proposé comme solution de contournement temporaire, mais après qu'un ami ait souligné que la Formclasse a finalement hérité d'une abstractclasse, nous DEVRAIENT être en mesure de le faire. S'ils peuvent le faire, nous pouvons le faire.

Nous sommes passés de ce code au problème

Form1 : Form

Problème

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

C'est là qu'intervient la question initiale. Comme dit précédemment, un ami a souligné qu'il System.Windows.Forms.Formimplémente une classe de base abstraite. Nous avons pu trouver ...

Preuve d'une meilleure solution

À partir de là, nous savions qu'il était possible pour le concepteur de montrer une classe qui implémentait une classe abstraite de base, il ne pouvait tout simplement pas afficher une classe de concepteur qui implémentait immédiatement une classe abstraite de base. Il devait y en avoir au maximum 5 entre les deux, mais nous avons testé 1 couche d'abstraction et avons initialement proposé cette solution.

Solution initiale

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

Cela fonctionne réellement et le concepteur le rend bien, le problème est résolu ... sauf que vous avez un niveau supplémentaire d'héritage dans votre application de production qui n'était nécessaire qu'en raison d'une insuffisance dans le concepteur de winforms!

Ce n'est pas une solution sûre à 100%, mais c'est plutôt bon. Fondamentalement, vous utilisez #if DEBUGpour trouver la solution raffinée.

Solution raffinée

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
... 

Cela n'utilise que la solution décrite dans «solution initiale», si elle est en mode débogage. L'idée est que vous ne publierez jamais le mode production via une version de débogage et que vous concevrez toujours en mode débogage.

Le concepteur s'exécutera toujours sur le code créé dans le mode actuel, vous ne pouvez donc pas utiliser le concepteur en mode version. Cependant, tant que vous concevez en mode débogage et publiez le code intégré en mode version, vous êtes prêt à partir.

La seule solution infaillible serait de pouvoir tester le mode conception via une directive de préprocesseur.


3
Votre formulaire et la classe de base abstraite ont-ils un constructeur sans argument? Parce que c'est tout ce que nous devions ajouter pour que le concepteur travaille à montrer un formulaire hérité d'une forme abstraite.
nos

A très bien fonctionné! Je suppose que je vais simplement apporter les modifications dont j'avais besoin aux différentes classes implémentant la classe abstraite, puis supprimer à nouveau la classe moyenne temporaire, et si jamais j'ai besoin d'apporter plus de modifications plus tard, je peux la rajouter. La solution de contournement a effectivement fonctionné. Merci!
neminem

1
Votre solution fonctionne très bien. Je ne peux tout simplement pas croire que Visual Studio vous oblige à franchir ces obstacles pour faire quelque chose d'aussi commun.
RB Davidson

1
Mais si j'utilise une middleClass qui n'est pas une classe abstraite, alors quiconque hérite de middleClass n'a plus à implémenter la méthode abstraite, cela va à l'encontre de l'objectif même d'utiliser une classe abstraite en premier lieu ... Comment résoudre cela?
Darius

1
@ ti034 Je n'ai trouvé aucune solution de contournement. Je fais donc simplement que les fonctions prétendument abstraites de la middleClass aient des valeurs par défaut qui peuvent facilement me rappeler de les remplacer, sans que le compilateur génère une erreur. Par exemple, si la méthode prétendument abstraite est de renvoyer le titre de la page, alors je lui ferai renvoyer une chaîne "Veuillez changer le titre".
Darius le

74

@smelch, Il existe une meilleure solution, sans avoir à créer un contrôle intermédiaire, même pour le débogage.

Ce que nous voulons

Tout d'abord, définissons la classe finale et la classe abstraite de base.

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

Maintenant, tout ce dont nous avons besoin est un fournisseur de description .

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);
    }
}

Enfin, nous appliquons simplement un attribut TypeDescriptionProvider au contrôle Abastract.

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

Et c'est tout. Aucun contrôle intermédiaire requis.

Et la classe de fournisseur peut être appliquée à autant de bases abstraites que nous le souhaitons dans la même solution.

* EDIT * Les éléments suivants sont également nécessaires dans l'app.config

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

Merci @ user3057544 pour la suggestion.



1
Cela a également fonctionné pour moi que TypeDescriptionProvider
j'utilise

4
Impossible de faire fonctionner cela dans VS 2010, bien que smelch ait fonctionné. Quelqu'un sait pourquoi?
RobC

5
@RobC Designer est un peu grincheux pour une raison quelconque. J'ai trouvé qu'après avoir implémenté ce correctif, je devais nettoyer la solution, fermer et relancer VS2010 et reconstruire; alors cela me permettrait de concevoir la sous-classe.
Oblivious Sage

3
Il est intéressant de noter que, comme ce correctif remplace une instance de la classe de base pour la classe abstraite, les éléments visuels ajoutés dans le Designer pour la classe abstraite ne seront pas disponibles lors de la conception des sous-classes.
Oblivious Sage

1
Cela a fonctionné pour moi, mais j'ai d'abord dû redémarrer VS 2013 après avoir construit le projet. @ObliviousSage - Merci pour le heads-up; dans mon cas actuel, au moins, ce n'est pas un problème, mais c'est quand même un bon problème à surveiller.
InteXX

10

@Smelch, merci pour la réponse utile, car je rencontrais le même problème récemment.

Voici une modification mineure de votre publication pour éviter les avertissements de compilation (en mettant la classe de base dans la #if DEBUGdirective pré-processeur):

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

5

J'ai eu un problème similaire mais j'ai trouvé un moyen de refactoriser les choses pour utiliser une interface à la place d'une classe de base abstraite:

interface Base {....}

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

Cela peut ne pas être applicable à toutes les situations, mais lorsque cela est possible, il en résulte une solution plus propre que la compilation conditionnelle.


1
Pourriez-vous fournir un exemple de code un peu plus complet? J'essaie de mieux comprendre votre design et je vais également le traduire en VB. Merci.
InteXX

Je sais que c'est vieux mais j'ai trouvé que c'était la solution la moins piratée. Comme je voulais toujours que mon interface soit liée à UserControl, j'ai ajouté une UserControlpropriété à l'interface et l'ai référencée chaque fois que j'avais besoin d'y accéder directement. Dans mes implémentations d'interface, j'étends UserControl et définit la UserControlpropriété surthis
chanban

3

J'utilise la solution dans cette réponse à une autre question, qui lie cet article . L'article recommande d'utiliser une TypeDescriptionProviderimplémentation personnalisée et concrète de la classe abstraite. Le concepteur demandera au fournisseur personnalisé quels types utiliser, et votre code peut renvoyer la classe concrète afin que le concepteur soit satisfait pendant que vous avez un contrôle complet sur la façon dont la classe abstraite apparaît en tant que classe concrète.

Mise à jour: j'ai inclus un exemple de code documenté dans ma réponse à cette autre question. Le code fonctionne, mais parfois, je dois passer par un cycle de nettoyage / construction comme indiqué dans ma réponse pour le faire fonctionner.


3

J'ai quelques conseils pour les personnes qui disent que le TypeDescriptionProviderpar Juan Carlos Diaz ne fonctionne pas et n'aiment pas non plus la compilation conditionnelle:

Tout d'abord, vous devrez peut-être redémarrer Visual Studio pour que les modifications de votre code fonctionnent dans le concepteur de formulaires (je devais, la reconstruction simple ne fonctionnait pas - ou pas à chaque fois).

Je présenterai ma solution de ce problème pour le cas de la forme de base abstraite. Disons que vous avez une BaseFormclasse et que vous souhaitez que tous les formulaires basés sur celle-ci soient modifiables (ce sera le cas Form1). Le TypeDescriptionProvidertel que présenté par Juan Carlos Diaz n'a pas fonctionné pour moi aussi. Voici comment je l'ai fait fonctionner, en le joignant à la solution MiddleClass (par smelch), mais sans la#if DEBUG compilation conditionnelle et avec quelques corrections:

[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
    }
}

Notez l'attribut sur la classe BaseForm. Ensuite, il vous suffit de déclarer la classe moyenneTypeDescriptionProvider et les deux classes moyennes , mais ne vous inquiétez pas, elles sont invisibles et non pertinentes pour le développeur de Form1 . Le premier implémente les membres abstraits (et rend la classe de base non abstraite). Le second est vide - il est juste nécessaire au concepteur de formulaires VS de fonctionner. Ensuite, vous affectez la deuxième classe moyenne au TypeDescriptionProviderde BaseForm. Aucune compilation conditionnelle.

J'avais encore deux problèmes:

  • Problème 1: après avoir modifié Form1 dans le concepteur (ou un code), l'erreur a été renvoyée (en essayant de l'ouvrir à nouveau dans le concepteur).
  • Problème 2: les contrôles de BaseForm ont été placés de manière incorrecte lorsque la taille de Form1 a été modifiée dans le concepteur et le formulaire a été fermé et rouvert dans le concepteur de formulaires.

Le premier problème (vous ne l'avez peut-être pas car c'est quelque chose qui me hante dans mon projet à quelques autres endroits et produit généralement une exception "Impossible de convertir le type X en type X"). Je l'ai résolu dans le TypeDescriptionProvideren comparant les noms de type (FullName) au lieu de comparer les types (voir ci-dessous).

Le deuxième problème. Je ne sais pas vraiment pourquoi les contrôles du formulaire de base ne sont pas concevables dans la classe Form1 et leurs positions sont perdues après le redimensionnement, mais je l'ai contourné (ce n'est pas une bonne solution - si vous en savez mieux, veuillez écrire). Je déplace simplement manuellement les boutons de BaseForm (qui devraient être dans le coin inférieur droit) à leurs positions correctes dans une méthode appelée de manière asynchrone à partir de l'événement Load de BaseForm: BeginInvoke(new Action(CorrectLayout));Ma classe de base n'a que les boutons "OK" et "Annuler", donc le le cas est simple.

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
{
}

Et ici vous avez la version légèrement modifiée de 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);
    }
}

Et c'est tout!

Vous n'avez rien à expliquer aux futurs développeurs de formulaires basés sur votre BaseForm et ils n'ont pas à faire d'astuces pour concevoir leurs formulaires! Je pense que c'est la solution la plus propre possible (sauf pour le repositionnement des commandes).

Encore un conseil:

Si, pour une raison quelconque, le concepteur refuse toujours de travailler pour vous, vous pouvez toujours faire la simple astuce de changer le public class Form1 : BaseFormto public class Form1 : BaseFormMiddle1(ou BaseFormMiddle2) dans le fichier de code, de le modifier dans le concepteur de formulaire VS, puis de le modifier à nouveau. Je préfère cette astuce à la compilation conditionnelle car il est moins probable d'oublier et de publier la mauvaise version .


1
Cela a résolu le problème que j'avais avec la solution de Juan dans VS 2013; au redémarrage de VS, les commandes se chargent de manière cohérente maintenant.
Luke Merrett

3

J'ai une astuce pour la solution Juan Carlos Diaz. Cela fonctionne très bien pour moi, mais cela pose un problème. Quand je lance VS et entre dans le concepteur, tout fonctionne bien. Mais après avoir exécuté la solution, arrêtez-la et quittez-la, puis essayez d'entrer dans le concepteur, l'exception apparaît encore et encore jusqu'au redémarrage de VS. Mais j'ai trouvé la solution - tout à faire est d'ajouter ci-dessous à votre app.config

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

2

Puisque la classe abstraite public abstract class BaseForm: Formdonne une erreur et évite l'utilisation du concepteur, je suis venu avec l'utilisation de membres virtuels. Fondamentalement, au lieu de déclarer des méthodes abstraites, j'ai déclaré des méthodes virtuelles avec le moins de corps possible. Voici ce que j'ai fait:

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. */

Puisqu'il DataFormétait censé être une classe abstraite avec le membre abstrait displayFields, je "simule" ce comportement avec des membres virtuels pour éviter l'abstraction. Le designer ne se plaint plus et tout fonctionne bien pour moi.

C'est une solution de contournement plus lisible, mais comme elle n'est pas abstraite, je dois m'assurer que toutes les classes enfants de DataFormont leur implémentation de displayFields. Soyez donc prudent lorsque vous utilisez cette technique.


C'est ce avec quoi je suis allé. Je viens de lancer NotImplementedException dans la classe de base pour rendre l'erreur évidente si elle est oubliée.
Shaun Rowan

1

Le Concepteur Windows Forms crée une instance de la classe de base de votre formulaire / contrôle et applique le résultat d'analyse de InitializeComponent. C'est pourquoi vous pouvez concevoir le formulaire créé par l'assistant de projet sans même créer le projet. En raison de ce comportement, vous ne pouvez pas non plus concevoir un contrôle dérivé d'une classe abstraite.

Vous pouvez implémenter ces méthodes abstraites et lever une exception lorsqu'elle ne s'exécute pas dans le concepteur. Le programmeur qui dérive du contrôle doit fournir une implémentation qui n'appelle pas votre implémentation de classe de base. Sinon, le programme planterait.


dommage, mais c'est comme ça que ça se fait encore. J'espérais une façon correcte de le faire.
Oliver Friedrich

Il y a un meilleur moyen, voir la réponse de Smelch
Allen Rice

-1

Vous pouvez simplement compiler conditionnellement le abstractmot - clé sans interposer une classe distincte:

#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
  }

Cela fonctionne à condition de BaseFormne pas avoir de méthodes abstraites (le abstractmot - clé empêche donc uniquement l'instanciation d'exécution de la classe).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.