Como tornar a criação de modelos de exibição em tempo de execução menos dolorosa


17

Peço desculpas pela longa pergunta, ela parece um pouco como um discurso retórico, mas prometo que não! Resumi minhas perguntas abaixo

No mundo MVC, as coisas são simples. O Modelo tem estado, a Visualização mostra o Modelo, e o Controlador faz coisas para / com o Modelo (basicamente), um controlador não tem estado. Para fazer coisas, o Controlador tem algumas dependências em serviços da Web, repositório e muito. Ao instanciar um controlador, você se preocupa em fornecer essas dependências, nada mais. Ao executar uma ação (método no Controller), você usa essas dependências para recuperar ou atualizar o Modelo ou chamar outro serviço de domínio. Se houver algum contexto, digamos que, como um usuário deseja ver os detalhes de um item específico, você passa o ID desse item como parâmetro para a Ação. Em nenhum lugar do Controlador há alguma referência a qualquer estado. Por enquanto, tudo bem.

Digite MVVM. Eu amo o WPF, amo a ligação de dados. Eu amo estruturas que tornam a ligação de dados ao ViewModels ainda mais fácil (usando o Caliburn Micro atm). Eu sinto que as coisas são menos simples neste mundo. Vamos fazer o exercício novamente: o modelo tem estado, a exibição mostra o ViewModel eo ViewModel faz coisas para / com o modelo (basicamente), um ViewModel faz tem estado! (para esclarecer, talvez ele delega todas as propriedades para um ou mais modelos, mas isso significa que ele deve ter uma referência ao modelo de uma forma ou de outra, que é o estado em si) Para fazercoisas que o ViewModel tem algumas dependências em serviços da web, repositório e muito. Ao instanciar um ViewModel, você se preocupa em fornecer essas dependências, mas também o estado. E isso, senhoras e senhores, me irrita sem fim.

Sempre que você precisar instanciar a ProductDetailsViewModelpartir de ProductSearchViewModel(do qual você chamou o ProductSearchWebServiceque por sua vez retornou IEnumerable<ProductDTO>, todo mundo ainda está comigo?), Você pode fazer uma destas coisas:

  • chamada new ProductDetailsViewModel(productDTO, _shoppingCartWebService /* dependcy */);, isso é ruim, imagine mais 3 dependências, isso significa que também é ProductSearchViewModelnecessário assumir essas dependências. Mudar o construtor também é doloroso.
  • Por exemplo _myInjectedProductDetailsViewModelFactory.Create().Initialize(productDTO);, a fábrica é apenas um Func, eles são facilmente gerados pela maioria das estruturas de IoC. Eu acho que isso é ruim porque os métodos Init são uma abstração com vazamento. Você também não pode usar a palavra-chave readonly para campos definidos no método Init. Tenho certeza de que existem mais algumas razões.
  • call _myInjectedProductDetailsViewModelAbstractFactory.Create(productDTO);Então ... esse é o padrão (fábrica abstrata) geralmente recomendado para esse tipo de problema. Eu pensei que era genial, pois satisfaz meu desejo de digitar estática, até que comecei a usá-lo. A quantidade de código clichê é, eu acho demais (você sabe, além dos nomes de variáveis ​​ridículos que eu uso). Para cada ViewModel que precisa de parâmetros de tempo de execução, você obtém dois arquivos extras (interface e implementação de fábrica) e precisa digitar as dependências que não são de tempo de execução, como 4 vezes extras. E toda vez que as dependências mudam, você também muda na fábrica. Parece que eu nem uso mais um contêiner de DI. (Eu acho que o Castelo Windsor tem algum tipo de solução para isso [com suas próprias desvantagens, me corrija se eu estiver errado]).
  • faça algo com tipos ou dicionário anônimos. Eu gosto da minha digitação estática.

Então sim. A mistura de estado e comportamento dessa maneira cria um problema que não existe no MVC. E sinto que atualmente não há uma solução realmente adequada para esse problema. Agora eu gostaria de observar algumas coisas:

  • As pessoas realmente usam MVVM. Portanto, eles não se preocupam com tudo isso, ou têm alguma outra solução brilhante.
  • Não encontrei um exemplo detalhado de MVVM com WPF. Por exemplo, o projeto de amostra NDDD me ajudou imensamente a entender alguns conceitos de DDD. Eu realmente gostaria que alguém pudesse me apontar na direção de algo semelhante para o MVVM / WPF.
  • Talvez eu esteja fazendo MVVM errado e eu deva virar meu design de cabeça para baixo. Talvez eu não devesse ter esse problema. Bem, eu sei que outras pessoas fizeram a mesma pergunta, então acho que não sou a única.

Resumir

  • Estou correto ao concluir que ter o ViewModel sendo um ponto de integração para o estado e o comportamento é o motivo de algumas dificuldades com o padrão MVVM como um todo?
  • O uso do padrão abstrato de fábrica é a única / melhor maneira de instanciar um ViewModel de maneira estaticamente tipada?
  • Existe algo como uma implementação de referência detalhada disponível?
  • Ter um monte de ViewModels com estado / comportamento é um cheiro de design?

10
É muito longo para ler, considere revisar, há muitas coisas irrelevantes lá. Você pode perder boas respostas porque as pessoas não se incomodam em ler tudo isso.
precisa

Você disse que ama o Caliburn.Micro, mas não sabe como essa estrutura pode ajudar a instanciar novos modelos de exibição? Veja alguns exemplos disso.
Euphoric

@Euphoric Você poderia ser um pouco mais específico, o Google parece não me ajudar aqui. Tem algumas palavras-chave que eu poderia procurar?
Dvdvorle

3
Eu acho que você está simplificando um pouco o MVC. Certamente, o modo de exibição mostra o modelo no início, mas durante a operação está mudando de estado. Esse estado de mudança é, na minha opinião, um "Editar modelo". Ou seja, uma versão achatada do modelo com restrições de consistência reduzidas. Na verdade, o que chamo de Modelo de Edição é o MVVM ViewModel. Ele mantém o estado em transição, anteriormente mantido pelo View no MVC ou empurrado de volta para uma versão não confirmada do Model, onde acho que não pertence. Então você tinha estado "em fluxo" antes. Agora está tudo no ViewModel.
11118 Scott-Locklock-

@ScottWhitlock Estou realmente simplificando o MVC. Mas não estou dizendo que é errado que o estado "em fluxo" esteja no ViewModel, estou dizendo que sobrecarregar o comportamento também torna mais difícil inicializar o ViewModel para um estado utilizável, digamos, outro ViewModel. Seu "Editar modelo" no MVC não sabe como se salvar (não possui um método Save). Mas o controlador sabe disso e tem todas as dependências necessárias para fazer isso.
Dvdvorle 02/11/12

Respostas:


2

A questão das dependências ao iniciar um novo modelo de visualização pode ser tratada com o IOC.

public class MyCustomViewModel{
  private readonly IShoppingCartWebService _cartService;

  private readonly ITimeService _timeService;

  public ProductDTO ProductDTO { get; set; }

  public ProductDetailsViewModel(IShoppingCartWebService cartService, ITimeService timeService){
    _cartService = cartService;
    _timeService = timeService;
  }
}

Ao instalar o contêiner ...

Container.Register<IShoppingCartWebService,ShoppingCartWebSerivce>().As.Singleton();
Container.Register<ITimeService,TimeService>().As.Singleton();
Container.Register<ProductDetailsViewModel>();

Quando você precisa do seu modelo de visualização:

var viewmodel = Container.Resolve<ProductDetailsViewModel>();
viewmodel.ProductDTO = myProductDTO;

Ao utilizar uma estrutura como o caliburn micro , geralmente existe alguma forma de contêiner do COI.

SomeCompositionView view = new SomeCompositionView();
ISomeCompositionViewModel viewModel = IoC.Get<ISomeCompositionViewModel>();
ViewModelBinder.Bind(viewModel, view, null);

1

Trabalho diariamente com o ASP.NET MVC e trabalho em um WPF há mais de um ano, e é assim que vejo:

MVC

O controlador deve orquestrar ações (busque isso, adicione aquilo).

A visualização é responsável por exibir o modelo.

O modelo normalmente abrange dados (por exemplo, UserId, FirstName) e também o estado (por exemplo, títulos) e geralmente é específico para exibição.

MVVM

O modelo normalmente contém apenas dados (por exemplo, UserId, FirstName) e geralmente é transmitido

O modelo de visualização abrange o comportamento da visualização (métodos), seus dados (modelo) e interações (comandos) - semelhantes ao padrão MVP ativo em que o apresentador está ciente do modelo. O modelo de vista é específico da vista (1 vista = 1 modelo de vista).

A visualização é responsável por exibir dados e ligação de dados ao modelo de visualização. Quando uma vista é criada, geralmente seu modelo de vista associado é criado com ela.


O que você deve se lembrar é que o padrão de apresentação MVVM é específico ao WPF / Silverlight devido à sua natureza de ligação de dados.

A visualização geralmente sabe com qual modelo de visualização está associado (ou uma abstração de uma).

Eu aconselho que você trate o modelo de exibição como um singleton, mesmo que seja instanciado por exibição. Em outras palavras, você deve ser capaz de criá-lo via DI através de um contêiner do IOC e chamar métodos apropriados para dizer; carregar seu modelo com base em parâmetros. Algo assim:

public partial class EditUserView
{
    public EditUserView(IContainer container, int userId) : this() {
        var viewModel = container.Resolve<EditUserViewModel>();
        viewModel.LoadModel(userId);
        DataContext = viewModel;
    }
}

Como exemplo, neste caso, você não criaria um modelo de visualização específico para o usuário que está sendo atualizado. Em vez disso, o modelo conteria dados específicos do usuário que são carregados por meio de alguma chamada no modelo de visualização.


Se meu nome é "Peter" e meus títulos são {"Rev", "Dr"} *, por que você considera os dados de nome e estado de título? Ou você pode esclarecer seu exemplo? * na verdade não
Pete Kirkham /

@PeteKirkham - o exemplo de 'títulos' que eu estava me referindo no contexto de, digamos, uma caixa de combinação. Geralmente, quando você envia informações para persistir, não envia o estado (por exemplo, uma lista de estados / províncias / títulos) que foi usado para fazer seleções. Qualquer estado que valha a pena transferir com os dados (por exemplo, é o nome de usuário em uso) deve ser verificado no ponto de processamento porque o estado pode ter ficado obsoleto (se você estivesse usando algum padrão assíncrono, como enfileiramento de mensagens).
Shelakel 8/03/13

Embora já se passaram dois anos desde este post, devo fazer um comentário a favor dos futuros espectadores: duas coisas me perturbaram com sua resposta. Uma View talvez corresponda a um ViewModel, mas um ViewModel pode ser representado por várias Views. Em segundo lugar, o que você está descrevendo é o antipadrão do Localizador de Serviço. IMHO você não deve resolver diretamente os modelos de exibição em todos os lugares. É para isso que serve o DI. Faça suas resoluções em menos pontos possível. Deixe o Caliburn fazer esse trabalho para você, por exemplo.
Jony Adamit

1

Resposta curta para suas perguntas:

  1. Sim Estado + Comportamento leva a esses problemas, mas isso é verdade para todos os OO. O verdadeiro culpado é o acoplamento do ViewModels, que é um tipo de violação do SRP.
  2. Estaticamente digitado, provavelmente. Mas você deve reduzir / eliminar sua necessidade de instanciar ViewModels de outros ViewModels.
  3. Não que eu esteja ciente.
  4. Não, mas ter ViewModels com estado e comportamento não relacionados (como algumas referências de modelo e algumas referências de ViewModel)

A versão longa:

Estamos enfrentando o mesmo problema e encontramos algumas coisas que podem ajudá-lo. Embora eu não conheça a solução "mágica", essas coisas estão aliviando um pouco a dor.

  1. Implemente modelos vinculáveis ​​de DTOs para rastreamento e validação de alterações. Esses modelos de "dados" não devem depender de serviços e não vêm do contêiner. Eles podem ser apenas "novos", distribuídos e podem até derivar do DTO. Bottomline é implementar um modelo específico para sua aplicação (como MVC).

  2. Desacoplar seus ViewModels. O Caliburn facilita o acoplamento dos ViewModels. Até o sugere através do seu modelo Screen / Conductor. Mas esse acoplamento torna os ViewModels difíceis de testar, cria muitas dependências e o mais importante: impõe o ônus de gerenciar o ciclo de vida do ViewModel nos seus ViewModels. Uma maneira de dissociá-los é usar algo como um serviço de navegação ou um controlador ViewModel. Por exemplo

    interface pública IShowViewModels {void Show (objeto inlineArgumentsAsAnonymousType, string regionId); }

Melhor ainda é fazer isso de alguma forma. Mas o importante é não lidar com o ciclo de vida do ViewModel de outros ViewModels. No MVC, os controladores não dependem um do outro e no MVVM ViewModels não devem depender um do outro. Integre-os de outras maneiras.

  1. Use seus contêineres com recursos dinâmicos e com tipos de string. Embora seja possível criar algo como INeedData<T1,T2,...>e impor parâmetros de criação com segurança de tipo, não vale a pena. Também não vale a pena criar fábricas para cada tipo de ViewModel. A maioria dos contêineres IoC fornece soluções para isso. Você obterá erros no tempo de execução, mas o desacoplamento e a testabilidade da unidade valem a pena. Você ainda faz algum tipo de teste de integração e esses erros são detectados facilmente.

0

A maneira como costumo fazer isso (usando o PRISM) é que cada assembly contém um módulo de inicialização de contêiner, no qual todas as interfaces e instâncias são registradas na inicialização.

private void RegisterResources()
{
    Container.RegisterType<IDataService, DataService>();
    Container.RegisterType<IProductSearchViewModel, ProductSearchViewModel>();
    Container.RegisterType<IProductDetailsViewModel, ProductDetailsViewModel>();
}

E, dadas as classes de exemplo, seriam implementadas dessa maneira, com o contêiner sendo passado por todo o caminho. Dessa forma, quaisquer novas dependências podem ser adicionadas facilmente, pois você já tem acesso ao contêiner.

/// <summary>
/// IDataService Interface
/// </summary>
public interface IDataService
{
    DataTable GetSomeData();
}

public class DataService : IDataService
{
    public DataTable GetSomeData()
    {
        MessageBox.Show("This is a call to the GetSomeData() method.");

        var someData = new DataTable("SomeData");
        return someData;
    }
}

public interface IProductSearchViewModel
{
}

public class ProductSearchViewModel : IProductSearchViewModel
{
    private readonly IUnityContainer _container;

    /// <summary>
    /// This will get resolved if it's been added to the container.
    /// Or alternately you could use constructor resolution. 
    /// </summary>
    [Dependency]
    public IDataService DataService { get; set; }

    public ProductSearchViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void SearchAndDisplay()
    {
        DataTable results = DataService.GetSomeData();

        var detailsViewModel = _container.Resolve<IProductDetailsViewModel>();
        detailsViewModel.DisplaySomeDataInView(results);

        // Create the view, usually resolve using region manager etc.
        var detailsView = new DetailsView() { DataContext = detailsViewModel };
    }
}

public interface IProductDetailsViewModel
{
    void DisplaySomeDataInView(DataTable dataTable);
}

public class ProductDetailsViewModel : IProductDetailsViewModel
{
    private readonly IUnityContainer _container;

    public ProductDetailsViewModel(IUnityContainer container)
    {
        _container = container;
    }

    public void DisplaySomeDataInView(DataTable dataTable)
    {
    }
}

É bastante comum ter uma classe ViewModelBase, da qual todos os seus modelos de exibição são derivados, que contém uma referência ao contêiner. Contanto que você adquira o hábito de resolver todos os modelos de exibição em vez new()'ingdeles, isso deverá tornar toda a resolução de dependências muito mais simples.


0

Às vezes, é bom ir para a definição mais simples, em vez de um exemplo completo: http://en.wikipedia.org/wiki/Model_View_ViewModel talvez a leitura do exemplo ZK Java seja mais esclarecedora que a do C #.

Outras vezes, ouça seu instinto ...

Ter um monte de ViewModels com estado / comportamento é um cheiro de design?

Seus modelos são objetos por mapeamentos de tabela? Talvez um ORM ajude a mapear objetos do domínio enquanto lida com os negócios ou atualiza várias tabelas.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.