How to validate my model in a custom model binder?
Asked Answered
J

4

17

I asked about an issue I have with comma delimited numeric values here.

Given some of the replies, I attempted to try to implement my own model binder as follows:

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

And updated my controller class as this:

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

Now, if I enter the price with commas, my custom model binder will remove the commas, that's what I want, but validation still fails. So, question is: How to do custom validation in my custom model binder such that the captured price value with commas can be avoided? In other words, I suspect that I need to do more in my custom model binder, but don't know how and what. Thanks.Open the screen shot in a new tab for a better view.

Update:

So, I tried @mare's solution at https://mcmap.net/q/131239/-setting-modelstate-values-in-custom-model-binder and updated my model binder as follows:

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

It sorta works, but if I enter 0 for price, the model comes back as valid, which is wrong because I have a Range annotation which says that the minimum price is 1. At my wit's end.

Update:

In order to test out a custom model binder with composite types. I've created the following view model classes:

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

And here is the controller:

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

Here is the custom model binder implementation:

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

And here is the Razor view:

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

<h2>Property Registration</h2>
<p>Enter your property and agent information below.</p>

@using (Html.BeginForm("Index", "Registration"))
{
    @Html.ValidationSummary();    
    <h4>Property Info</h4>
    <text>House Number</text> @Html.TextBoxFor(m => m.Property.HouseNumber)<br />
    <text>Street</text> @Html.TextBoxFor(m => m.Property.Street)<br />
    <text>City</text> @Html.TextBoxFor(m => m.Property.City)<br />
    <text>State</text> @Html.TextBoxFor(m => m.Property.State)<br />
    <text>Zip</text> @Html.TextBoxFor(m => m.Property.Zip)<br />
    <text>Price</text> @Html.TextBoxFor(m => m.Property.Price)<br /> 
    <h4>Agent Info</h4>
    <text>First Name</text> @Html.TextBoxFor(m => m.Agent.FirstName)<br />
    <text>Last Name</text> @Html.TextBoxFor(m => m.Agent.LastName)<br />
    <text>Annual Sales</text> @Html.TextBoxFor(m => m.Agent.AnnualSales)<br />
    <text>Agent Address L1</text>@Html.TextBoxFor(m => m.Agent.Address.Line1)<br />
    <text>Agent Address L2</text>@Html.TextBoxFor(m => m.Agent.Address.Line2)<br />
    <input type="submit" value="Submit" name="submit" />
}

And here is the global.asax file where I wire up the custom model binder. BTW, it seems this step isn't needed, coz I notice it still works without this step.

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

Maybe I am doing something wrong or not enough. I've noticed the following problems:

  1. Although I only need to modify the Price value of the property object, it seems I have to tediously populate all the other properties in the model binder. I have to do the same for the AnnualSales property of the agent property. Is there anyway this can be avoided in the model binder?
  2. I had thought that the default BindModel method will honor our annotation of our objects' properties and validate them accordingly after it calls GetPropertyValue, but it doesn't. If I enter some value way out of range for Price of the Property object or the AnnualSales of the Agent object, the model comes back as valid. In other words, the Range annotations are ignored. I know I can validate them by overriding OnModelUpdated in the custom model binder, but that's too much work, and plus, I have the annotations in place, why doesn't the default implementation of the model binder honor them just because I am overriding part of it?

@dotnetstep: Could you throw some insights into this? Thank you.

Josh answered 18/4, 2014 at 0:51 Comment(4)
Could you show data annotations used on Price property?Danczyk
#23098559Josh
did you tried this: #2589088Danczyk
Yes, I just tried the solution by @mare. It sorta worked. But then, if I enter 0 for price, the model comes back as valid, which is wrong because I have a Range annotation which says the minimum price is 1. Hmm, frustrating.Josh
J
21
    [HttpPost]
    public ActionResult Edit([ModelBinder(typeof(PropertyModelBinder))]PropertyModel model)
    {
        ModelState.Clear();
        TryValidateModel(model);
        if (ModelState.IsValid)
        {
            //Save property info.              
        }

        return View(model);
    }

Hope This will Help.

Also you can try @Ryan solution as well.

This could be your Custom ModelBinder. ( In this case you don't need to update your Edit Action Result As I suggested above)

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

As you have updated your scope for binding. I have provided my suggestion in comments. Also if you use ModelBinder for Property and Agent than you can do like this.

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

Also I would like to say that you are able to find many information related to this and also you can do same thing many ways. Like if you introduce new attribute that apply to class property for binding same way you apply ModelBinder at class level.

Josuejosy answered 18/4, 2014 at 4:20 Comment(9)
You rock. All I need to do is this override of GetPropertyValue. Overriding BindModel isn't even needed for my case. This answer will help a lot of people. I've been struggling for 3 days. Thank you very much. @Ryan's solution does not work, and I tried it before he suggested. That trick basically tries to take the validation info for Price out of ModelState, which isn't desirable.Josh
I realize that bindingContext.ValueProvider.GetValue(key) can only retrieve the value of a property of a simple type. If I define the real estate agent as its own class like public Agent { public string FirstName {get; set; } public string LastName{get; set; } public string PhoneNumber {get; set; } } and my PropertyModel would have this property: public Agent Agent{get; set; }. Suppose I also want to modify agent raw info from the form collection in GetPropertyValue, how can I reconstruct the Agent object inside GetPropertyValue? Thanks.Josh
This is depend on how you build your view. Please provide some example code so I can help on that.Josuejosy
I've updated my question, please see if you have some insights into the problems I have. Thanks.Josh
I would suggest that you should have to use modelbinder for Property and Agent class rather than CompositeViewModel.Josuejosy
But if I create separate model binders for Property and Agent respectively, how do I use them in the controller action method like this one public ActionResult Index([ModelBinder(typeof(PropertyRegistrationModelBinder))]PropertyRegistrationViewModel viewModel) ? Apparently, my view model is PropertyRegistration‌​ViewModel, which is a wrapper of both Property and Agent. Are we able to tag multiple model binders in the action method?Josh
You don't have to do that. Even in your updated sample you have configured in Global.asax so there is no need to specify in Index method again. Apart from that there is another way to apply binder and it is through attribute of class.Josuejosy
Oh, yeah, that is a good idea. I will try that out. Thanks a lot.Josh
Yes, I have tried it, and it works like a charm. This will greatly reduce the amount of code. Thank you very much. So, it seems that there is no need to mess with composite types in a model binder, simply custom bind the contained type, not the type container. I wonder why the kind of model binder Mark creates at markeverard.com/2011/07/18/… is needed to begin with, which I tried, and it does not work.Josh
D
2

This won't exactly answer your question, but I'm putting it as an answer anyway because I think it addresses what people were asking you about in your previous question.

In this particular instance, it really sounds as though you want a string. Yes, I know the values are, ultimately, numerical (integer, it seems) values, but when you are working with something like MVC, you have to remember that the models that are being used by your view do not have to match the models that are part of your business logic. I think you're running into this issue because you are attempting to mix the two.

Instead, I would recommend creating a model (a ViewModel) that is specifically for the View that you are displaying to the end user. Add the various data annotations to it that will help validate the STRING that makes up the Price. (You could easily do all the validation you want via a simple regular expression data annotation. Then, once the model is actually submitted to your controller (or whatever other data is submitted to your controller) and you know that it is valid (via the data annotation), you can then convert it to an integer that you want for the model you are using with your business logic, and go on using it as you would (as an integer).

This way, you avoid having all this unnecessary complexity that you have been asking about (which does have a solution, but it doesn't really fit the mindset behind MVC) and allows you to achieve the flexibility you are looking for in your view with the stringent requirement in your business logic.

Hopefully that makes sense. You can search the web for ViewModels in MVC. A good place to start out is the ASP.NET MVC tutorials.

Drin answered 18/4, 2014 at 2:37 Comment(5)
Thanks. I do feel like I am already hitting a wall. I have been hoping that in the custom model binder, I can process the raw value (650,000) , convert it to 650000, then somehow validate it such that it will pass. But MVC seems to validate before the model is constructed. I think the question is: how, if at all possible, to validate the model after I construct it in my way? Do you have a link to the solution you hinted?Josh
It looks like you already found an answer to what you were looking for, so that's good. However, I would really recommend understanding the above issue (follow the link I provided to the tutorial as it has a good example of what you would need to do what I am suggestion).Drin
Thanks for the follow-up. But I am not clear how that music store tutorial is related to the issue I wanted to solve, and the way I want it resolved.Josh
That's the problem - my solution would not solve it the way you wanted to solve the problem (that's why my caveat was at the beginning of my answer). Instead, I was suggestion relook at your design as what you are currently doing, while you got it to work, may not be the best approach. The tutorial shows you how to build that design out and separate concerns better. (Specifically, this section discusses how to approach the problem you were attempting to solve in the first place.)Drin
I did test out the strategy, and it seems to be a nice solution, without much of the hassle that comes with the custom model binder. It seems that if we use our custom model binder, our annotations of our object properties will be ignored, and we will have to also manually validate our model in our custom model binder. Correct me if I am wrong. I really hope that I'm wrong.Josh
W
2

I got your validation to work fine by changing when BindModel fires. In your code, you have these lines in 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;

I moved base.BindModel to fire immediately before returning the object (after reconstructing context) and now validation works as expected. Here is the new 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;
Willock answered 18/4, 2014 at 2:51 Comment(1)
I just tried. It does not work. The problem is, once we call base.BindModel(...), it again uses the old values with commas to validate and resulting in validation errors.Josh
S
0

This is what you can do to revalidate your model, after custom model binding:

  1. Define a new Controller type:
public class ValidatableController : Controller
{
   public bool RevalidateModel(object model)
   {
      ModelState.Clear();
      return TryValidateModel(model);
   }
}

Note: The reason for defining the above child class is that the built-in TryValidateModel method is protected and we cannot access it in our custome model binder.

  1. Change your controller to inherit from the above:
public class MyController : ValidatableController
{
   public ActionResult Save(MyModel myModel)
   {
      // save logic...
   }
}
  1. Now you can redo the validation once you have done your model binding:
public class MyCustomModelBinder : DefaultModelBinder
{
   public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
   {
      var controller = controllerContext.Controller as ValidatableController;
      var boundValue = base.BindModel(controllerContext, bindingContext);
         
      // do your custom model binding...
      ((MyModel)boundValue).Desc = MySanitizer.Sanitize(((MyModel)boundValue).Desc);

      // redo validation after model binding
      controller.RevalidateModel(boundValue);
      return boundValue;
   }
}
Smriti answered 19/10, 2023 at 6:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.