
Présentation
Bonjour collègues!
Aujourd'hui, je veux partager avec vous mon expérience dans le développement de l'architecture View Model dans le cadre du développement d'applications Web ASP.NET à l'aide du moteur de modèle Razor .
Les implémentations techniques décrites dans cet article conviennent à toutes les versions actuelles d' ASP. NET ( MVC 5 , Core , etc.). L'article lui-même est destiné aux lecteurs qui, au moins, avaient déjà une expérience de travail avec cette pile. Il convient également de noter que dans le cadre de cela, nous ne considérons pas l'utilité du modèle de vue et son application hypothétique (on suppose que le lecteur est déjà familier avec ces choses), nous discutons directement de la mise en œuvre.
Défi
Pour une assimilation pratique et rationnelle du matériel, je propose d'envisager immédiatement la tâche qui nous conduira naturellement à des problèmes potentiels et à leurs solutions optimales.
C'est le problème de l'ajout banal, par exemple, d'une nouvelle voiture à un certain catalogue de véhicules . Afin de ne pas compliquer la tâche abstraite, les détails des aspects restants seront intentionnellement manqués. Il semblerait cependant que la tâche élémentaire soit d'essayer de tout faire en mettant l'accent sur la poursuite de la mise à l'échelle du système (en particulier, l'expansion des modèles par rapport au nombre de propriétés et d'autres composants déterminants) afin que plus tard, il soit aussi confortable de travailler que possible.
Implémentation
Laissez le modèle se présenter comme suit (par souci de simplicité, des éléments tels que les propriétés de navigation , etc. ne sont pas indiqués):
class Transport { public int Id { get; set; } public int TransportTypeId { get; set; } public string Number { get; set; } }
Bien sûr, TransportTypeId est une clé étrangère vers un objet de type TransportType :
class TransportType { public int Id { get; set; } public string Name { get; set; } }
Pour la connexion entre le frontend et le backend, nous utiliserons le modèle Data Transfer Object . En conséquence, le DTO pour l'ajout d'une voiture ressemblera à ceci:
class TransportAddDTO { [Required] public int TransportTypeId { get; set; } [Required] [MaxLength(10)] public string Number { get; set; } }
* Utilise les attributs de validation standard de System.ComponentModel.DataAnnotations
.
Il est temps de comprendre ce que sera le modèle d'affichage pour la page d'ajout de voiture. Certains développeurs annonceraient volontiers que TransportAddDTO lui-même serait tel, cependant, c'est fondamentalement faux, car rien ne peut être «bourré» dans cette classe, sauf directement pour les informations de back-end nécessaires pour ajouter un nouvel élément (par définition). De plus, d'autres données peuvent être requises sur la page d'ajout: par exemple, un répertoire des types de véhicules (sur la base duquel TransportTypeId est ensuite exprimé). À cet égard, le modèle de vue suivant se suggère:
class TransportAddViewModel { public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Où TransportTypeDTO dans ce cas sera un mappage direct de TransportType (et c'est loin d'être toujours le cas - à la fois dans le sens de la troncature et dans le sens de l'expansion):
class TransportTypeDTO { public int Id { get; set; } public string Name { get; set; } }
À ce stade, la question raisonnable se pose: dans Razor, il ne sera possible de transférer qu'un seul modèle (et Dieu merci), comment alors TransportAddDTO peut-il être utilisé pour générer du code HTML à l'intérieur de cette page?
Très simple! Il suffit d'ajouter, en particulier, ce DTO au View Model , quelque chose comme ceci:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Maintenant, les premiers problèmes commencent. Essayons d'ajouter un TextBox standard pour le "numéro de véhicule" à la page de notre fichier .cshtml (que ce soit TransportAddView.cshtml):
@model TransportAddViewModel @Html.TextBoxFor(m => m.AddDTO.Number)
Cela rendra en code HTML comme ceci:
<input id="AddDTO_Number" name="AddDTO.Number" />
Imaginez que la partie du contrôleur avec la méthode d'ajout de véhicules ressemble à ceci (le code conformément à MVC 5, pour Core, il sera légèrement différent, mais l'essence est la même ):
[Route("add"), HttpPost] public ActionResult Add(TransportAddDTO transportAddDto) {
Ici, nous voyons au moins deux problèmes:
- Les attributs Id et Name ont le préfixe AddDTO et, par la suite, si la méthode d'ajout de transport dans le contrôleur à l'aide du principe de liaison de modèle tente de lier les données provenant du client à TransportAddDTO , alors l'objet à l'intérieur sera entièrement composé de zéros (valeurs par défaut), c'est-à-dire ce sera juste une nouvelle instance vide. C'est logique - le classeur attendait les noms du formulaire Number , pas AddDTO_Number .
- Tous les méta-attributs ont disparu, c'est-à-dire data-val-required et tous les autres que nous avons si soigneusement décrits dans AddDTO comme attributs de validation. Pour ceux qui utilisent toute la puissance de Razor, cela est essentiel, car il s'agit d'une perte importante d'informations pour le frontend.
Nous avons de la chance et ils ont des décisions correspondantes.
Ces choses "fonctionnent" lors de l'utilisation, par exemple, d'un wrapper pour l'interface utilisateur de Kendo (c'est- @Html.Kendo().TextBoxFor()
dire @Html.Kendo().TextBoxFor()
, etc.).
Commençons par le deuxième problème: la raison ici est que dans le modèle d'affichage, l' instance TransportAddDTO transférée était nulle . Et l'implémentation des mécanismes de rendu est telle que les attributs dans ce cas ne sont pas lus au moins pas complètement. La solution, respectivement, est évidente - d'abord dans le modèle de vue pour initialiser la propriété TransportAddDTO avec une instance de la classe à l'aide du constructeur par défaut. Il est préférable de le faire dans un service qui renvoie un modèle de vue initialisé, cependant, dans le cadre de l'exemple, il fera de même:
class TransportAddViewModel { public TransportAddDTO AddDTO { get; set; } = new TransportAddDTO(); public IEnumerable<TransportTypeDTO> TransportTypes { get; set; } }
Après ces modifications, le résultat sera similaire à:
<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" />
Déjà mieux! Il reste à régler le premier problème - avec lui, soit dit en passant, tout est un peu plus compliqué.
Pour le comprendre, vous devez d'abord comprendre ce que Razor (implique WebViewPage, dont une instance à l'intérieur de .cshtml est disponible comme ceci ) est une propriété Html à laquelle nous nous référons afin d'appeler TextBoxFor
.
En le regardant, vous pouvez instantanément comprendre qu'il est de type HtmlHelper<T>
, dans notre cas, HtmlHelper<TransportAddViewModel>
. Une solution possible au problème se pose - pour créer votre propre HtmlHelper à l' intérieur, et lui passer notre TransportAddDTO en entrée. Nous trouvons le plus petit constructeur possible pour une instance de cette classe:
HtmlHelper<T>.HtmlHelper(ViewContext viewContext, IViewDataContainer viewDataContainer);
Nous pouvons transmettre ViewContext directement à partir de notre instance this.ViewContext
via this.ViewContext
. Voyons maintenant où trouver une instance d'une classe qui implémente l'interface IViewDataContainer. Par exemple, créez votre implémentation:
public class ViewDataContainer<T> : IViewDataContainer where T : class { public ViewDataDictionary ViewData { get; set; } public ViewDataContainer(object model) { ViewData = new ViewDataDictionary(model); } }
Comme vous pouvez le voir, nous dépendons maintenant d'un objet passé au constructeur dans le but d'initialiser le ViewDataDictionary , car tout est simple ici - il s'agit d'une instance de notre TransportAddDTO du View Model. Autrement dit, vous pouvez obtenir l'instance chérie comme ceci:
var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO);
En conséquence, il n'y a aucun problème à créer un nouveau HtmlHelper:
var Helper = new HtmlHelper<T>(this.ViewContext, vdc);
Vous pouvez maintenant utiliser les éléments suivants:
@model TransportAddViewModel @{ var vdc = new ViewDataContainer<TransportAddDTO>(Model.AddDTO); var Helper = new HtmlHelper<T>(this.ViewContext, vdc); } @Helper.TextBoxFor(m => m.Number)
Cela rendra en code HTML comme ceci:
<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" />
Comme vous pouvez le voir, il n'y a plus de problème avec l'élément rendu et il est prêt à être utilisé. Il ne reste plus qu'à "peigner" le code pour qu'il ait l'air moins volumineux. Par exemple, nous étendons notre ViewDataContainer comme suit:
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); } }
Ensuite, depuis Razor, vous pouvez travailler comme ceci:
@model TransportAddViewModel @{ var Helper = new ViewDataContainer<TransportAddDTO>(Model.AddDTO).GetHtmlHelper(ViewContext); } @Helper.TextBoxFor(m => m.Number)
De plus, personne ne dérange pour étendre l'implémentation standard de WebViewPage afin qu'elle contienne la propriété souhaitée (avec un setter pour une instance de la classe DTO).
Conclusion
Cela a résolu le problème et a également obtenu l'architecture View Model pour travailler avec Razor, qui pourrait potentiellement contenir tous les éléments nécessaires.
Il convient de noter que le ViewDataContainer résultant s'est avéré être universel et adapté à l'utilisation.
Il reste à ajouter quelques boutons à notre fichier .cshtml, et la tâche sera terminée (sans envisager le traitement sur le backend'e). Je propose de le faire par moi-même.
Si un lecteur respecté a des idées sur la façon de mettre en œuvre ce qui est nécessaire de manière plus optimale, je serai heureux d'écouter les commentaires.
Cordialement
Peter Osetrov