ASP.NET Razor: Resolviendo algunos problemas de arquitectura para el modelo de vista

imagen


Introduccion


Hola colegas
Hoy quiero compartir con ustedes mi experiencia en el desarrollo de la arquitectura View Model como parte del desarrollo de aplicaciones web ASP.NET utilizando el motor de plantillas Razor .
Las implementaciones técnicas descritas en este artículo son adecuadas para todas las versiones actuales de ASP. NET ( MVC 5 , Core , etc.). El artículo en sí está destinado a lectores que, al menos, ya tenían experiencia trabajando con esta pila. También vale la pena señalar que, en el marco de esto, no consideramos la utilidad del Modelo de vista y su aplicación hipotética (se supone que el lector ya está familiarizado con estas cosas), discutimos directamente la implementación.


Desafío


Para una asimilación racional y conveniente del material, propongo considerar inmediatamente la tarea que naturalmente nos llevará a posibles problemas y sus soluciones óptimas.
Este es el problema de la adición banal de, por ejemplo, un auto nuevo a un determinado catálogo de vehículos . Para no complicar la tarea abstracta, se perderán intencionalmente los detalles de los aspectos restantes. Sin embargo, parece que la tarea primaria es tratar de hacer todo con un enfoque en la ampliación del sistema (en particular, expandir los modelos en relación con el número de propiedades y otros componentes definitorios) para que luego sea lo más cómodo posible para trabajar.


Implementación


Deje que el modelo se vea de la siguiente manera (en aras de la simplicidad, no se dan cosas como las propiedades de navegación, etc.):


class Transport { public int Id { get; set; } public int TransportTypeId { get; set; } public string Number { get; set; } } 

Por supuesto, TransportTypeId es una clave foránea para un objeto de tipo TransportType :


 class TransportType { public int Id { get; set; } public string Name { get; set; } } 

Para la conexión entre el frontend y el backend, utilizaremos la plantilla de Objeto de transferencia de datos . En consecuencia, el DTO para agregar un automóvil se verá así:


 class TransportAddDTO { [Required] public int TransportTypeId { get; set; } [Required] [MaxLength(10)] public string Number { get; set; } } 

* Utiliza atributos de validación estándar de System.ComponentModel.DataAnnotations .


Es hora de averiguar cuál será el modelo de vista para la página de agregar autos. Algunos desarrolladores con mucho gusto anunciarían que TransportAddDTO en sí mismo sería así, sin embargo, esto es fundamentalmente incorrecto, ya que nada puede ser "abarrotado" en esta clase, excepto directamente por la información de fondo necesaria para agregar un nuevo elemento (por definición). Además, se pueden requerir otros datos en la página de agregar: por ejemplo, un directorio de tipos de vehículos (sobre la base de los cuales TransportTypeId se expresa posteriormente). En este sentido, se sugiere el siguiente modelo de vista:


 class TransportAddViewModel { public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } } 

Donde TransportTypeDTO en este caso será una asignación directa de TransportType (y esto está lejos de ser siempre el caso, tanto en la dirección del truncamiento como en la dirección de la expansión):


 class TransportTypeDTO { public int Id { get; set; } public string Name { get; set; } } 

En esta etapa, surge la pregunta razonable: en Razor será posible transferir solo un modelo (y gracias a Dios), ¿cómo se puede usar TransportAddDTO para generar código HTML dentro de esta página?
Muy facil! Es suficiente agregar, en particular, este DTO al modelo de vista , algo como esto:


 class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } } 

Ahora comienzan los primeros problemas. Intentemos agregar un TextBox estándar para el "número de vehículo" a la página en nuestro archivo .cshtml (que sea TransportAddView.cshtml):


 @model TransportAddViewModel @Html.TextBoxFor(m => m.AddDTO.Number) 

Esto se procesará en un código HTML como este:


 <input id="AddDTO_Number" name="AddDTO.Number" /> 

Imagine que la parte del controlador con el método de agregar vehículos se ve así (el código de acuerdo con MVC 5, para Core será ligeramente diferente, pero la esencia es la misma ):


 [Route("add"), HttpPost] public ActionResult Add(TransportAddDTO transportAddDto) { //     transportAddDto... } 

Aquí vemos al menos dos problemas:


  1. Los atributos Id y Name tienen el prefijo AddDTO y, si el método de agregar transporte en el controlador utilizando el principio de enlace del modelo intenta enlazar los datos que vinieron del cliente a TransportAddDTO , el objeto dentro consistirá completamente en ceros (valores predeterminados), es decir será solo una nueva instancia vacía. Es lógico: los nombres esperados de la carpeta del número de formulario, no AddDTO_Number .
  2. Todos los meta atributos se han ido, es decir data-val-required y todos los demás que describimos tan cuidadosamente en AddDTO como atributos de validación. Para aquellos que usan todo el poder de Razor, esto es crítico, ya que es una pérdida significativa de información para el frontend.
    Somos afortunados y ellos tienen las decisiones correspondientes.

Estas cosas "funcionan" cuando se usa, por ejemplo, un contenedor para la interfaz de usuario de Kendo (es decir, @Html.Kendo().TextBoxFor() , etc.).


Comencemos con el segundo problema: la razón aquí es que en el Modelo de vista, la instancia de TransportAddDTO transferida era nula . Y la implementación de mecanismos de representación es tal que los atributos en este caso se leen al menos no completamente. La solución, respectivamente, es obvia: primero en el Modelo de vista para inicializar la propiedad TransportAddDTO con una instancia de la clase utilizando el constructor predeterminado. Es mejor hacer esto en un servicio que devuelve un Modelo de vista inicializado, sin embargo, como parte del ejemplo, hará lo mismo:


 class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } = new TransportAddDTO(); public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } } 

Después de estos cambios, el resultado será similar 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" /> 

Ya mejor! Queda por resolver el primer problema: con él, por cierto, todo es algo más complicado.
Para entenderlo, primero debe averiguar qué Razor (implica WebViewPage, una instancia de la cual está disponible dentro de .cshtml) es una propiedad Html a la que nos referimos para llamar a TextBoxFor .
Mirándolo, puede comprender instantáneamente que es del tipo HtmlHelper<T> , en nuestro caso, HtmlHelper<TransportAddViewModel> . Surge una posible solución al problema: crear su propio HtmlHelper dentro y pasarle nuestro TransportAddDTO como entrada. Encontramos el constructor más pequeño posible para una instancia de esta clase:


 HtmlHelper<T>.HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer); 

Podemos pasar ViewContext directamente desde nuestra instancia de this.ViewContext través de this.ViewContext . Ahora veamos dónde obtener una instancia de una clase que implemente la interfaz IViewDataContainer. Por ejemplo, cree su implementación:


 public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } } 

Como puede ver, ahora nos encontramos con la dependencia de algún objeto pasado al constructor con el propósito de inicializar ViewDataDictionary , ya que aquí todo es simple: esta es una instancia de nuestro TransportAddDTO del Modelo de vista. Es decir, puede obtener la instancia apreciada de esta manera:


 var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); 

En consecuencia, tampoco hay problemas para crear un nuevo HtmlHelper:


 var Helper = new HtmlHelper<T>(this.ViewContext, vdc); 

Ahora puede usar lo siguiente:


 @model TransportAddViewModel @{ var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); var Helper = new HtmlHelper<T>(this.ViewContext, vdc); } @Helper.TextBoxFor(m => m.Number) 

Esto se procesará en un código HTML como este:


 <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 puede ver, ahora no hay problemas con el elemento renderizado y está listo para su uso completo. Solo queda "peinar" el código para que se vea menos voluminoso. Por ejemplo, ampliamos nuestro ViewDataContainer de la siguiente manera:


 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); } } 

Entonces desde Razor puedes trabajar así:


 @model TransportAddViewModel @{ var Helper = new ViewDataContainer<TransportAddDTO>(Model.AddDTO).GetHtmlHelper(ViewContext); } @Helper.TextBoxFor(m => m.Number) 

Además, nadie se molesta en extender la implementación estándar de WebViewPage para que contenga la propiedad deseada (con un configurador para una instancia de la clase DTO).


Conclusión


Esto resolvió el problema y también obtuvo la arquitectura del Modelo de vista para trabajar con Razor, que podría contener todos los elementos necesarios.


Vale la pena señalar que el ViewDataContainer resultante resultó ser universal y adecuado para su uso.


Queda por agregar un par de botones a nuestro archivo .cshtml, y la tarea se completará (sin considerar el procesamiento en el backend'e). Esto me propongo hacer por mi cuenta.


Si un lector respetado tiene ideas sobre cómo implementar lo que se necesita de una manera más óptima, con gusto escucharé los comentarios.


Saludos
Peter Osetrov

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


All Articles