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 GUI auto-fabriquée constituée d'un panneau) et certains contrôles hérités qui sont pour chaque type de données qui pourraient être ajoutées à 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, Visual Studio 2008 Designer refuse de charger la fenêtre.
Existe-t-il un moyen de faire fonctionner le concepteur avec le résumé de contrôle de base?
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 trouvé comme solution de contournement temporaire, mais après qu'un ami a souligné que la classe Form
a finalement hérité d'une classe abstract
, nous DEVONS pouvoir le faire. S'ils peuvent le faire, nous pouvons le faire.
Nous sommes passés de ce code au problème
Form1 : Form
public class Form1 : BaseForm
...
public abstract class BaseForm : Form
C'est là que la question initiale est entrée en jeu. Comme dit précédemment, un ami a souligné que System.Windows.Forms.Form
implémente une classe de base qui est abstraite. Nous avons pu trouver ...
Hiérarchie d'héritage:
De cela, 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 montrer une classe de concepteur qui implémentait immédiatement une classe abstraite de base. Il devait y avoir au maximum 5 entre les deux, mais nous avons testé 1 couche d'abstraction et avons initialement proposé cette solution.
public class Form1 : MiddleClass
...
public class MiddleClass : BaseForm
...
public abstract class BaseForm : Form
...
Cela fonctionne réellement et le concepteur le rend très bien, le problème est résolu .... sauf que vous avez un niveau d'héritage supplémentaire 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 infaillible à 100% mais c'est assez bon. Fondamentalement, vous utilisez #if DEBUG
pour trouver la 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 ne fait qu'utiliser la solution décrite dans la "solution initiale", si elle est en mode débogage. L'idée est que vous ne libérerez jamais le mode de production via une version de débogage et que vous créerez toujours en mode de débogage.
Le concepteur s'exécutera toujours par rapport au code créé dans le mode actuel, vous ne pouvez donc pas utiliser le concepteur en mode de publication. Cependant, tant que vous concevez en mode débogage et libérez le code intégré en mode release, vous êtes prêt à partir.
La seule solution infaillible serait de pouvoir tester le mode de conception via une directive de préprocesseur.
@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
Définissons d'abord 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 Description du fournisseur.
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 voulons dans la même solution.
* EDIT * Les éléments suivants sont également nécessaires dans app.config
<appSettings>
<add key="EnableOptimizedDesignerReloading" value="false" />
</appSettings>
Merci @ user3057544 pour la suggestion.
@Smelch, merci pour la réponse utile, car je rencontrais récemment le même problème.
Voici une modification mineure de votre publication pour éviter les avertissements de compilation (en plaçant la classe de base dans le #if DEBUG
directive du préprocesseur):
public class Form1
#if DEBUG
: MiddleClass
#else
: BaseForm
#endif
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.
J'ai une astuce pour la solution de Juan Carlos Diaz. Cela fonctionne très bien pour moi, mais cela a posé problème. Quand je démarre VS et que j'entre dans Designer, 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 ce que vous avez à faire est d'ajouter ci-dessous à votre app.config
<appSettings>
<add key="EnableOptimizedDesignerReloading" value="false" />
</appSettings>
J'utilise la solution dans cette réponse à une autre question, qui relie cet article . L'article recommande d'utiliser une implémentation personnalisée TypeDescriptionProvider
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 n exemple de code documenté dans ma réponse à cette autre question. Le code fonctionne, mais parfois je dois passer par un cycle de nettoyage/génération comme indiqué dans ma réponse pour le faire fonctionner.
J'ai quelques conseils pour ceux qui disent que le TypeDescriptionProvider
de Juan Carlos Diaz ne fonctionne pas et n'aime 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 du formulaire de base abstrait. Supposons que vous ayez une classe BaseForm
et que vous souhaitiez que tout formulaire basé sur celle-ci soit concevable (ce sera Form1
). Le TypeDescriptionProvider
présenté par Juan Carlos Diaz n'a pas fonctionné pour moi aussi. Voici comment je l'ai fait fonctionner, en l'associant à la solution MiddleClass (par smelch), mais sans le #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 de la classe BaseForm. Ensuite, il vous suffit de déclarer les TypeDescriptionProvider
et deux classes moyennes, mais ne vous inquiétez pas, ils sont invisibles et non pertinents 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 requis pour que le concepteur de formulaire VS fonctionne. Ensuite, vous affectez la classe moyenne seconde à la TypeDescriptionProvider
de BaseForm
. Pas de compilation conditionnelle.
J'avais encore deux problèmes:
Le premier problème (vous ne l'avez peut-être pas parce que 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 TypeDescriptionProvider
par en comparant les noms de types (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é (pas une bonne solution - si vous en savez mieux, veuillez écrire). Je viens de déplacer manuellement les boutons de la BaseForm (qui devraient être dans le coin inférieur droit) à leurs positions correctes dans une méthode invoquée de manière asynchrone à partir de l'événement Load de la BaseForm: BeginInvoke(new Action(CorrectLayout));
Ma classe de base n'a que le "OK" et Boutons "Annuler", donc 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 voici 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).
n autre conseil:
Si, pour une raison quelconque, le concepteur refuse toujours de travailler pour vous, vous pouvez toujours faire le tour simple de changer le public class Form1 : BaseForm
En public class Form1 : BaseFormMiddle1
(Ou BaseFormMiddle2
) Dans le fichier de code, en le modifiant dans le concepteur de formulaires VS, puis en le modifiant à nouveau. Je préfère cette astuce à la compilation conditionnelle car c'est moins susceptible d'oublier et de publier la mauvaise version.
Puisque la classe abstraite public abstract class BaseForm: Form
donne 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 corps minimum 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. */
Puisque 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 ce n'est pas abstrait, je dois m'assurer que toutes les classes enfants de DataForm
ont leur implémentation de displayFields
. Soyez donc prudent lorsque vous utilisez cette technique.
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 construire 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 n'est pas en cours d'exécution 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.