Melhor design para formulários do Windows que compartilham funcionalidades comuns

20

No passado, eu usava herança para permitir a extensão de formulários do Windows no meu aplicativo. Se todos os meus formulários tivessem controles, arte-final e funcionalidade comuns, eu criaria um formulário básico implementando os controles e a funcionalidade comuns e permitiria que outros controles fossem herdados desse formulário base. No entanto, eu tive alguns problemas com esse design.

  1. Os controles podem estar apenas em um contêiner por vez, portanto, qualquer controle estático que você tiver será complicado. Por exemplo: Suponha que você tenha um formulário base chamado BaseForm que contenha um TreeView que você torne protegido e estático, para que todas as outras instâncias (derivadas) dessa classe possam modificar e exibir o mesmo TreeView. Isso não funcionaria para várias classes herdadas do BaseForm, porque esse TreeView pode estar apenas em um contêiner por vez. Provavelmente estaria no último formulário inicializado. Embora cada instância pudesse editar o controle, ele seria exibido apenas em um de cada vez. Claro, existem soluções alternativas, mas todas são feias. (Este parece ser um projeto muito ruim para mim. Por que vários contêineres não podem armazenar ponteiros para o mesmo objeto? De qualquer forma, é o que é.)

  2. Estado entre formulários, ou seja, estados de botão, texto da etiqueta etc., eu tenho que usar variáveis ​​globais e redefinir os estados no Load.

  3. Isso realmente não é muito bem suportado pelo designer do Visual Studio.

Existe um design melhor, mas ainda assim, de fácil manutenção para usar? Ou a herança de forma ainda é a melhor abordagem?

Atualização Passei de MVC para MVP, para o Observer Pattern e o Event Pattern. Aqui está o que estou pensando no momento, por favor, critique:

Minha classe BaseForm conterá apenas os controles e eventos conectados a esses controles. Todos os eventos que precisam de qualquer tipo de lógica para lidar com eles serão transmitidos imediatamente para a classe BaseFormPresenter. Essa classe manipulará os dados da interface do usuário, executará qualquer operação lógica e atualizará o BaseFormModel. O Modelo exporá eventos, que serão acionados com alterações de estado, à classe Presenter, na qual ele se inscreverá (ou observará). Quando o Apresentador recebe a notificação do evento, ele executa qualquer lógica e, em seguida, o Apresentador modifica a Visualização de acordo.

Haverá apenas uma de cada classe Model na memória, mas pode haver muitas instâncias do BaseForm e, portanto, do BaseFormPresenter. Isso resolveria meu problema de sincronizar cada instância do BaseForm com o mesmo modelo de dados.

Questões:

Qual camada deve armazenar itens como o último botão pressionado, para que eu possa mantê-lo destacado para o usuário (como em um menu CSS) entre os formulários?

Critique por favor este design. Obrigado pela ajuda!

Jonathan Henson
fonte
Não vejo por que você é forçado a usar variáveis ​​globais, mas se sim, então certamente deve haver uma abordagem melhor. Talvez fábricas para criar componentes comuns combinados com composição em vez de herança?
Stijn
Todo o seu design é falho. Se você já entende o que deseja fazer, não é suportado pelo Visual Studio, o que deve lhe dizer uma coisa.
Ramhound 12/12
1
@ Ramhound É suportado pelo visual studio, mas não muito bem. É assim que a Microsoft diz para você fazer isso. Apenas acho que é uma dor no A. msdn.microsoft.com/en-us/library/aa983613(v=vs.71).aspx De qualquer forma, se você tem uma idéia melhor, eu sou todo olho.
Jonathan Henson
@stijn Suponho que eu poderia ter um método em cada formulário base que carregará os estados de controle em outra instância. ou seja, LoadStatesToNewInstance (instância BaseForm). Eu poderia chamá-lo quando quisesse mostrar uma nova forma desse tipo de base. form_I_am_about_to_hide.LoadStatesToNewInstance (this);
Jonathan Henson
Eu não sou desenvolvedor de .net, você pode expandir o ponto número 1? Eu poderia ter uma solução
Imran Omar Bukhsh

Respostas:

6
  1. Não sei por que você precisa de controles estáticos. Talvez você saiba algo que eu não. Eu usei muita herança visual, mas nunca vi controles estáticos serem necessários. Se você tiver um controle de exibição em árvore comum, permita que cada instância do formulário tenha sua própria instância do controle e compartilhe uma única instância dos dados vinculados às exibições em árvore.

  2. Compartilhar estado de controle (em oposição a dados) entre formulários também é um requisito incomum. Tem certeza de que o FormB realmente precisa saber sobre o estado dos botões no FormA? Considere os projetos MVP ou MVC. Pense em cada forma como uma "visão" idiota que não sabe nada sobre as outras visualizações ou mesmo sobre o próprio aplicativo. Supervisione cada visualização com um apresentador / controlador inteligente. Se fizer sentido, um único apresentador pode supervisionar várias visualizações. Associe um objeto de estado a cada visualização. Se você tem algum estado que precisa ser compartilhado entre as visualizações, deixe o (s) apresentador (es) mediar isso (e considere a ligação de dados - veja abaixo).

  3. Concordado, o Visual Studio lhe dará dores de cabeça. Ao considerar a forma ou a herança de controle do usuário, você deve avaliar cuidadosamente os benefícios em relação ao custo potencial (e provável) da luta com as peculiaridades e limitações frustrantes do criador de formulários. Sugiro manter a herança de formulários no mínimo - use-a apenas quando o pagamento for alto. Lembre-se de que, como alternativa à subclasse, você pode criar um formulário "base" comum e simplesmente instancia-lo uma vez para cada "filho" em potencial e personalizá-lo imediatamente. Isso faz sentido quando as diferenças entre cada versão do formulário são menores em comparação com os aspectos compartilhados. (IOW: formulário básico complexo, formulários filho apenas pouco mais complexos)

Faça uso dos controles do usuário quando isso ajudar a impedir a duplicação significativa do desenvolvimento da interface do usuário. Considere a herança de controle do usuário, mas aplique as mesmas considerações da herança de formulário.

Acho que o conselho mais importante que posso oferecer é que, se você atualmente não emprega alguma forma do padrão de exibição / controlador, recomendo fortemente que você comece a fazê-lo. Obriga você a aprender e apreciar os benefícios do acoplamento solto e da separação de camadas.

resposta à sua atualização

Qual camada deve armazenar coisas como o último botão pressionado, para que eu possa mantê-la destacada para o usuário ...

Você pode compartilhar o estado entre as exibições da mesma forma que compartilharia o estado entre um apresentador e sua visualização. Crie uma classe especial SharedViewState. Para simplificar, você pode torná-lo um singleton ou instancia-lo no apresentador principal e passá-lo para todas as visualizações (por meio de seus apresentadores) a partir daí. Quando o estado estiver associado aos controles, use a ligação de dados sempre que possível. A maioria das Controlpropriedades pode ser vinculada a dados. Por exemplo, a propriedade BackColor de um Button pode ser vinculada a uma propriedade da sua classe SharedViewState. Se você fizer essa ligação em todos os formulários com botões idênticos, poderá realçar o Botão1 em todos os formulários apenas configurando SharedViewState.Button1BackColor = someColor.

Se você não estiver familiarizado com a ligação de dados do WinForms, pressione o MSDN e faça algumas leituras. Não é difícil. Aprenda INotifyPropertyChangede você está no meio do caminho.

Aqui está uma implementação típica de uma classe viewstate com a propriedade Button1BackColor como um exemplo:

public class SharedViewState : INotifyPropertyChanged
{
    // boilerplate INotifyPropertyChanged stuff
    public event PropertyChangedEventHandler PropertyChanged;
    protected void NotifyPropertyChanged(string info)
    {
        if (PropertyChanged != null)
        {
            PropertyChanged(this, new PropertyChangedEventArgs(info));
        }
    }

    // example of a property for data-binding
    private Color button1BackColor;
    public Color Button1BackColor
    {
        get { return button1BackColor; }
        set
        {
            if (value != button1BackColor)
            {
                button1BackColor = value;
                NotifyPropertyChanged("Button1BackColor");
            }
        }
    }
}
Igby Largeman
fonte
Obrigado pela sua resposta atenciosa. Você conhece uma boa referência ao padrão de exibição / controlador?
Jonathan Henson
1
Lembro-me de pensar que essa foi uma boa introdução: codebetter.com/jeremymiller/2007/05/22/…
Igby Largeman
por favor, veja minha atualização.
Jonathan Henson
@ JonathanHenson: resposta atualizada.
Igby Largeman
5

Eu mantenho um novo aplicativo WinForms / WPF com base no padrão MVVM e nos Controles de Atualização . Começou como WinForms, então criei uma versão WPF devido à importância de marketing de uma interface do usuário com boa aparência. Eu pensei que seria uma restrição de design interessante manter um aplicativo que suporta duas tecnologias de interface do usuário completamente diferentes com o mesmo código de back-end (modelos e modelos de exibição), e devo dizer que estou bastante satisfeito com essa abordagem.

No meu aplicativo, sempre que preciso compartilhar a funcionalidade entre partes da interface do usuário, uso controles personalizados ou controles de usuário (classes derivadas do WPF UserControl ou WinForms UserControl). Na versão WinForms, há um UserControl dentro de outro UserControl dentro de uma classe derivada do TabPage, que finalmente está dentro de um controle de tabulação no formulário principal. A versão do WPF, na verdade, possui UserControls aninhados um nível mais profundo. Usando controles personalizados, você pode facilmente compor uma nova interface de usuário dos UserControls que você criou anteriormente.

Como uso o padrão MVVM, coloquei o máximo de lógica de programa possível no ViewModel (incluindo o Presentation / Navigation Model ) ou no Model (dependendo se o código está relacionado à interface do usuário ou não), mas desde o mesmo ViewModel é usado pelas telas WinForms e WPF, não é permitido que o ViewModel contenha código projetado para WinForms ou WPF ou que interaja diretamente com a interface do usuário; esse código DEVE entrar em exibição.

Também no padrão MVVM, os objetos da interface do usuário devem evitar interagir uns com os outros! Em vez disso, eles interagem com os modelos de exibição. Por exemplo, um TextBox não perguntaria a um ListBox próximo qual item está selecionado; em vez disso, a caixa de listagem salvaria uma referência ao item atualmente selecionado em algum lugar da camada de modelo de vista, e o TextBox consultaria a camada de modelo de vista para descobrir o que está selecionado no momento. Isso resolve o problema de descobrir qual botão foi pressionado em um formulário de um formulário diferente. Você acabou de compartilhar um objeto Modelo de Navegação (que faz parte da camada de modelo de exibição do aplicativo) entre os dois formulários e coloca uma propriedade nesse objeto que representa qual botão foi pressionado.

O WinForms em si não suporta muito bem o padrão MVVM, mas o Update Controls fornece sua própria abordagem única, o MVVM, como uma biblioteca que fica sobre o WinForms.

Na minha opinião, essa abordagem para o design do programa funciona muito bem e pretendo usá-la em projetos futuros. Os motivos pelos quais funciona tão bem são (1) que o Update Controls gerencia dependências automaticamente e (2) é geralmente muito claro como você deve estruturar seu código: todo o código que interage com os objetos da interface do usuário pertence à View, todo o código que é Relacionado à interface do usuário, mas não precisa interagir com objetos da interface do usuário, pertence ao ViewModel. Muitas vezes, você dividirá seu código em duas partes, uma parte para o View e outra parte para o ViewModel. Tive alguma dificuldade em projetar menus de contexto no meu sistema, mas eventualmente criei um design para isso também.

Também tenho elogios sobre os controles de atualização no meu blog . Essa abordagem leva muito tempo para se acostumar, no entanto, e em aplicativos de grande escala (por exemplo, se suas caixas de listagem contêm milhares de itens), você pode encontrar problemas de desempenho devido às limitações atuais do gerenciamento automático de dependências.

Qwertie
fonte
3

Vou responder a isso mesmo que você já tenha aceitado uma resposta.

Como outros já apontaram, não entendo por que você teve que usar algo estático; parece que você está fazendo algo muito errado.

Enfim, tive o mesmo problema que você: no meu aplicativo WinForms, tenho vários formulários que compartilham algumas funcionalidades e alguns controles. Além disso, todos os meus formulários já derivam de um formulário base (vamos chamá-lo "MyForm"), que adiciona funcionalidade no nível da estrutura (independentemente do aplicativo). O Forms Designer do Visual Studio supostamente suporta a edição de formulários herdados de outros formulários, mas em pratique que funciona apenas enquanto seus formulários não fizerem nada além de "Olá, mundo! - OK - Cancelar".

O que acabei fazendo é o seguinte: mantenho minha classe base comum "MyForm", que é bastante complexa, e continuo derivando todos os formulários do meu aplicativo. No entanto, não faço absolutamente nada nesses formulários, portanto, o VS Forms Designer não tem problemas para editá-los. Esses formulários consistem apenas no código que o Designer de Formulários gera para eles. Em seguida, tenho uma hierarquia paralela separada de objetos que chamo de "Substitutos", que contêm toda a funcionalidade específica do aplicativo, como inicializar os controles, manipular eventos gerados pelo formulário e pelos controles, etc. correspondência entre classes substitutas e caixas de diálogo no meu aplicativo: existe uma classe substituta base que corresponde a "MyForm" e, em seguida, é derivada outra classe substituta que corresponde a "MyApplicationForm",

Cada substituto aceita como parâmetro de tempo de construção um tipo específico de formulário e se anexa a ele registrando-se para seus eventos. Ele também delega para a base, até o "MySurrogate", que aceita um "MyForm". Esse substituto se registra com o evento "Disposed" do formulário, de modo que, quando o formulário é destruído, o substituto invoca uma substituição em si mesmo, para que ele e todos os seus descendentes possam executar a limpeza. (Cancelar registro de eventos etc.)

Tem funcionado bem até agora.

Mike Nakis
fonte