17

I'm working with ASP.NET MVC and have a problem with the value sent from Ajax to my controller.

Let's say I have SampleViewModel like this:

public class SampleViewModel
{
    private string _firstName = string.Empty;

    public SampleViewModel()
    {
        _firstName = string.Empty;
    }

    public string FirstName
    {
        get { return _firstName; }
        set { _firstName = value ?? string.Empty; }
    }

    public string LastName { get; set; }

    public string FullName { get; set; }
}

Controller

[HttpPost]
public JsonResult ActionSubmit(SampleViewModel model)
{               
    var result = "";

    if(model.FirstName == null)
          result += "\nFirstName is null";

    if(model.LastName == null)
          result += "\nLastName is null";

    return Json(result);
}

Ajax

$('.submit').click(function() {
        $.ajax({
            url: '@Url.RouteUrl(new{ action="ActionSubmit", controller="Home"})',
            data: JSON.stringify({ FirstName: '', LastName: '', FullName: 'Phong_Nguyen' }),
                  // Even though I use { FirstName: '', LastName: '', FullName: 'Phong_Nguyen' } without JSON.stringify
            type: 'POST',
            dataType: 'json',
            contentType: "application/json; charset=utf-8",
            success: function(resp) {
                   alert(resp);
            }});
         });

As you can see, I send empty value but on the controller's end, I get null (the response value always "LastName is null"): enter image description here

Question

  1. Why is it that when in Ajax I am sending empty, I get null value in my controller?

  2. Is there a better way and more elegant to resolve my problem like below?

public string FirstName
{
   get { return _firstName; }
   set { _firstName = value ?? string.Empty; }
}
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56

4 Answers4

8

Why is it that when in Ajax I am sending empty, I get null value in my controller?

string is a reference type, and its default value is null. The ModelBinder sets the properties to their default value if no value is provided in the request.

Is there a better way and more elegant to resolve my problem like below?

  1. You can annotate the property with [DisplayFormat(ConvertEmptyStringToNull = false)], so the empty string value is preserved.

  2. You can write a custom ModelBinder that sets ConvertEmptyStringToNull to false, and apply it globally.

public class NullStringModelBinder : DefaultModelBinder {
    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext) {
        bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
        return base.BindModel(controllerContext, bindingContext);
    }
}

//register it in Application_Start()
ModelBinders.Binders.Add(typeof(string), new NullStringModelBinder());
Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
rhytonix
  • 968
  • 6
  • 14
  • Thanks for your reply. 2 ways to resolve look like good. However, **if no value is provided in the request** sounds like not really eligible for my question. As you can see, I'm sending empty value. Anyway +1. – Nguyễn Văn Phong Feb 09 '20 at 13:36
  • By "no value", I meant an empty string. The `ModelBinder`'s default behavior is to convert passed in empty strings to `null`, so we have to explicitly override that behavior. Check `ValueProviderResult` [implementation](https://referencesource.microsoft.com/#System.Web/ModelBinding/ValueProviderResult.cs,50) at line 50 for how it handles string conversion. `ValueProviderResult` is responsible for converting the input to the Action's argument type, if the trimmed input string length is `0`, it sets the destination to `null`. – rhytonix Feb 09 '20 at 15:53
  • Could you please tell me the reason why .Net have a change like that? I mean we all know that we should have an empty value to avoid `NullReferenceException` instead of null, right? – Nguyễn Văn Phong Feb 11 '20 at 02:12
  • @Phong It is obviously that modelBuinder uses by default `string.IsNullOrEmpty` to ckeck string values – Anton Feb 11 '20 at 06:01
  • @Phong according to [this](https://forums.asp.net/post/3680718.aspx), the decision was made to remain consistent with WebForms' behavior of treating empty strings as `null`. And I think another reason is to also remain consistent with the expectation of reference types -as I said in my original post-, e.g. an input of empty array is also converted to `null` by default since it too is a reference type, as it doesn't make sense to initialize some reference types to a non-null value while others get converted to `null`, instead they all should have the same uniform behavior. – rhytonix Feb 11 '20 at 16:30
4

This particular change has been documented here and it is one of the breaking changes from MVC 1.0. This logic of binding empty string to nulls is controlled with the ModelMetadata.ConvertEmptyStringToNull property which is used by the DefaultModelBinder.

Now if you do not want to annotate all your properties, you can create a custom model binder:

public class EmptyStringModelBinder : DefaultModelBinder 
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
        Binders = new ModelBinderDictionary() { DefaultBinder = this };
        return base.BindModel(controllerContext, bindingContext);
    }
}

And set it in your Global.asax:

ModelBinders.Binders.DefaultBinder = new EmptyStringModelBinder();

Or in your specific action:

[HttpPost]
public JsonResult ActionSubmit([ModelBinder(typeof(EmptyStringModelBinder))SampleViewModel model)

Why was this done?

This was done because the default value of a string is null and because string is a reference type and the default value for all reference types is null. Hence, this change of the framework might be reasonable. But on the other hand, we should try to avoid null values at all, therefore we need to write a custom model binder to avoid such cases.

There is a question on why the default value of the string type null instead of an empty string?. You can go over this to understand more about why this change was done.

As per @Anton: In c# 8.0 you can turn on null checks to avoid NullReferenceException and set to reference types default values instead of null

Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
Rahul Sharma
  • 7,768
  • 2
  • 28
  • 54
  • Could you please tell me the reason why .Net have a change like that? I mean we all know that we should have an empty value to avoid `NullReferenceException` instead of null, right? – Nguyễn Văn Phong Feb 11 '20 at 05:58
  • @Phong This was done because the default value of a `string` is `null` and because `string` is a `reference type` and the default value for all reference types is `null`. Hence, this change of the framework might be reasonable. But on the other hand we should try to avoid null values at all, therefore we need to write a custom model binder to avoid such cases. You can take a look at this question: https://stackoverflow.com/questions/14337551/why-is-the-default-value-of-the-string-type-null-instead-of-an-empty-string – Rahul Sharma Feb 11 '20 at 06:04
  • @Phong it was by design in C#. Now in c# 8.0 you can turn on null cheks to avoid `NullReferenceException` and set to reference types defaul values instead of ``null – Anton Feb 11 '20 at 06:04
  • Yes, I know we need to avoid null values at all. So why .Net automatically convert from empty to null, then now we have to convert one again and didn't know the reason why we got the weird value like a post above. – Nguyễn Văn Phong Feb 11 '20 at 06:17
  • @Phong .Net automatically converts empty to null because when it encounters a `string` which is empty, it sets the default value for the `string` and since `string` is a `reference type` and the default value for all reference types is `null`, it goes and changes them to null which is the expected behavior of the framework. – Rahul Sharma Feb 11 '20 at 06:20
  • Do you agree with me that the weird value makes us angry and takes time to find out? Have you ever got that? – Nguyễn Văn Phong Feb 11 '20 at 06:23
  • @Phong Yes, hence it is called a breaking change and should be handled appropriately to avoid application crashes. The value is not weird because the framework is handling the empty value as per its design. – Rahul Sharma Feb 11 '20 at 06:24
  • So what's the solution in your application? – Nguyễn Văn Phong Feb 11 '20 at 06:26
  • @Phong Setting the custom model binder in `Global.asax`: `ModelBinders.Binders.DefaultBinder = new EmptyStringModelBinder();` – Rahul Sharma Feb 11 '20 at 06:27
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/207569/discussion-between-phong-and-rahul-sharma). – Nguyễn Văn Phong Feb 11 '20 at 06:27
4

I decided summary from @Rahul Sharma's and @rhytonix's answers along with giving you examples and more detailed explanations.

  1. Why is it that when in Ajax I am sending empty, I get null value in my controller?

This is simply because MVC 2.0 defaults to initializing strings to null. To be more precise, if an empty string means has no value, So .NET sets the default value of its. And the default string (belonging to reference type) is null.

More details in Model String Property Binding Breaking Change

  1. Is there a better way and more elegant to resolve my problem like below?

There are some ways to bind String property as string.Empty instead of null

1. From C# 6, You can use DefaultValueAttribute to have auto-property an initial value like below

public string LastName => string.Empty; 

Basically, This way is the same as the OP's solution mentioned in the post, Just more elegant.

2. Custom default implementation of IModelBinder by inheriting from DefaultModelBinder and changing the ConvertEmptyStringToNull value to false on the internal ModelMetaData object.

public sealed class EmptyStringModelBinder : DefaultModelBinder 
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        bindingContext.ModelMetadata.ConvertEmptyStringToNull = false;
        return base.BindModel(controllerContext, bindingContext);
    }
}

Then in Application_Start() method of Global.asax.cs you need to do like below to complete

protected void Application_Start()
{
    ModelBinders.Binders.DefaultBinder = new EmptyStringModelBinder();
    RegisterRoutes( RouteTable.Routes );
}

3. Use DisplayFormatAttribute.ConvertEmptyStringToNull Property like below

[DisplayFormat(ConvertEmptyStringToNull = false)]
public string LastName { get; set; }

Simply because in ModelMetadata

true if empty string values are automatically converted to null; otherwise, false. The default is true

Nguyễn Văn Phong
  • 13,506
  • 17
  • 39
  • 56
0

When you declare a string variable in C#, its value is null until assigned a value.

When data is sent via a form submission, any fields where no information was entered are sent as empty strings. The best analogue to no information provided is null, so it sets those values to null (or more likely, doesn't set a value at all).

MVC cannot distinguish between an empty string because no information was provided and an empty string because that was the value assigned in JavaScript before being transmitted. It just knows one of the fields has no information, so that value should be null.

Nathan Miller
  • 785
  • 4
  • 11
  • Thanks for your answer. Unfortunately, I can't find new useful information than those above answers. So you might add more information with the solution clearly. – Nguyễn Văn Phong Feb 12 '20 at 01:29