Meilleures pratiques ViewModel


238

À partir de cette question , il semble logique qu'un contrôleur crée un ViewModel qui reflète plus précisément le modèle que la vue essaie d'afficher, mais je suis curieux de connaître certaines des conventions (je suis nouveau dans le modèle MVC , si ce n'était pas déjà évident).

Fondamentalement, j'avais les questions suivantes:

  1. J'aime normalement avoir une classe / fichier. Cela a-t-il un sens avec un ViewModel s'il est uniquement créé pour transférer des données d'un contrôleur vers une vue?
  2. Si un ViewModel appartient à son propre fichier et que vous utilisez une structure de répertoire / projet pour garder les choses séparées, à quoi appartient le fichier ViewModel ? Dans le répertoire des contrôleurs ?

C'est essentiellement ça pour l'instant. J'ai peut-être quelques questions à venir, mais cela me dérange depuis une heure environ, et je peux trouver des conseils cohérents ailleurs.

EDIT: En regardant l'exemple d' application NerdDinner sur CodePlex, il semble que les ViewModels fassent partie des contrôleurs , mais cela me met encore mal à l'aise de ne pas être dans leurs propres fichiers.


66
Je n'appellerais pas exactement NerdDinner un exemple de "meilleures pratiques". Votre intuition vous sert bien. :)
Ryan Montgomery

Réponses:


211

Je crée ce que j'appelle un "ViewModel" pour chaque vue. Je les place dans un dossier appelé ViewModels dans mon projet Web MVC. Je les nomme d'après le contrôleur et l'action (ou la vue) qu'ils représentent. Donc, si je dois transmettre des données à la vue SignUp sur le contrôleur d'appartenance, je crée une classe MembershipSignUpViewModel.cs et la place dans le dossier ViewModels.

J'ajoute ensuite les propriétés et méthodes nécessaires pour faciliter le transfert des données du contrôleur vers la vue. J'utilise Automapper pour passer de mon ViewModel au modèle de domaine et revenir si nécessaire.

Cela fonctionne également bien pour les ViewModels composites qui contiennent des propriétés qui sont du type d'autres ViewModels. Par exemple, si vous avez 5 widgets sur la page d'index dans le contrôleur d'appartenance et que vous avez créé un ViewModel pour chaque vue partielle - comment passez-vous les données de l'action Index aux partiels? Vous ajoutez une propriété à MembershipIndexViewModel de type MyPartialViewModel et lors du rendu du partiel, vous passeriez dans Model.MyPartialViewModel.

Le faire de cette façon vous permet d'ajuster les propriétés partielles de ViewModel sans avoir à changer du tout la vue Index. Il passe toujours dans Model.MyPartialViewModel, il y a donc moins de chance que vous deviez parcourir toute la chaîne des partiels pour corriger quelque chose lorsque tout ce que vous faites est d'ajouter une propriété au ViewModel partiel.

J'ajouterai également l'espace de noms "MyProject.Web.ViewModels" au web.config afin de me permettre de les référencer dans n'importe quelle vue sans jamais ajouter une déclaration d'importation explicite sur chaque vue. Le rend juste un peu plus propre.


3
Que faire si vous souhaitez POSTER à partir d'une vue partielle et renvoyer la vue entière (en cas d'erreur de modèle)? Dans la vue partielle, vous n'avez pas accès au modèle parent.
Cosmo

5
@Cosmo: puis POSTEZ sur une action qui peut renvoyer la vue entière en cas d'erreur de modèle. Côté serveur, vous en avez assez pour recréer le modèle parent.
Tomas Aschan

Qu'en est-il des actions de connexion [POST] et de connexion [GET]? avec différents modèles de vue?
Bart Calixto

Habituellement, la connexion [GET] n'appelle pas ViewModel car elle n'a pas besoin de charger de données.
Andre Figueiredo

Très bon conseil. Où doit aller l'accès aux données, le traitement et la définition des propriétés du modèle / VM? Dans mon cas, nous aurons des données provenant d'une base de données CMS locale et d'autres provenant de services Web, qui devront être traitées / manipulées avant d'être définies sur un modèle. Mettre tout cela dans le contrôleur devient assez compliqué.
xr280xr

124

Séparer les classes par catégorie (contrôleurs, modèles d'affichage, filtres, etc.) est un non-sens.

Si vous souhaitez écrire du code pour la section Accueil de votre site Web (/), créez un dossier nommé Home et placez-y le HomeController, IndexViewModel, AboutViewModel, etc. et toutes les classes connexes utilisées par les actions Home.

Si vous avez partagé des classes, comme un ApplicationController, vous pouvez le mettre à la racine de votre projet.

Pourquoi séparer les choses qui sont liées (HomeController, IndexViewModel) et garder les choses ensemble qui n'ont aucune relation (HomeController, AccountController)?


J'ai écrit un article de blog sur ce sujet.


13
Les choses vont devenir assez désordonnées assez rapidement si vous faites cela.
UpTheCreek

14
Non, le désordre est de mettre tous les contrôleurs dans un seul espace dir / names. Si vous avez 5 contrôleurs, chacun utilisant 5 modèles de vue, alors vous avez 25 modèles de vue. Les espaces de noms sont le mécanisme d'organisation du code et ne devraient pas être différents ici.
Max Toro

41
@Max Toro: surpris que vous ayez tant voté. Après un certain temps de travail sur ASP.Net MVC, je ressens beaucoup de peine à avoir tous les ViewModels en un seul endroit, tous les contrôleurs dans un autre et toutes les vues dans un autre encore. MVC est un trio de pièces liées, elles sont couplées - elles se soutiennent mutuellement. J'ai l'impression qu'une solution peut être beaucoup plus organisée si le Controller, les ViewModels et les vues d'une section donnée cohabitent dans le même répertoire. MyApp / Accounts / Controller.cs, MyApp / Accounts / Create / ViewModel.cs, MyApp / Accounts / Create / View.cshtml, etc.
quentin-starin

13
@RyanJMcGowan la séparation des préoccupations n'est pas la séparation des classes.
Max Toro

12
@RyanJMcGowan, peu importe la façon dont vous abordez le développement, le problème est ce que vous vous retrouvez, spécialement pour les grandes applications. Une fois en mode maintenance vous ne pensez pas à tous les modèles puis à tous les contrôleurs, vous ajoutez une fonction à la fois.
Max Toro

21

Je garde mes classes d'application dans un sous-dossier appelé "Core" (ou une bibliothèque de classes séparée) et j'utilise les mêmes méthodes que l' exemple d'application KIGG mais avec quelques légères modifications pour rendre mes applications plus SÈCHES.

Je crée une classe BaseViewData dans / Core / ViewData / où je stocke les propriétés communes à l'ensemble du site.

Après cela, je crée également toutes mes classes ViewData dans le même dossier qui dérivent ensuite de BaseViewData et ont des propriétés spécifiques à la vue.

Ensuite, je crée un ApplicationController dont tous mes contrôleurs dérivent. ApplicationController possède une méthode GetViewData générique comme suit:

protected T GetViewData<T>() where T : BaseViewData, new()
    {
        var viewData = new T
        {
           Property1 = "value1",
           Property2 = this.Method() // in the ApplicationController
        };
        return viewData;
    }

Enfin, dans mon action de contrôleur, je fais ce qui suit pour construire mon modèle ViewData

public ActionResult Index(int? id)
    {
        var viewData = this.GetViewData<PageViewData>();
        viewData.Page = this.DataContext.getPage(id); // ApplicationController
        ViewData.Model = viewData;
        return View();
    }

Je pense que cela fonctionne vraiment bien et qu'il garde vos vues bien rangées et vos contrôleurs maigres.


13

Une classe ViewModel est là pour encapsuler plusieurs éléments de données représentés par des instances de classes en un seul objet facile à gérer que vous pouvez transmettre à votre vue.

Il serait logique d'avoir vos classes ViewModel dans leurs propres fichiers, dans leur propre répertoire. Dans mes projets, j'ai un sous-dossier du dossier Modèles appelé ViewModels. C'est là que mes ViewModels (par exemple ProductViewModel.cs) vivent.


13

Il n'y a pas de bon endroit pour conserver vos modèles. Vous pouvez les conserver dans un assemblage séparé si le projet est grand et qu'il y a beaucoup de ViewModels (Data Transfer Objects). Vous pouvez également les conserver dans un dossier séparé du projet de site. Par exemple, dans Oxite, ils sont placés dans le projet Oxite qui contient également de nombreuses classes différentes. Les contrôleurs d'Oxite sont déplacés vers un projet séparé et les vues sont également dans un projet distinct.
Dans CodeCampServer, les ViewModels sont nommés * Form et sont placés dans le projet UI dans le dossier Models.
Dans le projet MvcPress , ils sont placés dans le projet Data, qui contient également tout le code pour travailler avec la base de données et un peu plus (mais je n'ai pas recommandé cette approche, c'est juste pour un échantillon)
Vous pouvez donc voir qu'il existe de nombreux points de vue. Je garde généralement mes ViewModels (objets DTO) dans le projet de site. Mais lorsque j'ai plus de 10 modèles, je préfère les déplacer vers un assemblage séparé. Habituellement, dans ce cas, je déplace également les contrôleurs vers un assemblage séparé.
Une autre question est de savoir comment mapper facilement toutes les données du modèle vers votre ViewModel. Je suggère de jeter un œil à la bibliothèque AutoMapper . Je l'aime beaucoup, ça fait tout un sale boulot pour moi.
Et moi aussi je suggère de regarder le projet SharpArchitecture . Il fournit une très bonne architecture pour les projets et il contient beaucoup de cadres et de guides sympas et une grande communauté.


8
ViewModels! = DTO
Bart Calixto

6

voici un extrait de code de mes meilleures pratiques:

    public class UserController : Controller
    {
        private readonly IUserService userService;
        private readonly IBuilder<User, UserCreateInput> createBuilder;
        private readonly IBuilder<User, UserEditInput> editBuilder;

        public UserController(IUserService userService, IBuilder<User, UserCreateInput> createBuilder, IBuilder<User, UserEditInput> editBuilder)
        {
            this.userService = userService;
            this.editBuilder = editBuilder;
            this.createBuilder = createBuilder;
        }

        public ActionResult Index(int? page)
        {
            return View(userService.GetPage(page ?? 1, 5));
        }

        public ActionResult Create()
        {
            return View(createBuilder.BuildInput(new User()));
        }

        [HttpPost]
        public ActionResult Create(UserCreateInput input)
        {
            if (input.Roles == null) ModelState.AddModelError("roles", "selectati macar un rol");

            if (!ModelState.IsValid)
                return View(createBuilder.RebuildInput(input));

            userService.Create(createBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }

        public ActionResult Edit(long id)
        {
            return View(editBuilder.BuildInput(userService.GetFull(id)));
        }

        [HttpPost]
        public ActionResult Edit(UserEditInput input)
        {           
            if (!ModelState.IsValid)
                return View(editBuilder.RebuildInput(input));

            userService.Save(editBuilder.BuilEntity(input));
            return RedirectToAction("Index");
        }
}

5

Nous jetons tous nos ViewModels dans le dossier Modèles (toute notre logique métier est dans un projet ServiceLayer distinct)


4

Personnellement, je suggère que si le ViewModel est tout sauf trivial, utilisez une classe distincte.

Si vous avez plus d'un modèle de vue, je suggère qu'il soit judicieux de le partitionner dans au moins un répertoire. si le modèle de vue est ultérieurement partagé, l'espace de nom impliqué dans le répertoire facilite le déplacement vers un nouvel assembly.


2

Dans notre cas, nous avons les modèles avec les contrôleurs dans un projet distinct des vues.

En règle générale, nous avons essayé de déplacer et d'éviter la plupart des éléments ViewData ["..."] vers le ViewModel afin d'éviter les castings et les chaînes magiques, ce qui est une bonne chose.

Le ViewModel contient également certaines propriétés communes telles que les informations de pagination pour les listes ou les informations d'en-tête de la page pour dessiner les fils d'Ariane et les titres. En ce moment, la classe de base contient trop d'informations à mon avis et nous pouvons la diviser en trois parties, les informations les plus élémentaires et nécessaires pour 99% des pages sur un modèle de vue de base, puis un modèle pour les listes et un modèle pour les formulaires qui contiennent des données spécifiques pour ces scénarios et héritent de celui de base.

Enfin, nous implémentons un modèle de vue pour chaque entité afin de traiter les informations spécifiques.


0

code dans le contrôleur:

    [HttpGet]
        public ActionResult EntryEdit(int? entryId)
        {
            ViewData["BodyClass"] = "page-entryEdit";
            EntryEditViewModel viewMode = new EntryEditViewModel(entryId);
            return View(viewMode);
        }

    [HttpPost]
    public ActionResult EntryEdit(Entry entry)
    {
        ViewData["BodyClass"] = "page-entryEdit";            

        #region save

        if (ModelState.IsValid)
        {
            if (EntryManager.Update(entry) == 1)
            {
                return RedirectToAction("EntryEditSuccess", "Dictionary");
            }
            else
            {
                return RedirectToAction("EntryEditFailed", "Dictionary");
            }
        }
        else
        {
            EntryEditViewModel viewModel = new EntryEditViewModel(entry);
            return View(viewModel);
        }

        #endregion
    }

code dans le modèle de vue:

public class EntryEditViewModel
    {
        #region Private Variables for Properties

        private Entry _entry = new Entry();
        private StatusList _statusList = new StatusList();        

        #endregion

        #region Public Properties

        public Entry Entry
        {
            get { return _entry; }
            set { _entry = value; }
        }

        public StatusList StatusList
        {
            get { return _statusList; }
        }

        #endregion

        #region constructor(s)

        /// <summary>
        /// for Get action
        /// </summary>
        /// <param name="entryId"></param>
        public EntryEditViewModel(int? entryId)
        {
            this.Entry = EntryManager.GetDetail(entryId.Value);                 
        }

        /// <summary>
        /// for Post action
        /// </summary>
        /// <param name="entry"></param>
        public EntryEditViewModel(Entry entry)
        {
            this.Entry = entry;
        }

        #endregion       
    }

projets:

  • DevJet.Web (le projet Web ASP.NET MVC)

  • DevJet.Web.App.Dictionary (un projet de bibliothèque de classes distinct)

    dans ce projet, j'ai fait des dossiers comme: DAL, BLL, BO, VM (dossier pour voir les modèles)


Bonjour, pouvez-vous partager quelle est la structure de la classe Entry?
Dinis Cruz

0

Créez une classe de base de modèle de vue qui possède des propriétés généralement requises comme le résultat de l'opération et des données contextuelles, vous pouvez également mettre les données et les rôles utilisateur actuels

class ViewModelBase 
{
  public bool HasError {get;set;} 
  public string ErrorMessage {get;set;}
  public List<string> UserRoles{get;set;}
}

Dans la classe de contrôleur de base, une méthode telle que PopulateViewModelBase () cette méthode remplira les données contextuelles et les rôles d'utilisateur. HasError et ErrorMessage, définissez ces propriétés s'il y a exception lors de l'extraction des données de service / db. Liez ces propriétés en vue pour afficher l'erreur. Les rôles utilisateur peuvent être utilisés pour afficher la section de masquage sur la vue en fonction des rôles.

Pour remplir les modèles de vue dans différentes actions get, il peut être rendu cohérent en ayant un contrôleur de base avec la méthode abstraite FillModel

class BaseController :BaseController 
{
   public PopulateViewModelBase(ViewModelBase model) 
{
   //fill up common data. 
}
abstract ViewModelBase FillModel();
}

Dans les contrôleurs

class MyController :Controller 
{

 public ActionResult Index() 
{
   return View(FillModel()); 
}

ViewModelBase FillModel() 
{ 
    ViewModelBase  model=;
    string currentAction = HttpContext.Current.Request.RequestContext.RouteData.Values["action"].ToString(); 
 try 
{ 
   switch(currentAction) 
{  
   case "Index": 
   model= GetCustomerData(); 
   break;
   // fill model logic for other actions 
}
}
catch(Exception ex) 
{
   model.HasError=true;
   model.ErrorMessage=ex.Message;
}
//fill common properties 
base.PopulateViewModelBase(model);
return model;
}
}
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.