
1. Introdução
Olá colegas!
Hoje, quero compartilhar com você minha experiência no desenvolvimento da arquitetura View Model como parte do desenvolvimento de aplicativos Web ASP.NET usando o mecanismo de modelo Razor .
As implementações técnicas descritas neste artigo são adequadas para todas as versões atuais do ASP. NET ( MVC 5 , Core , etc). O artigo em si é destinado a leitores que, pelo menos, já tinham experiência em trabalhar com essa pilha. Também é importante notar que, no contexto disso, não consideramos a utilidade do Modelo de Visualização e sua aplicação hipotética (pressupõe-se que o leitor já esteja familiarizado com essas coisas), discutimos diretamente a implementação.
Desafio
Para uma assimilação conveniente e racional do material, proponho considerar imediatamente a tarefa que naturalmente nos levará a problemas em potencial e suas soluções ideais.
Esse é o problema da adição banal de, digamos, um carro novo a um determinado catálogo de veículos . Para não complicar a tarefa abstrata, os detalhes dos aspectos restantes serão intencionalmente perdidos. Parece que a tarefa elementar, no entanto, é tentar fazer tudo com foco no dimensionamento adicional do sistema (em particular, na expansão de modelos com relação ao número de propriedades e outros componentes definidores), para que mais tarde seja o mais confortável possível trabalhar.
Implementação
Deixe o modelo com a seguinte aparência (por uma questão de simplicidade, coisas como propriedades de navegação e assim por diante não são fornecidas):
class Transport { public int Id { get; set; } public int TransportTypeId { get; set; } public string Number { get; set; } }
Obviamente, TransportTypeId é uma chave estrangeira para um objeto do tipo TransportType :
class TransportType { public int Id { get; set; } public string Name { get; set; } }
Para a conexão entre front-end e back-end, usaremos o modelo de objeto de transferência de dados . Consequentemente, o DTO para adicionar um carro será mais ou menos assim:
class TransportAddDTO { [Required] public int TransportTypeId { get; set; } [Required] [MaxLength(10)] public string Number { get; set; } }
* Usa atributos de validação padrão de System.ComponentModel.DataAnnotations
.
Está na hora de descobrir qual será o modelo de exibição da página de adição do carro. Alguns desenvolvedores declarariam com prazer que o próprio TransportAddDTO seria tal, no entanto, isso é fundamentalmente errado, já que nada pode ser “amontoado” nessa classe, exceto diretamente pelas informações de back-end necessárias para adicionar um novo elemento (por definição). Além disso, outros dados podem ser necessários na página de adição: por exemplo, um diretório de tipos de veículo (com base no qual TransportTypeId é posteriormente expresso). Nesse sentido, o seguinte modelo de exibição sugere-se:
class TransportAddViewModel { public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Onde TransportTypeDTO , neste caso, será um mapeamento direto de TransportType (e isso está longe de ser sempre o caso - tanto na direção do truncamento quanto na direção da expansão):
class TransportTypeDTO { public int Id { get; set; } public string Name { get; set; } }
Nesse estágio, surge uma pergunta razoável: no Razor , será possível transferir apenas um modelo (e graças a Deus), como então o TransportAddDTO pode ser usado para gerar código HTML dentro desta página?
Muito fácil! Basta adicionar, em particular, este DTO ao View Model , algo como isto:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Agora os primeiros problemas começam. Vamos tentar adicionar um TextBox padrão para o "número do veículo" à página do nosso arquivo .cshtml (seja TransportAddView.cshtml):
@model TransportAddViewModel @Html.TextBoxFor(m => m.AddDTO.Number)
Isso renderizará em código HTML assim:
<input id="AddDTO_Number" name="AddDTO.Number" />
Imagine que a parte do controlador com o método de adicionar veículos se parece com isso (o código de acordo com o MVC 5, para o Core, será um pouco diferente, mas a essência é a mesma ):
[Route("add"), HttpPost] public ActionResult Add(TransportAddDTO transportAddDto) {
Aqui vemos pelo menos dois problemas:
- Os atributos Id e Name são prefixados com AddDTO e, se o método de adicionar transporte no controlador usando o princípio de ligação do modelo tentar vincular os dados que vieram do cliente ao TransportAddDTO , o objeto dentro será composto inteiramente de zeros (valores padrão), isto é será apenas uma nova instância vazia. É lógico - os nomes esperados do fichário do formulário Number , não AddDTO_Number .
- Todos os meta atributos se foram, ou seja, data-val-required e todos os outros que descrevemos com tanto cuidado no AddDTO como atributos de validação. Para aqueles que usam todo o poder do Razor, isso é crítico, pois é uma perda significativa de informações para o frontend.
Temos sorte e eles têm decisões correspondentes.
Essas coisas "funcionam" ao usar, por exemplo, o wrapper para a interface do Kendo (por exemplo, @Html.Kendo().TextBoxFor()
, etc.).
Vamos começar com o segundo problema: o motivo aqui é que, no modelo de exibição, a instância TransportAddDTO transferida era nula . E a implementação dos mecanismos de renderização é tal que os atributos, neste caso, são lidos pelo menos não completamente. A solução, respectivamente, é óbvia - primeiro no modelo de exibição para inicializar a propriedade TransportAddDTO com uma instância da classe usando o construtor padrão. É melhor fazer isso em um serviço que retorna um View Model inicializado; no entanto, como parte do exemplo, ele fará o mesmo:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } = new TransportAddDTO(); public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Após essas alterações, o resultado será semelhante a:
<input data-val="true" id="AddDTO_Number" name="AddDTO.Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />
Já está melhor! Resta lidar com o primeiro problema - a propósito, tudo é um pouco mais complicado.
Para entendê-lo, primeiro você precisa descobrir o que o Razor (implica o WebViewPage, cuja instância dentro de .cshtml está disponível como esta ) é uma propriedade Html a que nos referimos para chamar TextBoxFor
.
Olhando para ele, você pode entender instantaneamente que é do tipo HtmlHelper<T>
, no nosso caso, HtmlHelper<TransportAddViewModel>
. Uma possível solução para o problema surge - criar seu próprio HtmlHelper dentro e passar nosso TransportAddDTO para ele como uma entrada. Encontramos o menor construtor possível para uma instância desta classe:
HtmlHelper<T>.HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer);
Podemos passar o ViewContext diretamente da nossa instância this.ViewContext
por this.ViewContext
. Agora vamos descobrir onde obter uma instância de uma classe que implementa a interface IViewDataContainer. Por exemplo, crie sua implementação:
public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } }
Como você pode ver, agora nos deparamos com algum objeto passado ao construtor com o objetivo de inicializar o ViewDataDictionary , já que tudo é simples aqui - esta é uma instância do nosso TransportAddDTO do View Model. Ou seja, você pode obter a instância estimada assim:
var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO);
Portanto, não há problemas na criação de um novo HtmlHelper:
var Helper = new HtmlHelper<T>(this.ViewContext, vdc);
Agora você pode usar o seguinte:
@model TransportAddViewModel @{ var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); var Helper = new HtmlHelper<T>(this.ViewContext, vdc); } @Helper.TextBoxFor(m => m.Number)
Isso renderizará em código HTML assim:
<input data-val="true" id="Number" name="Number" data-val-required="The Number field is required." data-val-length="The field Number must be a string with a maximum length of 10." data-val-length-max="10" />
Como você pode ver, agora não há problemas com o elemento renderizado, e ele está pronto para uso total. Resta apenas "pentear" o código para que pareça menos volumoso. Por exemplo, estendemos nosso ViewDataContainer da seguinte maneira:
public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } public HtmlHelper<T> GetHtmlHelper(ViewContext context) { return new HtmlHelper<T>(context, this); } }
Então, do Razor, você pode trabalhar assim:
@model TransportAddViewModel @{ var Helper = new ViewDataContainer<TransportAddDTO>(Model.AddDTO).GetHtmlHelper(ViewContext); } @Helper.TextBoxFor(m => m.Number)
Além disso, ninguém se preocupa em estender a implementação padrão do WebViewPage para que ele contenha a propriedade desejada (com um setter para uma instância da classe DTO).
Conclusão
Isso resolveu o problema e também obteve a arquitetura do View Model para trabalhar com o Razor, que poderia conter todos os elementos necessários.
É importante notar que o ViewDataContainer resultante acabou por ser universal e adequado para uso.
Resta adicionar alguns botões ao nosso arquivo .cshtml, e a tarefa será concluída (sem considerar o processamento no backend'e). Proponho fazer isso sozinho.
Se um leitor respeitado tiver idéias sobre como implementar o que é necessário de maneiras mais ótimas, terei prazer em ouvir os comentários.
Atenciosamente
Peter Osetrov