23

I'm working with the NerdDinner application trying to teach myself ASP.NET MVC. However, I have stumbled upon a problem with globalization, where my server presents floating point numbers with a comma as the decimal separator, but Virtual Earth map requires them with dots, which causes some problems.

I have already solved the issue with the mapping JavaScript in my views, but if I now try to post an edited dinner entry with dots as decimal separators the controller fails (throwing InvalidOperationException) when updating the model (in the UpdateModel() metod). I feel like I must set the proper culture somewhere in the controller as well, I tried it in OnActionExecuting() but that didn't help.

Community
  • 1
  • 1
Pawel Krakowiak
  • 9,940
  • 3
  • 37
  • 55

4 Answers4

65

I have just revisited the issue in a real project and finally found a working solution. Proper solution is to have a custom model binder for the type decimal (and decimal? if you're using them):

using System.Globalization;
using System.Web.Mvc;

public class DecimalModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        object result = null;

        // Don't do this here!
        // It might do bindingContext.ModelState.AddModelError
        // and there is no RemoveModelError!
        // 
        // result = base.BindModel(controllerContext, bindingContext);

        string modelName = bindingContext.ModelName;
        string attemptedValue = bindingContext.ValueProvider.GetValue(modelName)?.AttemptedValue;

        // in decimal? binding attemptedValue can be Null
        if (attemptedValue != null)
        {
            // Depending on CultureInfo, the NumberDecimalSeparator can be "," or "."
            // Both "." and "," should be accepted, but aren't.
            string wantedSeperator = NumberFormatInfo.CurrentInfo.NumberDecimalSeparator;
            string alternateSeperator = (wantedSeperator == "," ? "." : ",");

            if (attemptedValue.IndexOf(wantedSeperator, StringComparison.Ordinal) == -1
                && attemptedValue.IndexOf(alternateSeperator, StringComparison.Ordinal) != -1)
            {
                attemptedValue = attemptedValue.Replace(alternateSeperator, wantedSeperator);
            }

            try
            {
                if (bindingContext.ModelMetadata.IsNullableValueType && string.IsNullOrWhiteSpace(attemptedValue))
                {
                    return null;
                }

                result = decimal.Parse(attemptedValue, NumberStyles.Any);
            }
            catch (FormatException e)
            {
                bindingContext.ModelState.AddModelError(modelName, e);
            }
        }

        return result;
    }
}

Then in Global.asax.cs in Application_Start():

ModelBinders.Binders.Add(typeof(decimal), new DecimalModelBinder());
ModelBinders.Binders.Add(typeof(decimal?), new DecimalModelBinder());

Note that code is not mine, I actually found it at Kristof Neirynck's blog here. I just edited a few lines and am adding the binder for a specific data type, not replacing the default binder.

D.L.MAN
  • 990
  • 12
  • 18
Pawel Krakowiak
  • 9,940
  • 3
  • 37
  • 55
  • 2
    I just updated my answer adding an additional check in code to make it properly handle nullable properties. I discovered that if it found an empty string as the attempted value it would throw an exception even if the property being bound is nullable. Now it should work just fine. – Pawel Krakowiak Sep 02 '13 at 15:30
  • 1
    There's a complementary client side fix that should be applied along this one. You'll find it here: http://stackoverflow.com/a/8102159/41420 – Pawel Krakowiak Sep 02 '13 at 15:39
  • You don't need to do this. What you need to do is set the culture of the current thread before the model binder is executed as in [this example](https://stackoverflow.com/a/32839796). – NightOwl888 Oct 10 '17 at 21:29
7

Set this in your web.config

  <system.web>
    <globalization uiCulture="en" culture="en-US" />

You appear to be using a server that is setup with a language that uses comma's instead of decimal places. You can adjust the culture to one that uses the comma's in a way that your application is designed, such as en-US.

Juan Carlos Oropeza
  • 47,252
  • 12
  • 78
  • 118
Nick Berardi
  • 54,393
  • 15
  • 113
  • 135
0

I have a different take on this, you might like it. What I don't like about the accepted answer is it doesn't check for other characters. I know there will be a case where the currency symbol will be in the box because my user doesn't know better. So yeah I can check in javascript to remove it, but what if for some reason javascript isn't on? Then extra characters might get through. Or if someone tries to spam you passing unknown characters through... who knows! So I decided to use a regex. It's a bit slower, tiny fraction slower - for my case it was 1,000,000 iterations of the regex took just under 3 seconds, while around 1 second to do a string replace on a coma and period. But seeing as I don't know what characters might come through, then I am happy for this slightest of performance hits.

public class DecimalModelBinder : DefaultModelBinder
{
    public override object BindModel(ControllerContext controllerContext,
                                     ModelBindingContext bindingContext)
    {
        string modelName = bindingContext.ModelName;
        string attemptedValue =
            bindingContext.ValueProvider.GetValue(modelName).AttemptedValue;

        if (bindingContext.ModelMetadata.IsNullableValueType
                && string.IsNullOrWhiteSpace(attemptedValue))
        {
            return null;
        }

        if (string.IsNullOrWhiteSpace(attemptedValue))
        {
            return decimal.Zero;
        }

        decimal value = decimal.Zero;
        Regex digitsOnly = new Regex(@"[^\d]", RegexOptions.Compiled);
        var numbersOnly = digitsOnly.Replace(attemptedValue, "");
        if (!string.IsNullOrWhiteSpace(numbersOnly))
        {
            var numbers = Convert.ToDecimal(numbersOnly);
            value = (numbers / 100m);

            return value;
        }
        else
        {
            if (bindingContext.ModelMetadata.IsNullableValueType)
            {
                return null;
            }

        }

        return value;
    }
}

Basically, remove all characters that are not digits, for a string that isn't empty. Convert to decimal. Divide by 100. Return result.

Works for me.

Colin Wiseman
  • 848
  • 6
  • 10
  • 1
    While this works well in some instances, if the decimal has no .00 (i.e. there is javascript validation or something else that strips it out), the divide by 100 technique does not work. – Ben Apr 23 '17 at 16:50
  • Yes. I've noticed that in a couple of circumstances, and then made sure my code always adds the decimal places via JavaScript before posting. But in real life you can't always expect that to happen. – Colin Wiseman Apr 23 '17 at 20:56
0

Can you parse the text using the invariant culture - sorry, I don't have the NerdDinner code in fornt of me, but if you are passing in dot-separated decimals than the parsing should be OK if you tell it to use the invariant culture. E.g.

 float i = float.Parse("0.1", CultureInfo.InvariantCulture);

Edit. I suspect that this is a bug in the NerdDinner code by the way, along the same lines as your previous problem.

Steve
  • 8,469
  • 1
  • 26
  • 37
  • No, I'm not touching any properties. It is supposed to happen "magically" in UpdateModel(), but it throws a not so helpful exception without details. There are two problems - when presenting data from the model I need dots, because tha mapping JS will break, however when putting data back into the model I need commas this time, because otherwise the controller breaks. It's all a little weird and I'm not sure how to solve it, Google didn't help, neither did SO. :( – Pawel Krakowiak Apr 28 '09 at 08:31
  • Yes, I don't think you can solve it without changing the code. I have the ND code and I'll take a look sometime and try to verify this and send you a fix if I can, but I'm afraid it won't be soon as a have to work:-) – Steve Apr 28 '09 at 08:49
  • OK, I think you need to undo the change you made in your other question, I have posted a suggestion there on how to fix the issue and hopefuly my fix will not cause the side effect you are seeing here. – Steve Apr 28 '09 at 14:33
  • And then you need to take a look at http://stackoverflow.com/questions/528545/mvc-datetime-binding-with-incorrect-date-format to see how dates work (or otherwise) in MVC... – Steve Apr 28 '09 at 15:47