Search code examples
asp.net-mvcmodel-binding

TypeConverter attribute for decimal on ASP.NET MVC model class not working


In my model class, I have this property:

[TypeConverter(typeof(CommaDecimalConverter))]
public decimal? Foo { get; set; }

My converter is implemented as follows:

public class CommaDecimalConverter : DecimalConverter
{
    public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
    {
        if (value is string)
        {
            var text = (string)value;

            return decimal.Parse(text, new CultureInfo("cs-CZ"));
        }

        return base.ConvertFrom(context, culture, value);
    }
}

However, a breakpoint inside CommaDecimalConverter is never triggered, and the property gets filled with the default converter.

I want my decimal property to accept comma as a separator, without changing the model binder or adding a second property to convert from.


Solution

  • I got it working by implementing a model binder just for decimals. It changes model binding for every decimal, which is not as good as [TypeConverter(...)] would have been, but it works for my case.

    First you need to register a new IModelBinderProvider implementation in your Program.cs

    builder.Services.AddControllersWithViews(opts =>
            opts.ModelBinderProviders.Insert(0, new CommaDecimalModelBinderProvider())
        );
    

    IModelBinderProvider implementation looks like this:

    public class CommaDecimalModelBinderProvider : IModelBinderProvider
    {
        public IModelBinder GetBinder(ModelBinderProviderContext context)
        {
            if (context.Metadata.ModelType == typeof(decimal) || context.Metadata.ModelType == typeof(decimal?))
            {
                return new BinderTypeModelBinder(typeof(CommaDecimalModelBinder));
            }
    
            return null;
        }
    }
    

    Since it is registered first, it checks whether the property is a decimal. If it is not, default binding is used.

    If it is a decimal, this IModelBinder implementation is used:

    public class CommaDecimalModelBinder : IModelBinder
    {
        public Task BindModelAsync(ModelBindingContext bindingContext)
        {
            if (bindingContext == null)
            {
                throw new ArgumentNullException(nameof(bindingContext));
            }
    
            if (bindingContext.ModelType != typeof(decimal) && bindingContext.ModelType != typeof(decimal?))
            {
                return Task.CompletedTask;
            }
    
            var modelName = bindingContext.ModelName;
    
            var valueProviderResult = bindingContext.ValueProvider.GetValue(modelName);
    
            if (valueProviderResult == ValueProviderResult.None)
            {
                return Task.CompletedTask;
            }
    
            var value = valueProviderResult.FirstValue;
    
            if (string.IsNullOrEmpty(value))
            {
                bindingContext.Result = ModelBindingResult.Success(null);
    
            }
            else
            {
                var decimalValue = decimal.Parse(value, new CultureInfo("cs-CZ"));
    
                bindingContext.Result = ModelBindingResult.Success(decimalValue);
            }
    
            return Task.CompletedTask;
        }
    }
    

    In it, you specify how you want your decimal to be converted.