ASP.NET MVC custom binder for currency

I can imagine that’s quite common problem. You have a double (or better decimal) value that you want to show formatted as a currency field. Lets assume we are storing the price data in an object like this:

public class TestModel
{
    public double NumberField
    {
        get;
        set;
    }
    public double CurrencyField
    {
        get;
        set;
    }
}


The MVC view is strongly typed with this TestModel. And the view model value is formatted like this:
<%=Html.TextBox(“NumberField”, Model.NumberField)%>
<%=Html.TextBox(“CurrencyField”, Model.CurrencyField.ToString(“c”))%>

If you set the current culture to German-Swiss, for example in (base)controller like that:

protected override void Initialize(System.Web.Routing.RequestContext requestContext)
{
    base.Initialize(requestContext);
    string culture = "de-CH";
    CultureInfo ci = CultureInfo.GetCultureInfo(culture);
    Thread.CurrentThread.CurrentCulture = ci;
    Thread.CurrentThread.CurrentUICulture = ci;
}

Done so we will get a text boxes looking like this:
image
What will happen if you use a default binder to get this value? The default binder will try to parse the string as a double value and get an ModelState error. The string does not represent a double value.
To deal with this issue we have to write our own model binder. This can be done by implementing the IModelBinder. But I don’t want to reimplement this how DefaultModelBinder reads other values than my currency doubles. So I though I will set a special attribute to the “special” properties. Something like this:

[AttributeUsage(AttributeTargets.Property)]
public class CurrencyAttribute : Attribute
{
}

And my object:

public class TestModel
{
    public double NumberField
    {
        get;
        set;
    }
    [Currency]
    public double CurrencyField
    {
        get;
        set;
    }
}

And now we can overload the DefaultModelBinder and implement our TestModelBinder like this:

public class TestModelBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext,
            ModelBindingContext bindingContext)
        {
            object bindingObject = base.BindModel(controllerContext,
                bindingContext);
            foreach (System.Reflection.PropertyInfo propInfo
                in bindingObject.GetType().GetProperties())
            {
                object[] attributes =
                    propInfo.GetCustomAttributes(typeof(CurrencyAttribute), false);
                foreach (object attribute in attributes)
                {
                    CurrencyAttribute currAtt = attribute as CurrencyAttribute;
                    if (currAtt != null)
                    {
                        bindingContext.ModelState[propInfo.Name].Errors.Clear();
                        string attempted =
                            bindingContext.ValueProvider[propInfo.Name].AttemptedValue;
                        CultureInfo ci =
                            bindingContext.ValueProvider[propInfo.Name].Culture;
                        propInfo.SetValue(
                            bindingObject,
                            double.Parse(attempted, NumberStyles.Currency, ci),
                            null);
                    }
                }
            }
            return bindingObject;
        }
    }

And now we have to set the attribute to our object to tell MVC that it has to use your binder.

[ModelBinder(typeof(TestModelBinder))]
public class TestModel
{
    public double NumberField
    {
        get;
        set;
    }
    [Currency]
    public double CurrencyField
    {
        get;
        set;
    }
}

Done! Although there are a view issues with this solution (if you are using Entity Framework you cannot set custom attributes, and in the binder above you will have to implement checking if the field is present on the view), but you get the general idea!

One Comment

Leave a Reply to SD!! Cancel reply

Your email address will not be published. Required fields are marked *