Como posso obter o designer do Visual Studio 2008 Windows Forms para renderizar um Form que implementa uma classe base abstrata?

98

Tive um problema com controles herdados no Windows Forms e preciso de alguns conselhos sobre isso.

Eu uso uma classe base para itens em uma Lista (lista de GUI feita por mim mesma feita de um painel) e alguns controles herdados que são para cada tipo de dados que podem ser adicionados à lista.

Não havia nenhum problema com isso, mas agora descobri que seria correto fazer do controle base uma classe abstrata, uma vez que possui métodos, que precisam ser implementados em todos os controles herdados, chamados a partir do código dentro do base-control, mas não deve e não pode ser implementado na classe base.

Quando eu marco o controle base como abstrato, o Visual Studio 2008 Designer se recusa a carregar a janela.

Existe uma maneira de fazer o Designer trabalhar com o controle base tornado abstrato?

Oliver Friedrich
fonte

Respostas:

97

EU SABIA que deveria haver uma maneira de fazer isso (e encontrei uma maneira de fazer isso de forma limpa). A solução de Sheng é exatamente o que eu inventei como uma solução temporária, mas depois que um amigo apontou que a Formclasse acabou herdando de umabstract classe, DEVEMOS ser capazes de fazer isso. Se eles podem fazer isso, nós podemos fazer.

Partimos deste código para o problema

Form1 : Form

Problema

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

É aqui que a questão inicial entrou em jogo. Como disse antes, um amigo apontou que System.Windows.Forms.Formimplementa uma classe base que é abstrata. Conseguimos encontrar ...

Prova de uma solução melhor

A partir disso, sabíamos que era possível para o designer mostrar uma classe que implementasse uma classe abstrata de base, apenas não poderia mostrar uma classe de designer que implementasse imediatamente uma classe abstrata de base. Devia haver no máximo 5 intermediários, mas testamos 1 camada de abstração e inicialmente encontramos essa solução.

Solução Inicial

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

Isso realmente funciona e o designer processa bem, problema resolvido ... exceto que você tem um nível extra de herança em seu aplicativo de produção que só foi necessário por causa de uma inadequação no designer de winforms!

Esta não é uma solução 100% infalível, mas é muito boa. Basicamente, você usa #if DEBUGpara encontrar a solução refinada.

Solução 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
... 

O que isso faz é usar apenas a solução descrita na "solução inicial", se estiver no modo de depuração. A ideia é que você nunca liberará o modo de produção por meio de uma compilação de depuração e sempre projetará no modo de depuração.

O designer sempre será executado no código criado no modo atual, portanto, você não pode usar o designer no modo de liberação. No entanto, contanto que você projete no modo de depuração e libere o código integrado no modo de liberação, você está pronto para prosseguir.

A única solução infalível seria se você pudesse testar o modo de design por meio de uma diretiva de pré-processador.

fundir
fonte
3
O seu formulário e a classe base abstrata têm um construtor sem arg? Porque isso é tudo que tivemos que adicionar para fazer o designer trabalhar para mostrar um formulário que herdou de um formulário abstrato.
nos
Funcionou muito bem! Acho que vou apenas fazer as modificações necessárias nas várias classes que implementam a classe abstrata, depois remover a classe média temporária novamente e, se precisar fazer mais modificações posteriormente, posso adicioná-la de volta. A solução alternativa realmente funcionou. Obrigado!
neminem
1
Sua solução funciona muito bem. Eu simplesmente não posso acreditar que o Visual Studio exige que você supere tantos obstáculos para fazer algo tão comum.
RB Davidson
1
Mas se eu usar uma middleClass que não é uma classe abstrata, então quem quer que herde a middleClass não precisa mais implementar o método abstrato, isso anula o próprio propósito de usar uma classe abstrata em primeiro lugar ... Como resolver isso?
Darius
1
@ ti034 Não consegui encontrar nenhuma solução alternativa. Então, acabei de fazer as funções supostamente abstratas de middleClass terem alguns valores padrão que podem facilmente me lembrar de sobrescrevê-los, sem que o compilador gere erros. Por exemplo, se o método supostamente abstrato é retornar o título da página, então farei com que ele retorne uma string "Por favor, altere o título".
Darius de
74

@smelch, Existe uma solução melhor, sem ter que criar um middle control, mesmo para debug.

O que nós queremos

Primeiro, vamos definir a classe final e a classe abstrata base.

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

Agora, tudo o que precisamos é um provedor de descrição .

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

Por fim, apenas aplicamos um atributo TypeDescriptionProvider ao controle Abastract.

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

E é isso. Nenhum controle do meio necessário.

E a classe do provedor pode ser aplicada a quantas bases abstratas quisermos na mesma solução.

* EDIT * Além disso, o seguinte é necessário no app.config

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

Obrigado @ user3057544 pela sugestão.


jucardi
fonte
1
Também funcionou para mim que estou usando CF 3.5, não háTypeDescriptionProvider
Adrian Botor
4
Não foi possível fazer isso funcionar no VS 2010, embora o smelch funcionou. Alguém sabe por quê?
RobC
5
O @RobC Designer está meio mal-humorado por algum motivo. Descobri que, depois de implementar essa correção, precisava limpar a solução, fechar e reiniciar o VS2010 e reconstruir; então me permitiria projetar a subclasse.
Oblivious Sage
3
É importante notar que, como essa correção substitui uma instância da classe base pela classe abstrata, os elementos visuais adicionados no Designer para a classe abstrata não estarão disponíveis ao projetar as subclasses.
Oblivious Sage
1
Isso funcionou para mim, mas primeiro tive que reiniciar o VS 2013 depois de construir o projeto. @ObliviousSage - Obrigado pelo aviso; no meu caso atual, pelo menos, isso não é um problema, mas ainda assim é um bom problema a ser observado.
InteXX
10

@Smelch, obrigado pela resposta útil, pois estava tendo o mesmo problema recentemente.

A seguir está uma pequena alteração em sua postagem para evitar avisos de compilação (colocando a classe base dentro da #if DEBUGdiretiva do pré-processador):

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

Tive um problema semelhante, mas encontrei uma maneira de refatorar as coisas para usar uma interface no lugar de uma classe base abstrata:

interface Base {....}

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

Isso pode não ser aplicável a todas as situações, mas, quando possível, resulta em uma solução mais limpa do que a compilação condicional.

Jan Hettich
fonte
1
Você poderia fornecer um exemplo de código um pouco mais completo? Estou tentando entender melhor o seu design e também irei traduzi-lo para o VB. Obrigado.
InteXX
Eu sei que isso é antigo, mas descobri que essa era a solução menos hacky. Como ainda queria minha interface vinculada ao UserControl, adicionei uma UserControlpropriedade à interface e a referenciei sempre que precisei acessá-la diretamente. Em minhas implementações de interface, eu estendo UserControl e defino a UserControlpropriedade parathis
chanban
3

Estou usando a solução desta resposta para outra pergunta, que vincula este artigo . O artigo recomenda o uso de uma TypeDescriptionProviderimplementação personalizada e concreta da classe abstrata. O designer irá perguntar ao provedor customizado quais tipos usar, e seu código pode retornar a classe concreta para que o designer fique feliz enquanto você tem controle total sobre como a classe abstrata aparece como uma classe concreta.

Atualização: incluí um exemplo de código documentado em minha resposta a essa outra pergunta. O código ali funciona, mas às vezes tenho que passar por um ciclo de limpeza / construção, conforme observado em minha resposta, para fazê-lo funcionar.

Carl G
fonte
3

Tenho algumas dicas para quem diz que o TypeDescriptionProviderby Juan Carlos Diaz não está funcionando e também não gosta da compilação condicional:

Em primeiro lugar, talvez você precise reiniciar o Visual Studio para que as alterações em seu código funcionem no designer de formulário (eu precisava, a reconstrução simples não funcionava - ou nem sempre).

Apresentarei minha solução deste problema para o caso da Forma de base abstrata. Digamos que você tenha uma BaseFormclasse e deseja que todos os formulários baseados nela sejam projetáveis ​​(será Form1). O TypeDescriptionProviderapresentado por Juan Carlos Diaz não funcionou para mim também. Aqui está como eu fiz funcionar, juntando-o à solução MiddleClass (por smelch), mas sem a#if DEBUG compilação condicional e com algumas correções:

[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 o atributo na classe BaseForm. Depois, basta declarar as classes médiasTypeDescriptionProvider e duas , mas não se preocupe, elas são invisíveis e irrelevantes para o desenvolvedor do Form1 . O primeiro implementa os membros abstratos (e torna a classe base não abstrata). O segundo está vazio - é apenas necessário para o designer de formulário do VS funcionar. Em seguida, você atribui a segunda classe média ao TypeDescriptionProviderde BaseForm. Sem compilação condicional.

Eu estava tendo mais dois problemas:

  • Problema 1: Após alterar o Form1 no designer (ou algum código) ele estava dando o erro novamente (ao tentar abri-lo no designer novamente).
  • Problema 2: os controles do BaseForm foram colocados incorretamente quando o tamanho do Form1 foi alterado no designer e o formulário foi fechado e reaberto novamente no designer do formulário.

O primeiro problema (você pode não ter porque é algo que me assombra em meu projeto em alguns outros lugares e geralmente produz uma exceção "Não é possível converter o tipo X para o tipo X"). Eu resolvi no TypeDescriptionProviderpor comparar os nomes de tipo (FullName) em vez de comparar os tipos (veja abaixo).

O segundo problema. Eu realmente não sei por que os controles do formulário base não podem ser projetados na classe Form1 e suas posições são perdidas após o redimensionamento, mas eu trabalhei em torno (não é uma boa solução - se você souber de algo melhor, por favor escreva). Eu apenas movo manualmente os botões do BaseForm (que devem estar no canto inferior direito) para suas posições corretas em um método invocado assincronamente do evento Load do BaseForm: BeginInvoke(new Action(CorrectLayout));Minha classe base tem apenas os botões "OK" e "Cancelar", então o caso é simples.

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

E aqui você tem a versão ligeiramente 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);
    }
}

E é isso!

Você não precisa explicar nada aos futuros desenvolvedores de formulários baseados em seu BaseForm e eles não precisam fazer nenhum truque para projetar seus formulários! Acho que é a solução mais limpa que pode ser (exceto pelo reposicionamento dos controles).

Mais uma dica:

Se por algum motivo o designer ainda se recusar a trabalhar para você, você sempre pode fazer o truque simples de alterar o public class Form1 : BaseFormpara public class Form1 : BaseFormMiddle1(ou BaseFormMiddle2) no arquivo de código, editá-lo no designer de formulário VS e alterá-lo novamente. Prefiro esse truque em vez da compilação condicional porque é menos provável que esqueça e libere a versão errada .

PW
fonte
1
Isso resolveu o problema que eu estava tendo com a solução de Juan no VS 2013; ao reiniciar o VS os controles carregam consistentemente agora.
Luke Merrett
3

Tenho uma dica para a solução Juan Carlos Diaz. Funciona muito bem para mim, mas houve algum problema com isso. Quando eu inicio o VS e entro no designer, tudo funciona bem. Mas depois de executar a solução, então pare e saia dela e então tente entrar no designer a exceção aparecerá repetidamente até reiniciar o VS. Mas eu encontrei a solução para isso - tudo a fazer é adicionar abaixo ao seu app.config

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

Como a classe abstrata public abstract class BaseForm: Formdá um erro e evita o uso do designer, vim com o uso de membros virtuais. Basicamente, em vez de declarar métodos abstratos, declarei métodos virtuais com o corpo mínimo possível. Aqui está o que fiz:

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

Uma vez que DataFormera para ser uma classe abstrata com o membro abstratodisplayFields , eu "simulo" esse comportamento com membros virtuais para evitar a abstração. O designer não reclama mais e tudo funciona bem para mim.

É uma solução alternativa mais legível, mas como não é abstrata, preciso garantir que todas as classes filhas de DataFormtenham sua implementação de displayFields. Portanto, tome cuidado ao usar essa técnica.

Gabriel L.
fonte
Foi com isso que eu fui. Acabei de lançar NotImplementedException na classe base para tornar o erro óbvio se for esquecido.
Shaun Rowan
1

O Windows Forms Designer está criando uma instância da classe base do seu formulário / controle e aplica o resultado da análise InitializeComponent. É por isso que você pode projetar o formulário criado pelo assistente de projeto, mesmo sem construir o projeto. Devido a esse comportamento, você também não pode criar um controle derivado de uma classe abstrata.

Você pode implementar esses métodos abstratos e lançar uma exceção quando não estiver em execução no designer. O programador que deriva do controle deve fornecer uma implementação que não chame sua implementação de classe base. Caso contrário, o programa travaria.

Sheng Jiang 蒋 晟
fonte
pena, mas é assim que ainda é feito. Esperava encontrar uma maneira correta de fazer isso.
Oliver Friedrich,
Há uma maneira melhor, veja a resposta de Smelch
Allen Rice,
-1

Você poderia apenas compilar condicionalmente a abstractpalavra - chave sem interpor uma classe 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
  }

Isso funciona desde que BaseFormnão tenha nenhum método abstrato (a abstractpalavra-chave, portanto, apenas impede a instanciação em tempo de execução da classe).

Peter Gluck
fonte