Comment valider mon modèle dans un classeur de modèles personnalisé?

J’ai posé une question sur un problème que j’ai avec les valeurs numériques délimitées par des virgules ici .

Compte tenu de certaines des réponses, j’ai tenté d’implémenter mon propre classeur:

namespace MvcApplication1.Core { public class PropertyModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { object objectModel = new object(); if (bindingContext.ModelType == typeof(PropertyModel)) { HttpRequestBase request = controllerContext.HttpContext.Request; ssortingng price = request.Form.Get("Price").Replace(",", ssortingng.Empty); ModelBindingContext newBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( () => new PropertyModel() { Price = Convert.ToInt32(price) }, typeof(PropertyModel) ), ModelState = bindingContext.ModelState, ValueProvider = bindingContext.ValueProvider }; // call the default model binder this new binding context return base.BindModel(controllerContext, newBindingContext); } else { return base.BindModel(controllerContext, bindingContext); } } //protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) //{ // //return base.CreateModel(controllerContext, bindingContext, modelType); // PropertyModel model = new PropertyModel(); // if (modelType == typeof(PropertyModel)) // { // model = (PropertyModel)base.CreateModel(controllerContext, bindingContext, modelType); // HttpRequestBase request = controllerContext.HttpContext.Request; // ssortingng price = request.Form.Get("Price").Replace(",", ssortingng.Empty); // model.Price = Convert.ToInt32(price); // } // return model; //} } } 

Et mis à jour ma classe de contrôleur comme ceci:

 namespace MvcApplication1.Controllers { public class PropertyController : Controller { public ActionResult Edit() { PropertyModel model = new PropertyModel { AgentName = "John Doe", BuildingStyle = "Colonial", BuiltYear = 1978, Price = 650000, Id = 1 }; return View(model); } [HttpPost] public ActionResult Edit([ModelBinder(typeof(PropertyModelBinder))] PropertyModel model) { if (ModelState.IsValid) { //Save property info. } return View(model); } public ActionResult About() { ViewBag.Message = "Your app description page."; return View(); } public ActionResult Contact() { ViewBag.Message = "Your contact page."; return View(); } } } 

Désormais, si j’entre le prix avec des virgules, mon classeur de modèles personnalisé supprimera les virgules. C’est ce que je veux, mais la validation échoue toujours. La question qui se pose est donc la suivante: comment effectuer une validation personnalisée dans mon classeur de modèle personnalisé de manière à éviter la valeur de prix capturée avec des virgules ? En d’autres termes, je soupçonne que je dois en faire plus dans mon classeur de modèles personnalisé, mais je ne sais pas comment et quoi. Merci. Ouvrez la capture d'écran dans un nouvel onglet pour une meilleure vue.

Mettre à jour:

J’ai donc essayé la solution de @ mare à l’ adresse https://stackoverflow.com/a/2592430/97109 et mis à jour le classeur de modèle comme suit:

 namespace MvcApplication1.Core { public class PropertyModelBinder : DefaultModelBinder { public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) { object objectModel = new object(); if (bindingContext.ModelType == typeof(PropertyModel)) { HttpRequestBase request = controllerContext.HttpContext.Request; ssortingng price = request.Form.Get("Price").Replace(",", ssortingng.Empty); ModelBindingContext newBindingContext = new ModelBindingContext() { ModelMetadata = ModelMetadataProviders.Current.GetMetadataForType( () => new PropertyModel() { Price = Convert.ToInt32(price) }, typeof(PropertyModel) ), ModelState = bindingContext.ModelState, ValueProvider = bindingContext.ValueProvider }; // call the default model binder this new binding context object o = base.BindModel(controllerContext, newBindingContext); newBindingContext.ModelState.Remove("Price"); newBindingContext.ModelState.Add("Price", new ModelState()); newBindingContext.ModelState.SetModelValue("Price", new ValueProviderResult(price, price, null)); return o; } else { return base.BindModel(controllerContext, bindingContext); } } //protected override object CreateModel(ControllerContext controllerContext, ModelBindingContext bindingContext, Type modelType) //{ // //return base.CreateModel(controllerContext, bindingContext, modelType); // PropertyModel model = new PropertyModel(); // if (modelType == typeof(PropertyModel)) // { // model = (PropertyModel)base.CreateModel(controllerContext, bindingContext, modelType); // HttpRequestBase request = controllerContext.HttpContext.Request; // ssortingng price = request.Form.Get("Price").Replace(",", ssortingng.Empty); // model.Price = Convert.ToInt32(price); // } // return model; //} } } 

Cela fonctionne, mais si j’entre 0 pour le prix, le modèle revient comme valide, ce qui est faux car j’ai une annotation Range qui dit que le prix minimum est de 1. Au bout de mes peines.

Mettre à jour:

Afin de tester un classeur de modèle personnalisé avec des types composites. J’ai créé les classes de modèle de vue suivantes:

 using System.ComponentModel.DataAnnotations; namespace MvcApplication1.Models { public class PropertyRegistrationViewModel { public PropertyRegistrationViewModel() { } public Property Property { get; set; } public Agent Agent { get; set; } } public class Property { public int HouseNumber { get; set; } public ssortingng Street { get; set; } public ssortingng City { get; set; } public ssortingng State { get; set; } public ssortingng Zip { get; set; } [Required(ErrorMessage="You must enter the price.")] [Range(1000, 10000000, ErrorMessage="Bad price.")] public int Price { get; set; } } public class Agent { public ssortingng FirstName { get; set; } public ssortingng LastName { get; set; } [Required(ErrorMessage="You must enter your annual sales.")] [Range(10000, 5000000, ErrorMessage="Bad range.")] public int AnnualSales { get; set; } public Address Address { get; set; } } public class Address { public ssortingng Line1 { get; set; } public ssortingng Line2 { get; set; } } } 

Et voici le contrôleur:

 using MvcApplication1.Core; using MvcApplication1.Models; using System.Web.Mvc; namespace MvcApplication1.Controllers { public class RegistrationController : Controller { public ActionResult Index() { PropertyRegistrationViewModel viewModel = new PropertyRegistrationViewModel(); return View(viewModel); } [HttpPost] public ActionResult Index([ModelBinder(typeof(PropertyRegistrationModelBinder))]PropertyRegistrationViewModel viewModel) { if (ModelState.IsValid) { //save registration. } return View(viewModel); } } } 

Voici l’implémentation personnalisée du classeur de modèles:

 using MvcApplication1.Models; using System; using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; namespace MvcApplication1.Core { public class PropertyRegistrationModelBinder : DefaultModelBinder { protected override object GetPropertyValue( ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) { if (propertyDescriptor.ComponentType == typeof(PropertyRegistrationViewModel)) { if (propertyDescriptor.Name == "Property") { var price = bindingContext.ValueProvider.GetValue("Property.Price").AttemptedValue.Replace(",", ssortingng.Empty); var property = new Property(); // Question 1: Price is the only property I want to modify. Is there any way // such that I don't have to manually populate the rest of the properties like so? property.Price = ssortingng.IsNullOrWhiteSpace(price)? 0: Convert.ToInt32(price); property.HouseNumber = Convert.ToInt32(bindingContext.ValueProvider.GetValue("Property.HouseNumber").AttemptedValue); property.Street = bindingContext.ValueProvider.GetValue("Property.Street").AttemptedValue; property.City = bindingContext.ValueProvider.GetValue("Property.City").AttemptedValue; property.State = bindingContext.ValueProvider.GetValue("Property.State").AttemptedValue; property.Zip = bindingContext.ValueProvider.GetValue("Property.Zip").AttemptedValue; // I had thought that when this property object returns, our annotation of the Price property // will be honored by the model binder, but it doesn't validate it accordingly. return property; } if (propertyDescriptor.Name == "Agent") { var sales = bindingContext.ValueProvider.GetValue("Agent.AnnualSales").AttemptedValue.Replace(",", ssortingng.Empty); var agent = new Agent(); // Question 2: AnnualSales is the only property I need to process before validation, // Is there any way I can avoid tediously populating the rest of the properties? agent.AnnualSales = ssortingng.IsNullOrWhiteSpace(sales)? 0: Convert.ToInt32(sales); agent.FirstName = bindingContext.ValueProvider.GetValue("Agent.FirstName").AttemptedValue; agent.LastName = bindingContext.ValueProvider.GetValue("Agent.LastName").AttemptedValue; var address = new Address(); address.Line1 = bindingContext.ValueProvider.GetValue("Agent.Address.Line1").AttemptedValue + " ROC"; address.Line2 = bindingContext.ValueProvider.GetValue("Agent.Address.Line2").AttemptedValue + " MD"; agent.Address = address; // I had thought that when this agent object returns, our annotation of the AnnualSales property // will be honored by the model binder, but it doesn't validate it accordingly. return agent; } } return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); } protected override void OnModelUpdated(ControllerContext controllerContext, ModelBindingContext bindingContext) { var model = bindingContext.Model as PropertyRegistrationViewModel; //In order to validate our model, it seems that we will have to manually validate it here. base.OnModelUpdated(controllerContext, bindingContext); } } } 

Et voici la vue Razor:

 @model MvcApplication1.Models.PropertyRegistrationViewModel @{ ViewBag.Title = "Property Registration"; } 

Property Registration

Enter your property and agent information below.

@using (Html.BeginForm("Index", "Registration")) { @Html.ValidationSummary();

Property Info

House Number @Html.TextBoxFor(m => m.Property.HouseNumber)
Street @Html.TextBoxFor(m => m.Property.Street)
City @Html.TextBoxFor(m => m.Property.City)
State @Html.TextBoxFor(m => m.Property.State)
Zip @Html.TextBoxFor(m => m.Property.Zip)
Price @Html.TextBoxFor(m => m.Property.Price)

Agent Info

First Name @Html.TextBoxFor(m => m.Agent.FirstName)
Last Name @Html.TextBoxFor(m => m.Agent.LastName)
Annual Sales @Html.TextBoxFor(m => m.Agent.AnnualSales)
Agent Address [email protected](m => m.Agent.Address.Line1)
Agent Address [email protected](m => m.Agent.Address.Line2)
}

Et voici le fichier global.asax où je câble le classeur de modèles personnalisé. BTW, il semble que cette étape n’est pas nécessaire, car j’ai remarqué qu’il fonctionne toujours sans cette étape.

 using MvcApplication1.Core; using MvcApplication1.Models; using System.Web.Http; using System.Web.Mvc; using System.Web.Optimization; using System.Web.Routing; namespace MvcApplication1 { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { protected void Application_Start() { AreaRegistration.RegisterAllAreas(); WebApiConfig.Register(GlobalConfiguration.Configuration); FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters); RouteConfig.RegisterRoutes(RouteTable.Routes); BundleConfig.RegisterBundles(BundleTable.Bundles); AuthConfig.RegisterAuth(); ModelBinders.Binders.Add(typeof(PropertyRegistrationViewModel), new PropertyRegistrationModelBinder()); } } } 

Peut-être que je fais quelque chose de mal ou pas assez. J’ai remarqué les problèmes suivants:

  1. Bien que je n’aie besoin que de modifier la valeur Price de l’object property, il semble que je doive peupler fastidieusement toutes les autres propriétés du classeur de modèle. Je dois faire la même chose pour la propriété AnnualSales de la propriété de l’agent. Y a-t-il un moyen d’éviter ce problème dans le classeur?
  2. J’avais pensé que la méthode BindModel par défaut honorera notre annotation des propriétés de nos objects et les validera en conséquence après l’appel de GetPropertyValue, mais ce n’est pas le cas. Si j’entre une valeur en dehors de la plage de valeur pour l’object Price ou AnnualSales de l’agent, le modèle est considéré comme valide. En d’autres termes, les annotations Range sont ignorées. Je sais que je peux les valider en remplaçant OnModelUpdated dans le classeur de modèle personnalisé, mais cela demande trop de travail et, de plus, j’ai les annotations en place. Pourquoi l’implémentation par défaut du classeur de modèle ne les respecte-t-elle pas simplement parce que je substitue une partie de ça?

@dotnetstep: Pourriez-vous nous en dire plus? Je vous remercie.

  [HttpPost] public ActionResult Edit([ModelBinder(typeof(PropertyModelBinder))]PropertyModel model) { ModelState.Clear(); TryValidateModel(model); if (ModelState.IsValid) { //Save property info. } return View(model); } 

J’espère que ça va aider.

Vous pouvez aussi essayer la solution @Ryan.

Cela pourrait être votre ModelBinder personnalisé. (Dans ce cas, vous n’avez pas besoin de mettre à jour le résultat de votre action d’édition, comme je l’ai suggéré ci-dessus.)

 public class PropertyModelBinder : DefaultModelBinder { protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) { if(propertyDescriptor.ComponentType == typeof(PropertyModel)) { if (propertyDescriptor.Name == "Price") { var obj= bindingContext.ValueProvider.GetValue("Price"); return Convert.ToInt32(obj.AttemptedValue.ToSsortingng().Replace(",", "")); } } return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); } } 

Comme vous avez mis à jour votre scope pour la liaison. J’ai fourni ma suggestion dans les commentaires. De même, si vous utilisez ModelBinder pour Property and Agent, vous pouvez le faire de la sorte.

 //In Global.asax ModelBinders.Binders.Add(typeof(Property), new PropertyRegistrationModelBinder()); ModelBinders.Binders.Add(typeof(Agent), new PropertyRegistrationModelBinder()); //Updated ModelBinder look like this. public class PropertyRegistrationModelBinder : DefaultModelBinder { protected override object GetPropertyValue(ControllerContext controllerContext, ModelBindingContext bindingContext, System.ComponentModel.PropertyDescriptor propertyDescriptor, IModelBinder propertyBinder) { if (propertyDescriptor.ComponentType == typeof(Property) || propertyDescriptor.ComponentType == typeof(Agent)) { if(propertyDescriptor.Name == "Price" || propertyDescriptor.Name == "AnnualSales") { var value = bindingContext.ValueProvider.GetValue(bindingContext.ModelName).AttemptedValue.Replace(",", ssortingng.Empty); return ssortingng.IsNullOrEmpty(value) ? 0 : Convert.ToInt32(value); } } return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); } } 

Aussi, je voudrais dire que vous êtes capable de trouver beaucoup d’informations liées à cela et que vous pouvez faire la même chose de plusieurs façons. Comme si vous introduisez un nouvel atsortingbut qui s’applique à la propriété de classe pour la liaison, vous appliquez ModelBinder au niveau de la classe.

Cela ne répondra pas exactement à votre question, mais je le répète quand même car je pense que cela répond à ce que les gens vous demandaient dans votre question précédente.

Dans ce cas particulier, on dirait vraiment que vous voulez une chaîne. Oui, je sais que les valeurs sont, en définitive, des valeurs numériques (un nombre entier, semble-t-il), mais lorsque vous travaillez avec quelque chose comme MVC, vous devez vous rappeler que les modèles utilisés par votre vue ne doivent pas nécessairement correspondre aux modèles. qui font partie de votre logique métier. Je pense que vous rencontrez ce problème parce que vous essayez de mélanger les deux.

Au lieu de cela, je vous recommande de créer un modèle (un ViewModel) spécifiquement pour la vue que vous affichez à l’utilisateur final. Ajoutez-y les différentes annotations de données qui aideront à valider la chaîne qui constitue le prix. (Vous pouvez facilement effectuer toute la validation de votre choix via une simple annotation de données d’expressions régulières . Ensuite, une fois que le modèle est soumis à votre contrôleur (ou toute autre donnée soumise à votre contrôleur) et que vous savez qu’il est valide (via le annotation de données), vous pouvez ensuite le convertir en un entier que vous souhaitez pour le modèle que vous utilisez avec votre logique métier et continuer à l’utiliser comme vous le feriez (en tant qu’entier).

De cette façon, vous évitez toute la complexité inutile que vous avez demandée (ce qui offre une solution, mais elle ne correspond pas vraiment à l’état d’esprit derrière MVC) et vous permet d’obtenir la flexibilité que vous recherchez dans votre vue avec l’exigence ssortingcte dans votre logique métier.

Espérons que cela a du sens. Vous pouvez effectuer une recherche sur le Web pour ViewModels dans MVC. Les didacticiels ASP.NET MVC constituent un bon sharepoint départ.

Votre validation a bien fonctionné en changeant le moment où BindModel déclenche. Dans votre code, vous avez ces lignes dans PropertyModelBinder :

 object o = base.BindModel(controllerContext, newBindingContext); newBindingContext.ModelState.Remove("Price"); newBindingContext.ModelState.Add("Price", new ModelState()); newBindingContext.ModelState.SetModelValue("Price", new ValueProviderResult(price, price, null)); return o; 

J’ai déplacé base.BindModel pour qu’il se déclenche immédiatement avant de renvoyer l’object (après avoir reconstitué le contexte) et la validation fonctionne désormais comme prévu. Voici le nouveau code:

 newBindingContext.ModelState.Remove("Price"); newBindingContext.ModelState.Add("Price", new ModelState()); newBindingContext.ModelState.SetModelValue("Price", new ValueProviderResult(price, price, null)); object o = base.BindModel(controllerContext, newBindingContext); return o;