Como validar meu modelo em um fichário de modelo personalizado?

Eu perguntei sobre um problema que tenho com valores numéricos delimitados por vírgulas aqui .

Dadas algumas das respostas, tentei implementar meu próprio modelo de fichário da seguinte maneira:

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; string price = request.Form.Get("Price").Replace(",", string.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; // string price = request.Form.Get("Price").Replace(",", string.Empty); // model.Price = Convert.ToInt32(price); // } // return model; //} } } 

E atualizei minha class de controlador assim:

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

Agora, se eu inserir o preço com vírgulas, meu fichário de modelo personalizado removerá as vírgulas, é o que eu quero, mas a validação ainda falha. Portanto, a pergunta é: como fazer a validação personalizada no meu fichário de modelo personalizado, de modo que o valor do preço capturado com vírgulas possa ser evitado ? Em outras palavras, suspeito que preciso fazer mais em meu fichário de modelo personalizado, mas não sei como e o quê. Obrigado. Abra a captura de tela em uma nova guia para uma melhor visualização.

Atualizar:

Então, eu tentei a solução @ mare em https://stackoverflow.com/a/2592430/97109 e atualizei meu fichário de modelo da seguinte maneira:

 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; string price = request.Form.Get("Price").Replace(",", string.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; // string price = request.Form.Get("Price").Replace(",", string.Empty); // model.Price = Convert.ToInt32(price); // } // return model; //} } } 

É sorta funciona, mas se eu digitar 0 para o preço, o modelo volta como válido, o que é errado porque eu tenho uma anotação Range que diz que o preço mínimo é 1. No final da minha inteligência.

Atualizar:

Para testar um fichário de modelo personalizado com tipos compostos. Eu criei as seguintes classs de modelo de visão:

 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 string Street { get; set; } public string City { get; set; } public string State { get; set; } public string 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 string FirstName { get; set; } public string 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 string Line1 { get; set; } public string Line2 { get; set; } } } 

E aqui está o controlador:

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

Aqui está a implementação do fichário de modelo personalizado:

 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(",", string.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 = string.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(",", string.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 = string.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); } } } 

E aqui está a visão 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 L1@Html.TextBoxFor(m => m.Agent.Address.Line1)
Agent Address L2@Html.TextBoxFor(m => m.Agent.Address.Line2)
}

E aqui está o arquivo global.asax onde conecto o fichário de modelo personalizado. BTW, parece que este passo não é necessário, porque eu percebo que ainda funciona sem essa etapa.

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

Talvez eu esteja fazendo algo errado ou não o suficiente. Eu notei os seguintes problemas:

  1. Embora eu precise apenas modificar o valor de preço do object de propriedade, parece que tenho que tediosamente preencher todas as outras propriedades no fichário de modelo. Eu tenho que fazer o mesmo para a propriedade AnnualSales da propriedade do agente. Existe alguma maneira isso pode ser evitado no fichário do modelo?
  2. Eu tinha pensado que o método BindModel padrão honraria nossa anotação das propriedades de nossos objects e as validaria de acordo depois de chamar GetPropertyValue, mas isso não acontece. Se eu inserir algum valor fora do intervalo para Preço do object Propriedade ou o object AnnualSales of the Agent, o modelo retornará como válido. Em outras palavras, as annotations de intervalo são ignoradas. Eu sei que posso validá-los substituindo OnModelUpdated no fichário de modelo personalizado, mas isso é muito trabalho e, além disso, eu tenho as annotations no lugar, por que não a implementação padrão do fichário de modelo honrá-los apenas porque eu estou substituindo parte disso?

@dotnetstep: Você poderia lançar algumas ideias sobre isso? Obrigado.

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

Espero que isso ajude.

Também você pode tentar a solução @Ryan também.

Este poderia ser seu ModelBinder personalizado. (Neste caso, você não precisa atualizar seu resultado de ação de edição como sugeri acima)

 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.ToString().Replace(",", "")); } } return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); } } 

Como você atualizou seu escopo para binding. Eu forneci minha sugestão nos comentários. Além disso, se você usar o ModelBinder for Property and Agent do que você pode fazer assim.

 //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(",", string.Empty); return string.IsNullOrEmpty(value) ? 0 : Convert.ToInt32(value); } } return base.GetPropertyValue(controllerContext, bindingContext, propertyDescriptor, propertyBinder); } } 

Também gostaria de dizer que você é capaz de encontrar muitas informações relacionadas a isso e também pode fazer a mesma coisa de várias maneiras. Como se você introduzisse um novo atributo que se aplica à propriedade de class para binding da mesma maneira que você aplica ModelBinder no nível de class.

Isso não vai responder exatamente a sua pergunta, mas eu estou colocando isso como uma resposta de qualquer maneira, porque eu acho que ele aborda o que as pessoas estavam perguntando sobre a sua pergunta anterior.

Neste exemplo particular, realmente soa como se você quisesse uma corda. Sim, eu sei que os valores são, em última instância, numéricos (inteiro, parece) valores, mas quando você está trabalhando com algo como MVC, você tem que lembrar que os modelos que estão sendo usados ​​por sua visão não precisam corresponder aos modelos que fazem parte da sua lógica de negócios. Acho que você está enfrentando esse problema porque está tentando misturar os dois.

Em vez disso, recomendo criar um modelo (um ViewModel) específico para a exibição que você está exibindo para o usuário final. Adicione as várias annotations de dados que ajudarão a validar o STRING que compõe o preço. (Você poderia facilmente fazer toda a validação desejada por meio de uma simples anotação de dados de expressões regulares . Depois, quando o modelo for realmente submetido ao controlador (ou qualquer outro dado enviado ao controlador) e você souber que ele é válido (via (anotação de dados), você pode então convertê-lo em um inteiro que deseja para o modelo que está usando com sua lógica de negócios, e continuar usando-o como você faria (como um inteiro).

Desta forma, você evita ter toda essa complexidade desnecessária que você tem perguntado (que tem uma solução, mas não se encheckbox muito bem na mentalidade por trás do MVC) e permite que você alcance a flexibilidade que está procurando em sua visão com o requisito rigoroso em sua lógica de negócios.

Espero que isso faça sentido. Você pode pesquisar na web por ViewModels no MVC. Um bom lugar para começar são os tutoriais da ASP.NET MVC .

Eu tenho a sua validação para funcionar bem, alterando quando BindModel acionado. Em seu código, você tem essas linhas no 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; 

Eu movi base.BindModel para triggersr imediatamente antes de retornar o object (depois de reconstruir o contexto) e agora a validação funciona conforme o esperado. Aqui está o novo código:

 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;