ASP.NET Razor: Solucionando alguns problemas de arquitetura para o modelo de exibição

imagem


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) { //     transportAddDto... } 

Aqui vemos pelo menos dois problemas:


  1. 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 .
  2. 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

Source: https://habr.com/ru/post/pt416315/


All Articles