¿Cómo puedo hacer que el diseñador de formularios Windows Forms de Visual Studio 2008 represente un formulario que implemente una clase base abstracta?

98

Entré en un problema con los controles heredados en Windows Forms y necesito algunos consejos al respecto.

Utilizo una clase base para los elementos de una Lista (lista de GUI hecha por mí mismo hecha de un panel) y algunos controles heredados que son para cada tipo de datos que podrían agregarse a la lista.

No hubo ningún problema con eso, pero ahora descubrí que sería correcto, hacer del control base una clase abstracta, ya que tiene métodos, que deben implementarse en todos los controles heredados, llamados desde el código dentro del base-control, pero no debe ni puede implementarse en la clase base.

Cuando marco el control base como abstracto, Visual Studio 2008 Designer se niega a cargar la ventana.

¿Hay alguna manera de hacer que el diseñador funcione con el control base hecho abstracto?

Oliver Friedrich
fuente

Respuestas:

97

SABÍA que tenía que haber una manera de hacer esto (y encontré una manera de hacerlo limpiamente). La solución de Sheng es exactamente lo que se me ocurrió como una solución temporal, pero después de que un amigo señaló que la Formclase eventualmente heredada de una abstractclase, DEBEMOS poder hacer esto. Si pueden hacerlo, nosotros podemos hacerlo.

Pasamos de este código al problema.

Form1 : Form

Problema

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

Aquí es donde entró en juego la pregunta inicial. Como se dijo antes, un amigo señaló que System.Windows.Forms.Formimplementa una clase base que es abstracta. Pudimos encontrar ...

Prueba de una mejor solución

A partir de esto, sabíamos que era posible que el diseñador mostrara una clase que implementara una clase abstracta base, simplemente no podía mostrar una clase de diseñador que implementara inmediatamente una clase abstracta base. Tenía que haber un máximo de 5 entremedias, pero probamos 1 capa de abstracción e inicialmente se nos ocurrió esta solución.

Solución inicial

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

Esto realmente funciona y el diseñador lo muestra bien, problema resuelto ... ¡excepto que tiene un nivel adicional de herencia en su aplicación de producción que solo era necesario debido a una deficiencia en el diseñador de winforms!

Esta no es una solución 100% infalible, pero es bastante buena. Básicamente, usas #if DEBUGpara llegar a la solución refinada.

Solución refinada

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

Lo que hace esto es usar solo la solución descrita en "solución inicial", si está en modo de depuración. La idea es que nunca lanzará el modo de producción a través de una compilación de depuración y que siempre diseñará en modo de depuración.

El diseñador siempre se ejecutará con el código creado en el modo actual, por lo que no puede usar el diseñador en el modo de lanzamiento. Sin embargo, siempre que diseñe en modo de depuración y publique el código integrado en el modo de lanzamiento, está listo para comenzar.

La única solución infalible sería si puede probar el modo de diseño a través de una directiva de preprocesador.

oler
fuente
3
¿Tu formulario y la clase base abstracta tienen un constructor sin argumentos? Porque eso es todo lo que tuvimos que agregar para que el diseñador trabajara y mostrara una forma heredada de una forma abstracta.
nos
¡Funcionó muy bien! Supongo que haré las modificaciones necesarias en las diversas clases que implementan la abstracta, luego eliminaré la clase media temporal nuevamente, y si alguna vez necesito hacer más modificaciones más adelante, puedo volver a agregarla. La solución, de hecho, funcionó. ¡Gracias!
neminem
1
Tu solución funciona muy bien. No puedo creer que Visual Studio requiera que pases por esos obstáculos para hacer algo tan común.
RB Davidson
1
Pero si utilizo una clase media que no es una clase abstracta, entonces quien hereda la clase media ya no tiene que implementar el método abstracto, esto frustra el propósito mismo de usar la clase abstracta en primer lugar ... ¿Cómo resolver esto?
Darius
1
@ ti034 No pude encontrar ninguna solución. Así que solo hago que las funciones supuestamente abstractas de la clase media tengan algunos valores predeterminados que pueden recordarme fácilmente que las anule, sin que el compilador arroje un error. Por ejemplo, si el método supuestamente abstracto es devolver el título de la página, haré que devuelva una cadena "Por favor, cambie el título".
Darius
74

@smelch, existe una solución mejor, sin tener que crear un control medio, incluso para depuración.

Lo que nosotros queremos

Primero, definamos la clase final y la clase abstracta base.

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

Ahora todo lo que necesitamos es un proveedor de descripciones .

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

Finalmente, solo aplicamos un atributo TypeDescriptionProvider al control Abastract.

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

Y eso es. No se requiere control intermedio.

Y la clase de proveedor se puede aplicar a tantas bases abstractas como queramos en la misma solución.

* EDITAR * También se necesita lo siguiente en la app.config

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

Gracias @ user3057544 por la sugerencia.


jucardi
fuente
1
Esto también funcionó para mí que estoy usando CF 3.5 no hayTypeDescriptionProvider
Adrian Botor
4
No se pudo hacer que esto funcionara en VS 2010, aunque Smelch sí funcionó. ¿Alguien sabe por qué?
RobC
5
@RobC Designer es un poco gruñón por alguna razón. Descubrí que después de implementar esta solución tuve que limpiar la solución, cerrar y volver a iniciar VS2010 y reconstruir; entonces me dejaría diseñar la subclase.
Oblivious Sage
3
Vale la pena señalar que debido a que esta solución sustituye una instancia de la clase base por la clase abstracta, los elementos visuales agregados en el Diseñador para la clase abstracta no estarán disponibles al diseñar las subclases.
Oblivious Sage
1
Esto funcionó para mí, pero primero tuve que reiniciar VS 2013 después de construir el proyecto. @ObliviousSage - Gracias por el aviso; en mi caso actual, al menos, esto no es un problema, pero aún así es bueno tener cuidado.
InteXX
10

@Smelch, gracias por la útil respuesta, ya que me encontré con el mismo problema recientemente.

A continuación se muestra un cambio menor en su publicación para evitar advertencias de compilación (colocando la clase base dentro de la #if DEBUGdirectiva del preprocesador):

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

Tuve un problema similar pero encontré una manera de refactorizar las cosas para usar una interfaz en lugar de una clase base abstracta:

interface Base {....}

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

Esto puede no ser aplicable a todas las situaciones, pero cuando es posible da como resultado una solución más limpia que la compilación condicional.

Jan Hettich
fuente
1
¿Podría proporcionar una muestra de código un poco más completa? Estoy tratando de entender mejor su diseño y también lo traduciré a VB. Gracias.
InteXX
Sé que esto es antiguo, pero descubrí que esta era la solución menos hacky. Como todavía quería que mi interfaz estuviera vinculada al UserControl, agregué una UserControlpropiedad a la interfaz y la mencioné cada vez que necesitaba acceder directamente. En mis implementaciones de interfaz, extiendo UserControl y configuro la UserControlpropiedad enthis
chanban
3

Estoy usando la solución en esta respuesta a otra pregunta, que vincula este artículo . El artículo recomienda usar un personalizadoTypeDescriptionProvider implementación y concreta de la clase abstracta. El diseñador le preguntará al proveedor personalizado qué tipos usar, y su código puede devolver la clase concreta para que el diseñador esté contento mientras usted tiene control total sobre cómo aparece la clase abstracta como una clase concreta.

Actualización: incluí un ejemplo de código documentado en mi respuesta a esa otra pregunta. El código allí funciona, pero a veces tengo que pasar por un ciclo de limpieza / compilación como se indica en mi respuesta para que funcione.

Carl G
fuente
3

Tengo algunos consejos para las personas que dicen que el TypeDescriptionProviderde Juan Carlos Díaz no funciona y tampoco les gusta la compilación condicional:

En primer lugar, es posible que deba reiniciar Visual Studio para que los cambios en su código funcionen en el diseñador de formularios (tuve que hacerlo, la reconstrucción simple no funcionó, o no siempre).

Presentaré mi solución a este problema para el caso de la forma base abstracta. Supongamos que tiene una BaseFormclase y desea que cualquier formulario basado en ella pueda designarse (así será Form1). El TypeDescriptionProviderpresentado por Juan Carlos Díaz tampoco funcionó para mí. Así es como lo hice funcionar, uniéndolo con la solución MiddleClass (por smelch), pero sin la#if DEBUG compilación condicional y con algunas correcciones:

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

Observe el atributo de la clase BaseForm. A continuación, sólo tiene que declarar los TypeDescriptionProvidery las dos clases medias , pero no se preocupe, que son invisibles e irrelevantes para el desarrollador de Form1 . El primero implementa los miembros abstractos (y hace que la clase base no sea abstracta). El segundo está vacío; solo es necesario para que funcione el diseñador de formularios VS. Luego asigna la segunda clase media a la TypeDescriptionProviderde BaseForm. Sin compilación condicional.

Tenía dos problemas más:

  • Problema 1: después de cambiar Form1 en el diseñador (o en algún código), estaba dando el error nuevamente (al intentar abrirlo en el diseñador nuevamente).
  • Problema 2: los controles de BaseForm se colocaron incorrectamente cuando se cambió el tamaño de Form1 en el diseñador y el formulario se cerró y se volvió a abrir en el diseñador de formularios.

El primer problema (es posible que no lo tenga porque es algo que me persigue en mi proyecto en algunos otros lugares y generalmente produce una excepción "No se puede convertir el tipo X en el tipo X"). Lo resuelto en el TypeDescriptionProviderde la comparación de los nombres de los tipos (FullName) en lugar de la comparación de los tipos (ver más abajo).

El segundo problema. Realmente no sé por qué los controles del formulario base no se pueden designar en la clase Form1 y sus posiciones se pierden después de cambiar el tamaño, pero lo he solucionado (no es una buena solución; si sabe algo mejor, escriba). Simplemente muevo manualmente los botones de BaseForm (que deberían estar en la esquina inferior derecha) a sus posiciones correctas en un método invocado asincrónicamente desde el evento Load de BaseForm: BeginInvoke(new Action(CorrectLayout));Mi clase base solo tiene los botones "Aceptar" y "Cancelar", por lo que el el caso es 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
{
}

Y aquí tienes la versión ligeramente modificada 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);
    }
}

¡Y eso es!

¡No tiene que explicar nada a los futuros desarrolladores de formularios basados ​​en su BaseForm y ellos no tienen que hacer ningún truco para diseñar sus formularios! Creo que es la solución más limpia que puede ser (excepto por el reposicionamiento de los controles).

Un consejo más:

Si por alguna razón el diseñador aún se niega a trabajar para usted, siempre puede hacer el simple truco de cambiar public class Form1 : BaseForma public class Form1 : BaseFormMiddle1(o BaseFormMiddle2) en el archivo de código, editarlo en el diseñador de formularios VS y luego volver a cambiarlo. Prefiero este truco a la compilación condicional porque es menos probable que olvide y publique la versión incorrecta .

PW
fuente
1
Esto resolvió el problema que estaba teniendo con la solución de Juan en VS 2013; al reiniciar VS, los controles se cargan de manera consistente ahora.
Luke Merrett
3

Tengo un consejo para la solución de Juan Carlos Díaz. Funciona muy bien para mí, pero fue un problema. Cuando inicio VS y entro al diseñador, todo funciona bien. Pero después de ejecutar la solución, deténgala y salga y luego intente ingresar al diseñador, la excepción aparece una y otra vez hasta que reinicia VS. Pero encontré la solución: todo lo que hay que hacer es agregar a continuación a su app.config

  <appSettings>
   <add key="EnableOptimizedDesignerReloading" value="false" />
  </appSettings>
usuario3057544
fuente
2

Dado que la clase abstracta public abstract class BaseForm: Formda un error y evita el uso del diseñador, vine con el uso de miembros virtuales. Básicamente, en lugar de declarar métodos abstractos, declaré métodos virtuales con el cuerpo mínimo posible. Esto es lo que hice:

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

Como DataFormse suponía que era una clase abstracta con el miembro abstracto displayFields, "finjo" este comportamiento con miembros virtuales para evitar la abstracción. El diseñador ya no se queja y todo me funciona bien.

Es una solución alternativa más legible, pero como no es abstracto, tengo que asegurarme de que todas las clases secundarias de DataFormtengan su implementación displayFields. Por lo tanto, tenga cuidado al utilizar esta técnica.

Gabriel L.
fuente
Esto es con lo que fui. Acabo de lanzar NotImplementedException en la clase base para que el error sea obvio si se olvida.
Shaun Rowan
1

El Diseñador de Windows Forms está creando una instancia de la clase base de su formulario / control y aplica el resultado de análisis de InitializeComponent . Es por eso que puede diseñar el formulario creado por el asistente de proyectos sin siquiera construir el proyecto. Debido a este comportamiento, tampoco puede diseñar un control derivado de una clase abstracta.

Puede implementar esos métodos abstractos y lanzar una excepción cuando no se esté ejecutando en el diseñador. El programador que deriva del control debe proporcionar una implementación que no llame a la implementación de su clase base. De lo contrario, el programa fallaría.

Sheng Jiang 蒋 晟
fuente
lástima, pero así es como se hace todavía. Esperaba una forma correcta de hacer esto.
Oliver Friedrich
Hay una mejor manera, vea la respuesta de Smelch
Allen Rice
-1

Podría simplemente compilar condicionalmente en la abstractpalabra clave sin interponer una clase separada:

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

Esto funciona siempre que BaseFormno tenga ningún método abstracto (la abstractpalabra clave, por lo tanto, solo evita la instanciación en tiempo de ejecución de la clase).

Peter Gluck
fuente