views:

1601

answers:

5

Hi,

I have a custom modal class which contains a decimal member and a view to accept entry for this class. Everything worked well till I added javascripts to format the number inside input control. The format code format the inputted number with thousand separator ',' when focus blur.

The problem is that the decimal value inside my modal class isn't bind/parsed well with thousand separator. ModelState.IsValid returns false when I tested it with "1,000.00" but it is valid for "100.00" without any changes.

Could you share with me if you have any solution for this?

Thanks in advance.

Sample Class

public class Employee
{
    public string Name { get; set; }
    public decimal Salary { get; set; }
}

Sample Controller

public class EmployeeController : Controller
{
    [AcceptVerbs(HttpVerbs.Get)]
    public ActionResult New()
    {
        return View();
    }

    [AcceptVerbs(HttpVerbs.Post)]
    public ActionResult New(Employee e)
    {
        if (ModelState.IsValid) // <-- It is retruning false for values with ','
        {
            //Subsequence codes if entry is valid.
            //
        }
        return View(e);
    }
}

Sample View

<% using (Html.BeginForm())
   { %>

    Name:   <%= Html.TextBox("Name")%><br />
    Salary: <%= Html.TextBox("Salary")%><br />

    <button type="submit">Save</button>

<% } %>


I tried a workaround with Custom ModelBinder as Alexander suggested. The probelm solved. But the solution doesn't go well with IDataErrorInfo implementation. The Salary value become null when 0 is entered because of the validation. Any suggestion, please? Do Asp.Net MVC team members come to stackoverflow? Can I get a little help from you?

Updated Code with Custom Model Binder as Alexander suggested

Model Binder

public class MyModelBinder : DefaultModelBinder {

    public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
        if (bindingContext == null) {
            throw new ArgumentNullException("bindingContext");
        }

        ValueProviderResult valueResult;
        bindingContext.ValueProvider.TryGetValue(bindingContext.ModelName, out valueResult);
        if (valueResult != null) {
            if (bindingContext.ModelType == typeof(decimal)) {
                decimal decimalAttempt;

                decimalAttempt = Convert.ToDecimal(valueResult.AttemptedValue);

                return decimalAttempt;
            }
        }
        return null;
    }
}

Employee Class

    public class Employee : IDataErrorInfo {

    public string Name { get; set; }
    public decimal Salary { get; set; }

    #region IDataErrorInfo Members

    public string this[string columnName] {
        get {
            switch (columnName)
            {
                case "Salary": if (Salary <= 0) return "Invalid salary amount."; break;
            }
            return string.Empty;
        }
    }

    public string Error{
        get {
            return string.Empty;
        }
    }

    #endregion
}
A: 

Did you try to convert it to Decimal in the controller? This should do the trick:

string _val = "1,000.00"; Decimal _decVal = Convert.ToDecimal(_val); Console.WriteLine(_decVal.ToString());

Naweed Akram
Hi, I am using Default Model Binder to do the job. Pls have a look of the code I just included and advise me. Thanks for your time.
K Z
+3  A: 

The reason behind it is, that in ConvertSimpleType in ValueProviderResult.cs a TypeConverter is used.

The TypeConverter for decimal does not support a thousand separator. Read here about it: http://social.msdn.microsoft.com/forums/en-US/clr/thread/1c444dac-5d08-487d-9369-666d1b21706e

I did not check yet, but at that post they even said the CultureInfo passed into TypeConverter is not used. It will always be Invariant.

           string decValue = "1,400.23";

        TypeConverter converter = TypeDescriptor.GetConverter(typeof(decimal));
        object convertedValue = converter.ConvertFrom(null /* context */, CultureInfo.InvariantCulture, decValue);

So I guess you have to use a workaround. Not nice...

Malcolm Frexner
Hi Mathias, Thanks for detail explanation.
K Z
+2  A: 

It seems there are always workarounds of some form or another to be found in order to make the default model binder happy! I wonder if you could create a "pseudo" property that is used only by the model binder? (Note, this is by no means elegant. Myself, I seem to resort to similar tricks like this more and more often simply because they work and they get the job "done"...) Note also, if you were using a separate "ViewModel" (which I recommend for this), you could put this code in there, and leave your domain model nice and clean.

public class Employee
{
    private decimal _Salary;
    public string MvcSalary // yes, a string. Bind your form values to this!
    {
        get { return _Salary.ToString(); }
        set
        { 
            // (Using some pseudo-code here in this pseudo-property!)
            if (AppearsToBeValidDecimal(value)) {
                _Salary = StripCommas(value);
            }
        }
    }
    public decimal Salary
    {
        get { return _Salary; }
        set { _Salary = value; }
    }
}

P.S., after I typed this up, I look back at it now and am even hesitating to post this, it is so ugly! But if you think it might be helpful I'll let you decide...

Best of luck!
-Mike

Funka
Mike, I like your workaround. Though, it adds more properties to our classes than we would like, it still solve the problem. We have no choice but to live with these, I think. Thank you!Btw, I would love to see some comments form MVC team members on this. Am I too ambitious?
K Z
I added "pseudo" properties for the decimals which are needed to be validated. And I still keep custom model binder for other decimal properties which aren't being validated.
K Z
I think, this workaround together with custom modal binder is simple and good enough to solve the issue. Thanks a lot Mike.
K Z
A: 

Hey I had one more thought... This builds on Naweed's answer, but will still let you use the default model binder. The concept is to intercept the posted form, modify some of the values in it, then pass the [modified] form collection to the UpdateModel (default model binder) method... I use a modified version of this for dealing with checkboxes/booleans, to avoid the situation where anything other than "true" or "false" causes an unhandled/silent exception within the model binder.

(You would of course want to refactor this to be more re-useable, to perhaps deal with all decimals)

public ActionResult myAction(NameValueCollection nvc)
{
    Employee employee = new Employee();
    string salary = nvc.Get("Salary");
    if (AppearsToBeValidDecimal(salary)) {
        nvc.Remove("Salary");
        nvc.Add("Salary", StripCommas(salary));
    }
    if (TryUpdateModel(employee, nvc)) {
        // ...
    }
}

P.S., I may be confused on my NVC methods, but I think these will work.

Again, good luck!
-Mike

Funka
P.P.S., hmmm, if you find that you can't modify the nvc as I've posted above (for example, it occurs to me that you can't really modify Request.Form), then take a look at the link in my first paragraph for a way to rebuild a new dictionary altogether.
Funka
+2  A: 

Hi, I didn't like the solutions above and came up with this:

In my custom modelbinder, I basically replace the value with the culture invariant value if it is a decimal and then hand over the rest of the work to the default model binder. The rawvalue being a array seems strange to me, but this is what I saw/stole in the original code.

        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
    {
        if(bindingContext.ModelType == typeof(decimal) || bindingContext.ModelType==typeof(Nullable<decimal>))
        {
            ValueProviderResult valueProviderResult = bindingContext.ValueProvider[bindingContext.ModelName];
            if (valueProviderResult != null)
            {
                decimal result;
                var array = valueProviderResult.RawValue as Array;
                object value;
                if (array != null && array.Length > 0)
                {
                    value = array.GetValue(0);
                    if (decimal.TryParse(value.ToString(), out result))
                    {
                        string val = result.ToString(CultureInfo.InvariantCulture.NumberFormat);
                        array.SetValue(val, 0);
                    }
                }
            }
        }
        return base.BindModel(controllerContext, bindingContext);
    }
of course I could do some refactoring to include other numeric types.