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")]