ASP.NET Architecture MVC: ViewModel par composition, héritage ou duplication?
J'utilise ASP.NET MVC 3 et Entity Framework 4.1 Code en premier.
Disons que j'ai une entité User
:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
Lors de l'édition dans ma UserController
je veux ajouter un PasswordConfirmation
champ et vérifiez que PasswordConfirmation == Password
1. Par composition
Mon premier essai était :
public class EditUserModel
{
[Required]
public User User { get; set; }
[Compare("User.Password", ErrorMessage = "Passwords don't match.")]
public string PasswordConfirmation { get; set; }
}
Dans ce cas, la validation côté client fonctionne mais (Edit: le travail de validation côté client était une coïncidence.) ne fonctionne pas et le validation côté serveur échoue avec le message suivant: impossible de trouver une propriété nommée User.Mot de passe
Edit: je pense que la meilleure solution, dans ce cas, serait de créer un personnalisé CompareAttribute
La mise en Œuvre de IValidatableObject
public class EditUserModel : IValidatableObject
{
[Required]
public User User { get; set; }
public string PasswordConfirmation { get; set; }
public IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
{
if(this.PasswordConfirmation != this.User.Password)
return new[] { new ValidationResult("Passwords don't match", new[] { "PasswordConfirmation " }) };
return new ValidationResult[0];
}
}
Dans ce cas, la validation côté serveur Fonctionne mais la validation côté client ne fonctionne plus. Implémenter IClientValidatable
semble un peu trop compliqué et je préfère ne pas avoir de validation côté client dans ce cas cas.
2. Par héritage
public class EditUserModel : User
{
[Compare("Password", ErrorMessage = "Passwords don't match.")]
public string PasswordConfirmation { get; set; }
}
Lorsque vous essayez d'enregistrer directement EditUserModel
en utilisant EF, cela ne fonctionne pas, je reçois un message d'erreur sur les métadonnées EditUserModel
, donc j'utilise AutoMapper pour convertir de User
à EditUserModel
et en arrière.
Cette solution Fonctionne mais elle est plus complexe car je dois convertir du modèle au modèle de vue et en arrière.
3. Par duplication
(Suggéré par Malte Clasen)
Le modèle de vue aurait toutes les propriétés du modèle plus les autres. AutoMapper peut être utilisé pour convertir de l'un à l'autre.
public class EditUserModel {
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
[Compare("Password", ErrorMessage = "Passwords don't match.")]
public string ConfirmPassword { get; set; }
}
C'est la solution que j'aime le moins à cause de la duplication de code (DRY)
Questions
Quels sont les avantages et les inconvénients de l'héritage, de la composition et de la duplication dans ce cas ?
Existe-t-il un moyen simple d'avoir une validation côté client et côté serveur sans avoir à convertir le modèle en vue modèle et arrière ?
5 réponses
Ayant lutté avec cette question avant, j'ai dans divers cas allé avec les trois. En général, la plupart des opinions que j'ai vues favorisent la duplication dans un projet MVC, avec un ViewModel construit spécifiquement pour chaque vue. De cette manière, la convention que vous utiliseriez est quelque chose comme UserDetailsViewModel
et UserCreateViewModel
. Comme vous l'avez dit, à ce moment-là, AutoMapper ou un autre outil de mappage automatique serait utilisé pour convertir vos objets de domaine en ces ViewModels plats.
Alors que moi aussi, je n'aime pas en répétant du code, je n'aime pas non plus polluer mes objets de domaine avec la validation ou d'autres attributs spécifiques à la vue. Un autre avantage, bien que presque personne n'ait à faire face (indépendamment de ce que disent tous les pros), est que vous pouvez manipuler vos objets de domaine d'une certaine manière sans nécessairement manipuler vos ViewModels. Je mentionne cela parce qu'il est couramment Cité, pas parce qu'il porte beaucoup de poids pour moi.
Enfin, l'utilisation d'un ViewModel vraiment plat permet de balisage plus propre. Lorsque j'ai utilisé la composition, j'ai souvent fait des erreurs en créant des éléments HTML avec des noms qui sont quelque chose comme User.Address.Street
. Un ViewModel plat réduit au moins ma Probabilité de le faire (je sais, je pourrais toujours utiliser des routines HtmlHelper pour créer des éléments, mais ce n'est pas toujours faisable).
Mes projets récents ont également besoin de ViewModels séparés ces jours-ci de toute façon. Ils ont tous été basés sur NHibernate, et l'utilisation de proxies sur les objets NHibernate ne permet pas de les utiliser directement pour les vues.
Update - Voici un bon article auquel j'ai fait référence dans le passé: http://geekswithblogs.net/michelotti/archive/2009/10/25/asp.net-mvc-view-model-patterns.aspx
Vous pouvez également envisager des classes indépendantes pour les modèles de domaine et de vue, dans ce cas par exemple
public class EditUserModel {
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string ConfirmPassword { get; set; }
}
Si l'Id est stocké dans l'url. Si vous voulez éviter la copie manuelle entre les instances de User et EditorUserModel, AutoMapper peut vous aider. De cette façon, vous pouvez facilement dissocier la chaîne de mot de passe dans votre modèle de vue du hachage de mot de passe dans votre modèle de domaine.
J'ai essayé de résoudre ce problème et j'ai trouvé une solution qui n'implique pas la duplication de code. C'est une sorte de solution de contournement, mais, à mon avis, c'est mieux que les autres solutions proposées.
Vous avez le modèle utilisateur avec toute la validation:
public class UserModel
{
[Required]
public int Id { get; set; }
[Required]
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
}
Vous composez le modèle précédent avec un nouveau modèle
public class EditUserModel
{
public UserModel User { get; set; }
[Required]
public string PasswordConfirmation { get; set; }
}
, L'astuce est dans l'action, vous pourriez recevoir plus d'un modèle:
[HtttPost]
public ActionResult UpdateInformation(UserModel user, EditUserModel editUserModel) {
if (ModelState.IsValid) {
// copy the inner model to the outer model, workaround here:
editUserModel.User = user
// do whatever you want with editUserModel, it has all the needed information
}
}
De cette façon, la validation fonctionne comme prévu.
J'espère que ça aider.
Je n'utilise pas trop de Modèles D'entités, je préfère les modèles LINQ - SQL, donc cela peut être incorrect:
Pourquoi ne pas utiliser une classe de méta-données qui est appliquée à L'entité? Avec LINQ - SQL, les métadonnées attribuées sont prises en compte pour la validation côté client et Côté Serveur.
D'après ce que je comprends, l'application d'un attribut [MetaDataType] est similaire à l'héritage seulement il fonctionne sans implémenter une nouvelle classe (modèle) pour les modifications de la base entité.
En outre, une autre option que vous pourriez essayer est de créer un attribut personnalisé - je l'ai fait une fois dans un but similaire. Essentiellement un drapeau qui indique la persistance d'un membre.
Donc j'aurais une entité définie comme suit:
public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Email { get; set; }
public string Password { get; set; }
[DoNotPersist]
public string ConfirmPassword {get; set;}
}
En outre, Je ne sais pas ce que vous faites pour stocker des données mais j'avais accroché un override dans les fonctions OnInserting , OnEditing, OnDeleting pour mon DataContext qui supprimait fondamentalement tous les membres ayant mon attribut personnalisé.
J'aime cette méthode simple parce que nous utilisons beaucoup de données temporaires, plutôt algorithmiques pour chaque modèle (construire de bonnes interfaces utilisateur pour la Business Intelligence) qui ne sont pas enregistrées dans la base de données mais sont utilisées partout dans les fonctions du modèle, les contrôleurs , etc.
Espérons que cela aide!
PS: - Composition vs héritage - cela dépend vraiment de l'utilisateur cible de l'application. Si c'est pour une application intranet où la sécurité est moins problématique et que l'environnement utilisateur / navigateur est contrôlé, utilisez simplement la validation côté client, c'est-à-dire: composition.
Je préférerais la composition à l'héritage.
Dans le cas de votre mot de passe utilisateur, il semble que vous stockiez réellement le mot de passe dans la table des utilisateurs en texte clair, ce qui est très, très mauvais.
Vous ne devez stocker qu'un hachage salé, et votre EditUserModel
devrait avoir deux propriétés de chaîne pour le mot de passe et la confirmation du mot de passe, qui ne sont pas les champs de votre table.