Como mapear o View Model de volta para o Domain Model em uma ação POST?

87

Todos os artigos encontrados na Internet sobre o uso de ViewModels e Automapper fornecem as diretrizes do mapeamento de direção "Controlador -> Visualização". Você pega um modelo de domínio junto com todas as Listas de Seleção em um ViewModel especializado e o passa para a visualização. Isso é claro e bom.
A view possui um formulário e eventualmente estamos na ação POST. Aqui, todos os Model Binders entram em cena junto com [obviamente] outro View Model que está [obviamente] relacionado ao ViewModel original, pelo menos na parte das convenções de nomenclatura para fins de vinculação e validação.

Como você mapeia para o seu modelo de domínio?

Que seja uma ação de inserção, poderíamos usar o mesmo Automapper. Mas e se fosse uma ação de atualização? Temos que recuperar nossa Entidade de Domínio do Repositório, atualizar suas propriedades de acordo com os valores no ViewModel e salvar no Repositório.

ADENDO 1 (9 de fevereiro de 2010): Às vezes, atribuir as propriedades do Model não é suficiente. Deve ser realizada alguma ação contra o Modelo de Domínio de acordo com os valores do Modelo de Visão. Ou seja, alguns métodos devem ser chamados no Modelo de Domínio. Provavelmente, deve haver uma espécie de camada de serviço de aplicativo que fica entre o controlador e o domínio para processar os modelos de visão ...


Como organizar este código e onde colocá-lo para atingir os seguintes objetivos?

  • manter os controladores finos
  • honrar a prática SoC
  • seguir os princípios do Domain-Driven Design
  • ser SECO
  • continua ...
Anthony Serdyukov
fonte

Respostas:

37

Eu uso uma interface IBuilder e implemento-a usando o ValueInjecter

public interface IBuilder<TEntity, TViewModel>
{
      TEntity BuildEntity(TViewModel viewModel);
      TViewModel BuildViewModel(TEntity entity);
      TViewModel RebuildViewModel(TViewModel viewModel); 
}

... (implementação) RebuildViewModel apenas chamaBuildViewModel(BuilEntity(viewModel))

[HttpPost]
public ActionResult Update(ViewModel model)
{
   if(!ModelState.IsValid)
    {
       return View(builder.RebuildViewModel(model);
    }

   service.SaveOrUpdate(builder.BuildEntity(model));
   return RedirectToAction("Index");
}

btw eu não escrevo ViewModel eu escrevo Input porque é muito mais curto, mas isso não é realmente importante
espero que ajude

Atualização: Estou usando essa abordagem agora no aplicativo de demonstração ProDinner ASP.net MVC , agora se chama IMapper, também há um pdf fornecido onde essa abordagem é explicada em detalhes

Omu
fonte
Eu gosto dessa abordagem. Uma coisa que não estou certa é a implementação do IBuilder, especialmente à luz de um aplicativo em camadas. Por exemplo, meu ViewModel tem 3 SelectLists. Como a implementação do construtor recupera os valores da lista de seleção do repositório?
Matt Murrell de
@Matt Murrell olhe para prodinner.codeplex.com Eu faço isso lá, e chamo de IMapper lá em vez de IBuilder
Omu
6
Eu gosto dessa abordagem, implementei uma amostra dela aqui: gist.github.com/2379583
Paul Stovell
Em minha opinião, não é compatível com a abordagem do Modelo de Domínio. Parece uma abordagem CRUD para requisitos pouco claros. Não deveríamos usar Fábricas (DDD) e métodos relacionados no Modelo de Domínio para transmitir alguma ação razoável? Dessa forma, é melhor carregar uma entidade do banco de dados e atualizá-la conforme necessário, certo? Portanto, parece que não está totalmente correto.
Artyom
7

Ferramentas como o AutoMapper podem ser usadas para atualizar o objeto existente com dados do objeto de origem. A ação do controlador para atualização pode ser semelhante a:

[HttpPost]
public ActionResult Update(MyViewModel viewModel)
{
    MyDataModel dataModel = this.DataRepository.GetMyData(viewModel.Id);
    Mapper<MyViewModel, MyDataModel>(viewModel, dataModel);
    this.Repostitory.SaveMyData(dataModel);
    return View(viewModel);
}

Além do que é visível no snippet acima:

  • Os dados POST para visualizar o modelo + validação são feitos no ModelBinder (pode ser estendido com ligações personalizadas)
  • Tratamento de erros (ou seja, captura de exceção de acesso a dados lançada pelo Repositório) pode ser feito pelo filtro [HandleError]

A ação do controlador é muito fina e os interesses são separados: os problemas de mapeamento são tratados na configuração do AutoMapper, a validação é feita pelo ModelBinder e o acesso aos dados pelo Repositório.

PanJanek
fonte
6
Não tenho certeza se o Automapper é útil aqui, pois não pode reverter o nivelamento. Afinal, o Modelo de Domínio não é um DTO simples como o Modelo de Visualização, portanto, pode não ser suficiente atribuir algumas propriedades a ele. Provavelmente, algumas ações devem ser executadas no Modelo de Domínio de acordo com o conteúdo do Modelo de Visão. No entanto, 1 para compartilhar uma abordagem muito boa.
Anthony Serdyukov
@Anton ValueInjecter pode reverter o nivelamento;)
Omu
com esta abordagem você não mantém o controlador fino, você viola SoC e DRY ... como Omu mencionou, você deve ter uma camada separada que cuida do material de mapeamento.
Rookian
5

Eu gostaria de dizer que você reutiliza o termo ViewModel para ambas as direções da interação do cliente. Se você leu código ASP.NET MVC suficiente em liberdade, provavelmente viu a distinção entre ViewModel e EditModel. Eu acho isso importante.

Um ViewModel representa todas as informações necessárias para renderizar uma visualização. Isso pode incluir dados que são renderizados em locais não interativos estáticos e também dados puramente para realizar uma verificação para decidir o que exatamente renderizar. Uma ação GET do controlador é geralmente responsável por empacotar o ViewModel para sua View.

Um EditModel (ou talvez um ActionModel) representa os dados necessários para realizar a ação que o usuário deseja fazer para aquele POST. Portanto, um EditModel está realmente tentando descrever uma ação. Isso provavelmente excluirá alguns dados do ViewModel e, embora relacionados, acho importante perceber que eles são realmente diferentes.

Uma ideia

Dito isso, você poderia facilmente ter uma configuração do AutoMapper para ir de Model -> ViewModel e uma diferente para ir de EditModel -> Model. Então, as diferentes ações do controlador precisam apenas usar o AutoMapper. Inferno, o EditModel poderia ter funções para validar suas propriedades em relação ao modelo e para aplicar esses valores ao próprio Modelo. Ele não está fazendo mais nada e você tem ModelBinders no MVC para mapear a solicitação para o EditModel de qualquer maneira.

Outra ideia

Além disso, algo em que estive pensando recentemente que funciona com a ideia de um ActionModel é que o que o cliente está postando de volta para você é na verdade a descrição de várias ações que o usuário realizou e não apenas um grande conjunto de dados. Isso certamente exigiria algum Javascript do lado do cliente para gerenciar, mas a ideia é intrigante, eu acho.

Essencialmente, conforme o usuário executa ações na tela que você os apresentou, o Javascript começa a criar uma lista de objetos de ação. Um exemplo é possivelmente o usuário estar em uma tela de informações do funcionário. Eles atualizam o sobrenome e adicionam um novo endereço porque o funcionário se casou recentemente. Nos bastidores, isso produz um ChangeEmployeeNamee um AddEmployeeMailingAddressobjetos para uma lista. O usuário clica em 'Salvar' para confirmar as alterações e você envia a lista de dois objetos, cada um contendo apenas as informações necessárias para realizar cada ação.

Você precisaria de um ModelBinder mais inteligente do que o padrão, mas um bom serializador JSON deve ser capaz de cuidar do mapeamento dos objetos de ação do lado do cliente para os do lado do servidor. Os do lado do servidor (se você estiver em um ambiente de 2 camadas) podem facilmente ter métodos que concluem a ação no modelo com o qual trabalham. Assim, a ação Controller acaba obtendo apenas um Id para a instância de Model puxar e uma lista de ações a serem executadas nela. Ou as ações têm o id para mantê-los bem separados.

Então, talvez algo assim seja percebido no lado do servidor:

public interface IUserAction<TModel>
{
     long ModelId { get; set; }
     IEnumerable<string> Validate(TModel model);
     void Complete(TModel model);
}

[Transaction] //just assuming some sort of 2-tier with transactions handled by filter
public ActionResult Save(IEnumerable<IUserAction<Employee>> actions)
{
     var errors = new List<string>();
     foreach( var action in actions ) 
     {
         // relying on ORM's identity map to prevent multiple database hits
         var employee = _employeeRepository.Get(action.ModelId);
         errors.AddRange(action.Validate(employee));
     }

     // handle error cases possibly rendering view with them

     foreach( var action in editModel.UserActions )
     {
         var employee = _employeeRepository.Get(action.ModelId);
         action.Complete(employee);
         // against relying on ORMs ability to properly generate SQL and batch changes
         _employeeRepository.Update(employee);
     }

     // render the success view
}

Isso realmente torna a ação de postagem de volta bastante genérica, pois você está contando com seu ModelBinder para obter a instância IUserAction correta e sua instância IUserAction para realizar a própria lógica correta ou (mais provavelmente) chamar o Model com as informações.

Se você estivesse em um ambiente de 3 camadas, a IUserAction poderia ser transformada em DTOs simples para serem disparados além do limite e executados em um método semelhante na camada do aplicativo. Dependendo de como você faz essa camada, ela pode ser facilmente dividida e ainda permanecer em uma transação (o que vem à mente é a solicitação / resposta de Agatha e aproveitando o DI e o mapa de identidade do NHibernate).

De qualquer forma, tenho certeza de que não é uma ideia perfeita, exigiria algum JS do lado do cliente para gerenciar, e ainda não consegui fazer um projeto para ver como se desenrola, mas o post estava tentando pensar em como ir e voltar, então resolvi dar minhas opiniões. Espero que ajude e adoraria ouvir sobre outras maneiras de gerenciar as interações.

Sean Copenhaver
fonte
Interessante. Com relação à distinção entre ViewModel e EditModel ... você está sugerindo que para uma função de edição, você usaria um ViewModel para criar o formulário e, em seguida, vincularia a um EditModel quando o usuário o postasse? Em caso afirmativo, como você lidaria com as situações em que precisaria postar novamente o formulário devido a erros de validação (por exemplo, quando o ViewModel continha elementos para preencher um menu suspenso) - você apenas incluiria os elementos suspensos no EditModel também? Nesse caso, qual seria a diferença entre os dois?
UpTheCreek
Suponho que sua preocupação seja que se eu usar um EditModel e houver um erro, terei que reconstruir meu ViewModel, o que pode ser muito caro. Eu diria apenas reconstruir o ViewModel e certificar-se de que ele tenha um lugar para colocar mensagens de notificação de usuário (provavelmente tanto positivas quanto negativas, como erros de validação). Se for um problema de desempenho, você pode sempre armazenar em cache o ViewModel até que a próxima solicitação da sessão termine (provavelmente sendo a postagem do EditModel).
Sean Copenhaver