Model-View-Presenter em WinForms

90

Estou tentando implementar o método MVP pela primeira vez, usando WinForms.

Estou tentando entender a função de cada camada.

Em meu programa, tenho um botão GUI que, quando clicado, abre uma janela de diálogo aberto.

Portanto, usando o MVP, a GUI lida com o evento de clique do botão e chama presenter.openfile ();

Dentro de presenter.openfile (), isso deve então delegar a abertura desse arquivo para a camada de modelo, ou como não há dados ou lógica para processar, ele deve simplesmente agir sobre a solicitação e abrir a janela openfiledialog?

Atualização: decidi oferecer uma recompensa, pois sinto que preciso de mais ajuda sobre isso, e de preferência sob medida para meus pontos específicos abaixo, para que eu tenha contexto.

Ok, depois de ler sobre MVP, decidi implementar a visão passiva. Efetivamente, terei um monte de controles em um Winform que serão manipulados por um Apresentador e, em seguida, as tarefas delegadas ao (s) Modelo (s). Meus pontos específicos estão abaixo:

  1. Quando o winform carrega, ele deve obter uma visualização em árvore. Estou correto em pensar que a view deve, portanto, chamar um método como: presenter.gettree (), este por sua vez irá delegar ao modelo, que irá obter os dados para a treeview, criá-la e configurá-la, retorná-la ao apresentador, que por sua vez passará para a visão que então simplesmente o atribuirá a, digamos, um painel?

  2. Seria o mesmo para qualquer controle de dados no Winform, já que também tenho um datagridview?

  3. Meu aplicativo possui várias classes de modelo com a mesma montagem. Ele também suporta uma arquitetura de plug-ins com plug-ins que precisam ser carregados na inicialização. A visão simplesmente chamaria um método do apresentador, que por sua vez chamaria um método que carrega os plug-ins e exibe as informações na visão? Qual camada controlaria as referências do plugin. A visualização conteria referências a eles ou ao apresentador?

  4. Estou correto em pensar que a visualização deve lidar com todas as coisas sobre a apresentação, desde a cor do nó da visualização em árvore ao tamanho do datagrid, etc?

Eu acho que eles são minhas principais preocupações e se eu entender como o fluxo deve ser para eles, acho que vou ficar bem.

Darren Young
fonte
Este link lostechies.com/derekgreer/2008/11/23/… explica alguns dos estilos de MVP. Pode ser útil, além da excelente resposta de Johann.
ak3nat0n 01 de

Respostas:

123

Esta é minha humilde opinião sobre MVP e seus problemas específicos.

Em primeiro lugar , qualquer coisa com a qual um usuário possa interagir, ou apenas ser mostrado, é uma visualização . As leis, comportamento e características de tal visão são descritos por uma interface . Essa interface pode ser implementada usando uma IU WinForms, uma IU de console, uma IU da web ou até mesmo nenhuma IU (geralmente ao testar um apresentador) - a implementação concreta simplesmente não importa, contanto que obedeça às leis de sua interface de visualização .

Em segundo lugar , uma visualização é sempre controlada por um apresentador . As leis, comportamento e características de tal apresentador também são descritos por uma interface . Essa interface não tem interesse na implementação de visão concreta, desde que obedeça às leis de sua interface de visão.

Terceiro , como um apresentador controla sua visualização, para minimizar as dependências, não há realmente nenhuma vantagem em ter a visualização sabendo alguma coisa sobre seu apresentador. Existe um contrato acordado entre o apresentador e a visão e isso é declarado pela interface da visão.

As implicações do Terceiro são:

  • O apresentador não tem nenhum método que a visão possa chamar, mas a visão tem eventos que o apresentador pode assinar.
  • O apresentador conhece sua visão. Eu prefiro fazer isso com injeção de construtor no apresentador de concreto.
  • A visualização não tem ideia de qual apresentador a está controlando; apenas nunca será fornecido um apresentador.

Para o seu problema, o código acima pode ser parecido com este em um código um tanto simplificado:

interface IConfigurationView
{
    event EventHandler SelectConfigurationFile;

    void SetConfigurationFile(string fullPath);
    void Show();
}

class ConfigurationView : IConfigurationView
{
    Form form;
    Button selectConfigurationFileButton;
    Label fullPathLabel;

    public event EventHandler SelectConfigurationFile;

    public ConfigurationView()
    {
        // UI initialization.

        this.selectConfigurationFileButton.Click += delegate
        {
            var Handler = this.SelectConfigurationFile;

            if (Handler != null)
            {
                Handler(this, EventArgs.Empty);
            }
        };
    }

    public void SetConfigurationFile(string fullPath)
    {
        this.fullPathLabel.Text = fullPath;
    }

    public void Show()
    {
        this.form.ShowDialog();        
    }
}

interface IConfigurationPresenter
{
    void ShowView();
}

class ConfigurationPresenter : IConfigurationPresenter
{
    Configuration configuration = new Configuration();
    IConfigurationView view;

    public ConfigurationPresenter(IConfigurationView view)
    {
        this.view = view;            
        this.view.SelectConfigurationFile += delegate
        {
            // The ISelectFilePresenter and ISelectFileView behaviors
            // are implicit here, but in a WinForms case, a call to
            // OpenFileDialog wouldn't be too far fetched...
            var selectFilePresenter = Gimme.The<ISelectFilePresenter>();
            selectFilePresenter.ShowView();
            this.configuration.FullPath = selectFilePresenter.FullPath;
            this.view.SetConfigurationFile(this.configuration.FullPath);
        };
    }

    public void ShowView()
    {
        this.view.SetConfigurationFile(this.configuration.FullPath);
        this.view.Show();
    }
}

Além do exposto acima, geralmente tenho uma IViewinterface básica onde guardo a Show()e qualquer visualização do proprietário ou título de visualização da qual minhas visualizações geralmente se beneficiam.

Para suas perguntas:

1. Quando o winform carrega, ele deve obter uma visualização em árvore. Estou correto em pensar que a view deve, portanto, chamar um método como: presenter.gettree (), este por sua vez irá delegar ao modelo, que irá obter os dados para a treeview, criá-la e configurá-la, retorná-la ao apresentador, que por sua vez passará para a visão que então simplesmente o atribuirá a, digamos, um painel?

Eu ligaria IConfigurationView.SetTreeData(...)de IConfigurationPresenter.ShowView(), logo antes da ligação paraIConfigurationView.Show()

2. Isso seria o mesmo para qualquer controle de dados no Winform, já que também tenho um datagridview?

Sim, eu chamaria IConfigurationView.SetTableData(...)por isso. Cabe à visualização formatar os dados fornecidos a ela. O apresentador simplesmente obedece ao contrato da visão de que deseja dados tabulares.

3. Meu aplicativo tem várias classes de modelo com a mesma montagem. Ele também oferece suporte a uma arquitetura de plug-ins com plug-ins que precisam ser carregados na inicialização. A visão simplesmente chamaria um método do apresentador, que por sua vez chamaria um método que carrega os plug-ins e exibe as informações na visão? Qual camada controlaria as referências do plugin. A visualização conteria referências a eles ou ao apresentador?

Se os plug-ins estiverem relacionados a visualizações, as visualizações devem conhecê-los, mas não o apresentador. Se eles tratam de dados e modelo, a visão não deve ter nada a ver com eles.

4. Estou correto em pensar que a visualização deve lidar com tudo sobre a apresentação, desde a cor do nó da visualização em árvore ao tamanho do datagrid, etc?

Sim. Pense nisso como o apresentador fornecendo XML que descreve os dados e a exibição que obtém os dados e aplica uma folha de estilo CSS a eles. Em termos concretos, o apresentador pode chamar IRoadMapView.SetRoadCondition(RoadCondition.Slippery)e a visualização então renderiza a estrada na cor vermelha.

E quanto aos dados para nós clicados?

5. Se, ao clicar nos treenodos, devo passar pelo nó específico para o apresentador e, a partir daí, o apresentador trabalhará quais dados ele precisa e, em seguida, solicitará esses dados ao modelo, antes de apresentá-los de volta à exibição?

Se possível, eu passaria todos os dados necessários para apresentar a árvore em uma visualização de uma só vez. Mas se alguns dados forem muito grandes para serem transmitidos desde o início ou se forem dinâmicos em sua natureza e precisarem do "último instantâneo" do modelo (por meio do apresentador), eu adicionaria algo como event LoadNodeDetailsEventHandler LoadNodeDetailsa interface de visualização, para que o o apresentador pode se inscrever nele, buscar os detalhes do nó LoadNodeDetailsEventArgs.Node(possivelmente por meio de seu ID de algum tipo) do modelo, de modo que a visualização possa atualizar os detalhes do nó mostrado quando o delegado do manipulador de eventos retornar. Observe que padrões assíncronos disso podem ser necessários se a busca de dados for muito lenta para uma boa experiência do usuário.

Johann Gerell
fonte
3
Não acho que seja necessário separar a visualização e o apresentador. Eu geralmente desacopro o modelo e o apresentador, fazendo com que o apresentador ouça os eventos do modelo e aja de acordo (atualizo a visualização). Ter um apresentador na visualização facilita a comunicação entre a visualização e o apresentador.
kasperhj
11
@lejon: Você diz que Ter um apresentador na visualização facilita a comunicação entre a visualização e o apresentador , mas discordo totalmente . Meu ponto de vista é o seguinte: quando a visão sabe sobre o apresentador, então, para cada evento de visão, a visão deve decidir qual método de apresentador é o apropriado para chamar. São "2 pontos de complexidade", já que a visão realmente não sabe qual evento de visão corresponde a qual método do apresentador . O contrato não especifica isso.
Johann Gerell
5
@lejon: Se, por outro lado, a visualização apenas expõe o evento real, então o próprio apresentador (quem sabe o que ele quer fazer quando ocorre um evento de visualização) apenas se inscreve para fazer a coisa certa. Isso é apenas "1 ponto de complexidade", que em meu livro é duas vezes tão bom quanto "2 pontos de complexidade". De modo geral, menos acoplamento significa menos custo de manutenção durante a execução de um projeto.
Johann Gerell
9
Eu também tendo a usar o apresentador encapsulado conforme explicado neste link lostechies.com/derekgreer/2008/11/23/… em que a visualização é a única detentora do apresentador.
ak3nat0n
3
@ ak3nat0n: Com relação aos três estilos de MVP explicados no link que você forneceu, acredito que esta resposta de Johann esteja mais alinhada com o terceiro estilo, denominado Estilo do Apresentador Observador : "O benefício do estilo do Apresentador Observador é que ele separa completamente o conhecimento do Apresentador da Visualização, tornando a Visualização menos suscetível a alterações dentro do Apresentador. "
DavidRR
11

O apresentador, que contém toda a lógica da visualização, deve responder ao botão clicado como @JochemKempe diz . Em termos práticos, o botão click do manipulador de eventos chama presenter.OpenFile(). O apresentador pode então determinar o que deve ser feito.

Se decidir que o usuário deve selecionar um arquivo, ele chama de volta para a visualização (por meio de uma interface de visualização) e permite que a visualização, que contém todos os detalhes técnicos da IU, exiba o OpenFileDialog. Essa é uma distinção muito importante, pois o apresentador não deve ter permissão para executar operações vinculadas à tecnologia de IU em uso.

O arquivo selecionado será então devolvido ao apresentador, que continua sua lógica. Isso pode envolver qualquer modelo ou serviço que deve lidar com o processamento do arquivo.

O principal motivo para usar um padrão MVP, imo, é separar a tecnologia de UI da lógica de exibição. Assim, o apresentador orquestra toda a lógica enquanto a visualização a mantém separada da lógica da IU. Isso tem o efeito colateral muito bom de tornar o apresentador totalmente testável na unidade.

Atualização: uma vez que o apresentador é a personificação da lógica encontrada em uma visão específica , a relação visão-apresentador é IMO uma relação um-para-um. E para todos os fins práticos, uma instância de visualização (digamos, um Form) interage com uma instância de apresentador e uma instância de apresentador interage com apenas uma instância de exibição.

Dito isso, na minha implementação do MVP com WinForms, o apresentador sempre interage com a visualização por meio de uma interface que representa as habilidades de IU da visualização. Não há limitação sobre qual visão implementa esta interface, portanto, diferentes "widgets" podem implementar a mesma interface de visão e reutilizar a classe do apresentador.

Peter Lillevold
fonte
Obrigado. Portanto, no método presenter.OpenFile (), ele não deve ter o código para mostrar o openfiledialog? Em vez disso, ele deve voltar para a visualização para mostrar aquela janela?
Darren Young
4
Certo, eu nunca deixaria o apresentador abrir as caixas de diálogo diretamente, pois isso interromperia seus testes. Descarregue isso para a visualização ou, como fiz em alguns cenários, tenha uma classe "FileOpenService" separada para lidar com a interação de diálogo real. Dessa forma, você pode falsificar o serviço de abertura de arquivos durante os testes. Colocar esse código em um serviço separado pode causar bons efeitos colaterais de reutilização :)
Peter Lillevold
2

O apresentador deve agir no final da solicitação e mostrar a janela openfiledialog como você sugeriu. Como nenhum dado é exigido do modelo, o apresentador pode, e deve, lidar com a solicitação.

Vamos supor que você precise dos dados para criar algumas entidades em seu modelo. Você pode passar o canal de fluxo para a camada de acesso onde tem um método para criar entidades do fluxo, mas eu sugiro que você lide com a análise do arquivo em seu apresentador e use um construtor ou método Create por entidade em seu modelo.

JochemKempe
fonte
1
Obrigado pela resposta. Além disso, você teria um único apresentador para a exibição? E esse apresentador lida com a solicitação ou, se os dados forem necessários, ele delega para qualquer número de classes de modelo que atuam de acordo com as solicitações específicas? Essa é a maneira correta? Obrigado novamente.
Darren Young
3
Uma exibição tem um apresentador, mas um apresentador pode ter várias exibições.
JochemKempe