1

I'm using the following DataAnnotations for a field on a data object in a c# .netcore razor pages application:

    [Display(Name = "Markup For Profit")]
    [Required]
    [DisplayFormat(DataFormatString = "{0:P0}", ApplyFormatInEditMode = true)]
    public double ProfitMarkup { get; set; }

On the Razor page I use this field in an input:

<input asp-for="ProfitMarkup" class="form-control" autocomplete="off" />

A decimal value of ProfitMarkup displays correctly (i.e. 0.2 displays as 20%) which is good. However when I go to submit the form the client side validation raises the error:

"The field Markup For Profit must be a number."

Which I'm pretty sure is because the '%' sign has been added by the display format.

What is the best way to allow an input on the page of 20% to make it through to the object and set set the decimal to 0.2 ?

2 Answers2

2

Adding a new Percentage type and a PercentageConverter TypeConverter has solved this problem for me.

I have declared a new property that reads the double ProfitMarkup property like this:

    [Display(Name = "Markup For Profit")]
    [Required]
    [NotMapped]
    public Percentage ProfitMarkupPercentage { get { return new Percentage(ProfitMarkup); } set { ProfitMarkup = value; } }

ProfitMarkup gets written to/from the database as a double and ProfitMarkupPercentage is displayed on the razor page like this:

<input asp-for="ProfitMarkupPercentage" class="form-control" autocomplete="off" />

The code for percentage object and it's TypeConverter is (this was provided kindly by the response in this thread: How to convert percentage string to double?):

[TypeConverter(typeof(PercentageConverter))]
public struct Percentage
{
    public double Value;

    public Percentage(double value)
    {
        Value = value;
    }

    static public implicit operator double(Percentage pct)
    {
        return pct.Value;
    }

    static public implicit operator string(Percentage pct) { return pct.ToString(); }

    public Percentage(string value)
    {
        Value = 0.0;
        var pct = (Percentage)TypeDescriptor.GetConverter(GetType()).ConvertFromString(value);
        Value = pct.Value;
    }

    public override string ToString()
    {
        return ToString(CultureInfo.InvariantCulture);
    }

    public string ToString(CultureInfo Culture)
    {
        return TypeDescriptor.GetConverter(GetType()).ConvertToString(null, Culture, this);
    }
}

public class PercentageConverter : TypeConverter
{
    static TypeConverter conv = TypeDescriptor.GetConverter(typeof(double));

    public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
    {
        return conv.CanConvertFrom(context, sourceType);
    }

    public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
    {
        if (destinationType == typeof(Percentage))
        {
            return true;
        }

        return conv.CanConvertTo(context, destinationType);
    }

    public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
    {
        if (value == null)
        {
            return new Percentage();
        }

        if (value is string)
        {
            string s = value as string;
            s = s.TrimEnd(' ', '\t', '\r', '\n');

            var percentage = s.EndsWith(culture.NumberFormat.PercentSymbol);
            if (percentage)
            {
                s = s.Substring(0, s.Length - culture.NumberFormat.PercentSymbol.Length);
            }

            double result = (double)conv.ConvertFromString(s);
            if (percentage)
            {
                result /= 100;
            }

            return new Percentage(result);
        }

        return new Percentage((double)conv.ConvertFrom(context, culture, value));
    }

    public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
    {
        if (!(value is Percentage))
        {
            throw new ArgumentNullException("value");
        }

        var pct = (Percentage)value;

        if (destinationType == typeof(string))
        {
            return conv.ConvertTo(context, culture, pct.Value * 100, destinationType) + culture.NumberFormat.PercentSymbol;
        }

        return conv.ConvertTo(context, culture, pct.Value, destinationType);
    }
}

The default model binder now populates to and from the property on to the razor page correctly.

You can also add validation data annotation to the ProfitMarkupPercentage property using the Regex:

[RegularExpression(@"^(0*100{1,1}\.?((?<=\.)0*)?%?$)|(^0*\d{0,2}\.?((?<=\.)\d*)?%?)$", ErrorMessage = "Invalid percentage")]
1

The default ModelBinder won't be able to get the correct posted value , when you have the Percent sign , in the input box, as part of DataAnnotation. You may create a custom ModelBinder. An example using ASP.Net MVC:

// The ViewModel
    public class HomeViewModel
    {
        [Display(Name = "Markup For Profit")]
        [Required]
        [DisplayFormat(DataFormatString = "{0:P2}", ApplyFormatInEditMode =true)]
        public double ProfitMarkup { get; set; }
    }

// Two Classes to implement custom ModelBinder
    public class DoublePercentDataBinder : DefaultModelBinder
    {
        public override object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            if (bindingContext.ModelType == typeof(HomeViewModel))
            {
                HttpRequestBase request = controllerContext.HttpContext.Request;
                string pMarkup = request.Form.Get("ProfitMarkup").TrimEnd('%');
                return new HomeViewModel
                {
                    ProfitMarkup = Double.Parse(pMarkup)/100
                };
            }
            else
            {
                return base.BindModel(controllerContext, bindingContext);
            }
        }
    }

    public class DoublePercentBinder : IModelBinder
    {
        public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
        {
            HttpRequestBase request = controllerContext.HttpContext.Request;
            string pMarkup = request.Form.Get("ProfitMarkup").TrimEnd('%');
            return new HomeViewModel
            {
                ProfitMarkup = Double.Parse(pMarkup)/100
            };

        }
    }

// Attach it in Application_Start in Global.asax
       protected void Application_Start()
        {
            ModelBinders.Binders.Add(typeof(HomeViewModel), new DoublePercentBinder());
            AreaRegistration.RegisterAllAreas();
            RouteConfig.RegisterRoutes(RouteTable.Routes);
        }

// HomeController Get and Post
    public class HomeController : Controller
    {
        public ActionResult Index()
        {
            HomeViewModel vm = new HomeViewModel();
            vm.ProfitMarkup = 1.2;
            return View(vm);
    }

        [HttpPost]
        public ActionResult Index([ModelBinder(typeof(DoublePercentBinder))] HomeViewModel hvm)
        {
            return View(hvm);
        }

// The Razor View
@using (Html.BeginForm("Index", "Home", FormMethod.Post))
{
    <div>
        @Html.EditorFor(x => x.ProfitMarkup)
        @Html.ValidationMessageFor(x => x.ProfitMarkup)
    </div>
    <br />
    <input type="submit" value="Submit" />
}