Why is getter being called?
Asked Answered
O

1

4

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?

Oleaster answered 31/1, 2018 at 5:46 Comment(7)
Not sure I understand your question, but the DefaultModelBinder calls the getter of properties as part of the model binding and validation process.Flight
You can always modify the getter to check if Item is nullFlight
@StephenMuecke I thought of that first, but honestly if it's called and Item is null then I 'want' it to throw an error because something went wrong. So I would rather make it a method than have to write extra workaround code for serialization that could potentially hide a problem later. But the reason I asked the question is so I can better understand the serialization process as I don't understand why it would just call getters on all properties just because they happen to exist, you know?Oleaster
But it sounds like you're saying that's what the DefaultModelBinder does... call all getters regardless of whether there is a setter on the property and regardless of whether the form actually had data to put in the property or not. That seems kinds of strange.Oleaster
But good to knowOleaster
Its part of the validation process called by the DefaultModelBinder. Your property is bool which has an implied [Required] (because it cannot be null) so it needs to call the getter (I can give you a link to the source code if you want to understand the internal workings).Flight
Interesting. Wouldn't have thought it needed to check it since it doesn't need to set it. That's the answer I was looking for though... that it has implied [Required] so validation checks it. So now I know to be aware of that in the future. Thanks.Oleaster
P
8

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.

Patsypatt answered 29/1, 2019 at 23:52 Comment(1)
Nice. I do use model binding in places, but I suppose that I could just take your solution and update it such that it only gets data from properties that have both a getter and a setter and I believe that would solve the issue for me. Thanks for the code!Oleaster

© 2022 - 2024 — McMap. All rights reserved.