9
votes

I ran into the following problem:

I've got a asp.net mvc 5 controller with a reference type as a parameter:

    [Route("")]
    [HttpGet]
    public ActionResult GetFeeds(Location location)
    {
        if (location == null)
        {
            // init location or smth
        }

        // Do smth with location

        return new EmptyResult();
   }

You will notice that I'm using AttributeRouting. There are NO other methods with the name of this action.

However - this is my location class:

    public class Location : ILocation
    {
       public DbGeography Coordinates { get; set; }

       public double Latitude { get; set; }

       public double Longitude { get; set; }
    }

Nothing special here (the interface defines all of those properties).

If I'm accessing the controllers action (actually using powershell) and passing something like:

http://localhost:2000/Feed?latitude=23.1&longitude=37

everthing works fine but if I'm using

http://localhost:2000/Feed

the location parameter is NOT null (it is a new Location with default values), which is the behavior I want to have :( .

Does anyone have an idea why this happens?

Thanks in advance

4
The binder's working as designed - it's instantiating a Location object and populating its values with defaults except where you supply override values (which you aren't in the second example). It won't create a null instance of your Location object just because your route supplied no values. - 48klocs
@ChristopherKellner - it does not appear possible due to "how the model binder" works. Take a look at the link in my answer for a little bit more explanation. - Tommy

4 Answers

7
votes

The MVC model binder has taken over. What I originally posted would work in instances that do not go through the model binder. However, based on other answers on SO and my own quick testing, it appears that a viewmodel parameter will never be null due to how the binder works and ties properties to form values.

In your instance, I would check if both latitude and longitude are null to see if nothing was passsed. Which means you will need to make them nullable on your ViewModel

public class Location : ILocation
    {
       public DbGeography Coordinates { get; set; }

       public double? Latitude { get; set; }

       public double? Longitude { get; set; }
    }

Updated Controller Code

if (location.Latitude == null && location.Longitude == null)
    {
        // init location or smth
    }
4
votes

ModelBinder creates a new instance of the object, so you have two options here:

Have a [Required] DataAnnotation on 'required properties' AND mark them as nullable then check for ModelState.IsValid (recommended)

make Latitude and Longitude nullable double? and you can check for Latitude.HasValue && Longitude.HasValue

UPDATE :

public class Location : ILocation
    {
       public DbGeography Coordinates { get; set; }

       public double? Latitude { get; set; }

       public double? Longitude { get; set; }
    }

public class LocationGetFeedsViewModel : LocationGetFeedsBinderModel {
       // change coordinates to string because maybe that's easier to handle on the view.
       public string Coordinates { get; set; }
       // added to sum to the example
       public IEnumerable<SelectListItem> Zones { get; set; } 
}

public class LocationGetFeedsBinderModel {
       [Required]
       public double? Latitude { get; set; }
       [Required]
       public double? Longitude { get; set; }
}

Controller:

public ActionResult GetFeeds(LocationGetFeedsBinderModel location) {
    if (!ModelState.IsValid)
        // redirect or display some error 
    return new EmptyResult();
}
1
votes

The default model binder will always instantiate a complex object so it will unfortunately never be null and any optional assignment will be skipped.

The end result of this behavior as it applies to your system is going to be that you must detect the use of a default constructor. There is no way to do this with reflection or implicit inspection.

It must be done explicitly.

It could be accomplished by having a flag in the class set in the default constructor, with nullable properties as suggested in the accepted answer, by using data annotations as Bart suggests, by using custom get and set methods for the properties, or a variety of other ways.

1
votes

I've been testing posting data with angularjs.

I find that if you force a value to null instead of undefined, ModelBinder doesn't instantiate a complex object.

if (!location)
    location = null;

$http({ method: "POST", url: "/Location/Create", data: { location } })