asp.net-mvcmodelbinders

Why is getter being called?


Have the method below in my model:

public bool ShowShipping() { return Modalities.Scheduled.Has(Item.Modality); }

But previously it was a property like this:

public bool ShowShipping { get { return Modalities.Scheduled.Has(Item.Modality); } }

Upon accessing the page, the entire model is populated with data which includes the Item property. Item contains data that needs to be displayed on the view, but no data that needs to be posted back. So on post back (yes, the post action takes the model as a parameter) the Item property is left null.

This should not be a problem because there is only one line of code that accesses ShowShipping, which is on the view. So I am expecting that it will never be accessed except when Item is populated. However on post back I get an error which occurs before the first line of my post action is hit and it shows a null reference error in ShowShipping. So I have to assume the error is happening as it serializes form data into a new instance of the model... but why would it call this property in serialization when the only place in the entire solution that accesses it is one line in the view?


Solution

  • In System.Web.Mvc version 5.2.3.0, the DefaultModelBinder does perform validation, arguably violating separation of concerns, and there isn't a way to shut it off entirely via any setting or configuration. Other SO posts mention turning off the implicit required attribute for value types with the following line of code in your Global.asax.cs, Application_Start() method...

    DataAnnotationsModelValidatorProvider.AddImplicitRequiredAttributeForValueTypes = false;
    

    See: https://stackoverflow.com/a/2224651 (which references a forum with an answer directly from the asp.net team).

    However, that is not enough. All model class getters are still executed because of the code inside the DefaultModelBinder.BindProperty(...) method. From the source code...

    https://github.com/mono/aspnetwebstack/blob/master/src/System.Web.Mvc/DefaultModelBinder.cs

    215  // call into the property's model binder
    216  IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
    217  object originalPropertyValue = propertyDescriptor.GetValue(bindingContext.Model);
    218  ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
    219  propertyMetadata.Model = originalPropertyValue;
    220  ModelBindingContext innerBindingContext = new ModelBindingContext()
    221  {
    222      ModelMetadata = propertyMetadata,
    223      ModelName = fullPropertyKey,
    224      ModelState = bindingContext.ModelState,
    225      ValueProvider = bindingContext.ValueProvider
    226  };
    227  object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
    

    Line 217 is the offender. It calls the getter prior to setting the value from the request (the ultimate purpose of this method), apparently so that it can pass the original value in the ModelBindingContext parameter to the GetPropertyValue(...) method on line 227. I could not find any reason for this.

    I use calculated properties extensively in my model classes that certainly throw exceptions if the property expression relies on data that has not been previously set since that would indicate a bug elsewhere in the code. The DefaultModelBinder behavior spoils that design.

    To solve the problem in my case, I wrote a custom model binder that overrides the BindProperty(...) method and removes the call to the getters. This code is just a copy of the original source, minus lines 217 and 219. I also removed lines 243 through 259 since I am not using model validation, and that code references a private method to which the derived class does not have access (another problematic design of the DefaultModelBinder.BindProperty(...) method). Here is the custom model binder.

    public class NoGetterModelBinder : DefaultModelBinder {
    
       protected override void BindProperty(ControllerContext controllerContext, ModelBindingContext bindingContext, PropertyDescriptor propertyDescriptor) {
    
          string fullPropertyKey = CreateSubPropertyName(bindingContext.ModelName, propertyDescriptor.Name);
          if (!bindingContext.ValueProvider.ContainsPrefix(fullPropertyKey)) return;
          IModelBinder propertyBinder = Binders.GetBinder(propertyDescriptor.PropertyType);
          ModelMetadata propertyMetadata = bindingContext.PropertyMetadata[propertyDescriptor.Name];
          ModelBindingContext innerBindingContext = new ModelBindingContext() {
    
             ModelMetadata = propertyMetadata,
             ModelName = fullPropertyKey,
             ModelState = bindingContext.ModelState,
             ValueProvider = bindingContext.ValueProvider,
    
          };
          object newPropertyValue = GetPropertyValue(controllerContext, innerBindingContext, propertyDescriptor, propertyBinder);
          propertyMetadata.Model = newPropertyValue;
          ModelState modelState = bindingContext.ModelState[fullPropertyKey];
          if (modelState == null || modelState.Errors.Count == 0) {
    
             if (OnPropertyValidating(controllerContext, bindingContext, propertyDescriptor, newPropertyValue)) {
    
                SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
                OnPropertyValidated(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
    
             }
    
          } else {
    
             SetProperty(controllerContext, bindingContext, propertyDescriptor, newPropertyValue);
    
          }
    
       }
    
    }
    

    You can place that class anywhere in your web project, I just put it in Global.asax.cs. Then, again in Global.asax.cs, in Application_Start(), add the following line of code to make it the default model binder for all classes...

    ModelBinders.Binders.DefaultBinder = new NoGetterModelBinder();
    

    This will prevent getters from being called on your model classes.