Comment mapper le modèle de vue au modèle de domaine dans une action POST?

87

Chaque article trouvé sur Internet sur l'utilisation de ViewModels et l'utilisation d'Automapper donne les directives de la cartographie de direction "Controller -> View". Vous prenez un modèle de domaine avec toutes les listes de sélection dans un ViewModel spécialisé et le transmettez à la vue. C'est clair et bien.
La vue a une forme, et finalement nous sommes dans l'action POST. Ici, tous les Model Binders arrivent avec [évidemment] un autre View Model qui est [évidemment] lié au ViewModel original au moins dans la partie des conventions de dénomination pour des raisons de liaison et de validation.

Comment le mappez-vous à votre modèle de domaine?

Que ce soit une action d'insertion, nous pourrions utiliser le même Automapper. Mais que faire s'il s'agissait d'une action de mise à jour? Nous devons récupérer notre entité de domaine à partir du référentiel, mettre à jour ses propriétés en fonction des valeurs du ViewModel et enregistrer dans le référentiel.

ADDENDA 1 (9 février 2010): Parfois, l'attribution des propriétés du modèle ne suffit pas. Des mesures doivent être prises contre le modèle de domaine en fonction des valeurs de View Model. Par exemple, certaines méthodes doivent être appelées sur le modèle de domaine. Probablement, il devrait y avoir une sorte de couche de service d'application qui se situe entre le contrôleur et le domaine afin de traiter les modèles de vue ...


Comment organiser ce code et où le placer pour atteindre les objectifs suivants?

  • garder les contrôleurs minces
  • honorer la pratique SoC
  • suivre les principes de conception pilotée par domaine
  • être SEC
  • à suivre ...
Anthony Serdyukov
la source

Réponses:

37

J'utilise une interface IBuilder et je l'implémente en utilisant ValueInjecter

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

... (implémentation) RebuildViewModel appelle simplementBuildViewModel(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 je n'écris pas ViewModel J'écris Input car c'est beaucoup plus court, mais ce n'est pas vraiment important
j'espère que cela aide

Mise à jour: J'utilise cette approche maintenant dans l' application de démonstration ProDinner ASP.net MVC , elle s'appelle IMapper maintenant, il y a aussi un pdf fourni où cette approche est expliquée en détail

Omu
la source
J'aime cette approche. Une chose sur laquelle je ne suis pas clair est la mise en œuvre d'IBuilder, en particulier à la lumière d'une application à plusieurs niveaux. Par exemple, mon ViewModel a 3 SelectLists. Comment l'implémentation du générateur récupère-t-elle les valeurs de la liste de sélection à partir du référentiel?
Matt Murrell
@Matt Murrell regarde prodinner.codeplex.com Je fais ça là-dedans, et je l'appelle IMapper là-bas au lieu d'IBuilder
Omu
6
J'aime cette approche, j'en ai implémenté un échantillon ici: gist.github.com/2379583
Paul Stovell
À mon avis, il n'est pas conforme à l'approche du modèle de domaine. Cela ressemble à une approche CRUD pour des exigences peu claires. Ne devrions-nous pas utiliser les usines (DDD) et les méthodes associées dans le modèle de domaine pour véhiculer une action raisonnable? De cette façon, nous ferions mieux de charger une entité à partir de la base de données et de la mettre à jour si nécessaire, non? Il semble donc que ce n'est pas tout à fait correct.
Artyom
7

Des outils comme AutoMapper peuvent être utilisés pour mettre à jour un objet existant avec les données de l'objet source. L'action du contrôleur pour la mise à jour peut ressembler à ceci:

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

En dehors de ce qui est visible dans l'extrait ci-dessus:

  • Les données POST pour afficher le modèle + la validation sont effectuées dans ModelBinder (peuvent être étendues avec des liaisons personnalisées)
  • La gestion des erreurs (c'est-à-dire la capture des exceptions d'accès aux données lancées par le référentiel) peut être effectuée par le filtre [HandleError]

L'action du contrôleur est assez mince et les préoccupations sont séparées: les problèmes de mappage sont résolus dans la configuration d'AutoMapper, la validation est effectuée par ModelBinder et l'accès aux données par le référentiel.

PanJanek
la source
6
Je ne suis pas sûr qu'Automapper soit utile ici car il ne peut pas inverser l'aplatissement. Après tout, Domain Model n'est pas un simple DTO comme View Model, il ne suffit donc pas de lui attribuer des propriétés. Il est probable que certaines actions doivent être effectuées sur le modèle de domaine en fonction du contenu de View Model. Cependant, +1 pour partager une assez bonne approche.
Anthony Serdyukov
@Anton ValueInjecter peut inverser l'aplatissement;)
Omu
avec cette approche, vous ne gardez pas le contrôleur fin, vous violez SoC et DRY ... comme Omu l'a mentionné, vous devriez avoir une couche séparée qui s'occupe du mappage.
Rookian
5

Je voudrais dire que vous réutilisez le terme ViewModel pour les deux sens de l'interaction client. Si vous avez lu suffisamment de code ASP.NET MVC dans la nature, vous avez probablement vu la distinction entre un ViewModel et un EditModel. Je pense que c'est important.

Un ViewModel représente toutes les informations requises pour rendre une vue. Cela peut inclure des données qui sont rendues dans des endroits statiques non interactifs et également des données purement pour effectuer une vérification pour décider de ce qu'il faut exactement rendre. Une action Controller GET est généralement responsable de l'empaquetage du ViewModel pour sa vue.

Un EditModel (ou peut-être un ActionModel) représente les données requises pour effectuer l'action que l'utilisateur voulait faire pour ce POST. Donc, un EditModel essaie vraiment de décrire une action. Cela exclura probablement certaines données du ViewModel et bien que liées, je pense qu'il est important de réaliser qu'elles sont en effet différentes.

Une idée

Cela dit, vous pourriez très facilement avoir une configuration AutoMapper pour aller de Model -> ViewModel et une autre pour aller de EditModel -> Model. Ensuite, les différentes actions du contrôleur doivent simplement utiliser AutoMapper. Enfer, l'EditModel pourrait avoir une fonction pour valider ses propriétés par rapport au modèle et appliquer ces valeurs au modèle lui-même. Cela ne fait rien d'autre et vous avez de toute façon ModelBinders dans MVC pour mapper la demande à EditModel.

Une autre idée

Au-delà de cela, quelque chose auquel j'ai pensé récemment et qui fonctionne en quelque sorte sur l'idée d'un ActionModel est que ce que le client vous renvoie est en fait la description de plusieurs actions que l'utilisateur a effectuées et pas seulement un gros glob de données. Cela nécessiterait certainement du Javascript côté client pour gérer mais l'idée est intrigante je pense.

Essentiellement au fur et à mesure que l'utilisateur effectue des actions sur l'écran que vous avez présenté, Javascript commence à créer une liste d'objets d'action. Un exemple est peut-être que l'utilisateur se trouve sur un écran d'informations sur les employés. Ils mettent à jour le nom de famille et ajoutent une nouvelle adresse parce que l'employé s'est récemment marié. Sous les couvertures cela produit unChangeEmployeeName et un AddEmployeeMailingAddressobjets dans une liste. L'utilisateur clique sur «Enregistrer» pour valider les modifications et vous soumettez la liste de deux objets, chacun contenant uniquement les informations nécessaires pour effectuer chaque action.

Vous auriez besoin d'un ModelBinder plus intelligent que celui par défaut, mais un bon sérialiseur JSON devrait être en mesure de prendre en charge le mappage des objets d'action côté client vers ceux côté serveur. Ceux du côté serveur (si vous êtes dans un environnement à 2 niveaux) pourraient facilement avoir des méthodes qui ont terminé l'action sur le modèle avec lequel ils travaillent. Ainsi, l'action Controller finit par obtenir simplement un identifiant pour l'instance de modèle à extraire et une liste d'actions à effectuer dessus. Ou les actions ont l'identifiant en elles pour les garder très séparées.

Alors peut-être que quelque chose comme ça se réalise du côté serveur:

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
}

Cela rend vraiment l'action de publication assez générique puisque vous comptez sur votre ModelBinder pour vous obtenir la bonne instance IUserAction et votre instance IUserAction pour exécuter la logique correcte elle-même ou (plus probablement) appeler le modèle avec les informations.

Si vous étiez dans un environnement à 3 niveaux, l'IUserAction pourrait simplement être de simples DTO à tirer à travers la frontière et exécutés de manière similaire sur la couche d'application. Selon la façon dont vous faites cette couche, elle pourrait être divisée très facilement et rester dans une transaction (ce qui vient à l'esprit est la demande / réponse d'Agatha et tirer parti de la carte d'identité de DI et NHibernate).

Quoi qu'il en soit, je suis sûr que ce n'est pas une idée parfaite, cela nécessiterait un peu de JS côté client pour le gérer, et je n'ai pas encore été en mesure de faire un projet pour voir comment il se déroule, mais l'article essayait de réfléchir à la façon de le faire. aller et revenir encore alors j'ai pensé que je donnerais mes pensées. J'espère que cela aide et j'aimerais entendre d'autres façons de gérer les interactions.

Sean Copenhague
la source
Intéressant. En ce qui concerne la distinction entre ViewModel et EditModel ... suggérez-vous que pour une fonction d'édition, vous utiliseriez un ViewModel pour créer le formulaire, puis vous lieriez à un EditModel lorsque l'utilisateur l'a posté? Si tel est le cas, comment gérez-vous les situations où vous auriez besoin de republier le formulaire en raison d'erreurs de validation (par exemple lorsque le ViewModel contient des éléments pour remplir une liste déroulante) - incluriez-vous simplement les éléments déroulants dans EditModel également? Dans quel cas, quelle serait la différence entre les deux?
UpTheCreek
Je suppose que votre préoccupation est que si j'utilise un EditModel et qu'il y a une erreur, je dois reconstruire mon ViewModel qui pourrait être très coûteux. Je dirais qu'il suffit de reconstruire le ViewModel et de s'assurer qu'il a un endroit pour mettre les messages de notification utilisateur (probablement à la fois positifs et négatifs tels que les erreurs de validation). Si cela s'avère être un problème de performances, vous pouvez toujours mettre en cache le ViewModel jusqu'à ce que la prochaine requête de cette session se termine (probablement la publication du EditModel).
Seanenhaver