mirror of
https://github.com/radzenhq/radzen-blazor.git
synced 2026-07-08 18:16:08 +00:00
Support for IFormattable (#1318)
* Support IFormattable, if TValue implements it * Include demo for custom numeric types * improve method name * In cases where the value is already of the correct type, do not perform conversion. Refactor SetParametersAsync to honor custom numeric support * perform the Min/Max check before the equality shortcut * remove unnecessary type conversion logic * fixup: if type implements IComparable, be sure to return value immediately, if in range * PR feedback: this resolves the issue, in that the value is pushed to the dom via JS. I couldn't understand why this existing statement had the other conditionals. Why would you want to trigger the ValueChanged/Changed callbacks, if the value indeed hasn't changed, or if there's a Format. However, admittedly I don't know the background of why it was this way. https://github.com/radzenhq/radzen-blazor/pull/1318#issuecomment-1886478912 --------- Co-authored-by: Josh H <josh@inventivecoders.com>
This commit is contained in:
@@ -479,5 +479,24 @@ namespace Radzen.Blazor.Tests
|
||||
Assert.Contains($" value=\"{maxDollars.ToString()}\"", component.Markup);
|
||||
Assert.Equal(component.Instance.Value, maxDollars);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Numeric_Supports_IFormattable()
|
||||
{
|
||||
using var ctx = new TestContext();
|
||||
|
||||
var valueToTest = new Temperature(60.23m);
|
||||
const string format = "F";
|
||||
|
||||
var component = ctx.RenderComponent<RadzenNumeric<Temperature>>(
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Temperature>.Format), format),
|
||||
ComponentParameter.CreateParameter(nameof(RadzenNumeric<Temperature>.Value), valueToTest)
|
||||
);
|
||||
|
||||
component.Render();
|
||||
|
||||
var input = component.Find("input").GetAttribute("value");
|
||||
input.MarkupMatches(valueToTest.ToString(format));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
Radzen.Blazor.Tests/Temperature.cs
Normal file
35
Radzen.Blazor.Tests/Temperature.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Radzen.Blazor.Tests;
|
||||
|
||||
public record struct Temperature(decimal DegreesCelsius)
|
||||
: IFormattable
|
||||
{
|
||||
public decimal Celsius => DegreesCelsius;
|
||||
public decimal Fahrenheit => DegreesCelsius * 9 / 5 + 32;
|
||||
public decimal Kelvin => DegreesCelsius + 273.15m;
|
||||
|
||||
public override string ToString() => ToString("G");
|
||||
public string ToString(string format) => ToString(format, CultureInfo.CurrentCulture);
|
||||
|
||||
public string ToString(string format, IFormatProvider provider)
|
||||
{
|
||||
provider ??= CultureInfo.CurrentCulture;
|
||||
if (string.IsNullOrEmpty(format))
|
||||
format = "G";
|
||||
|
||||
switch (format.ToUpperInvariant())
|
||||
{
|
||||
case "G":
|
||||
case "C":
|
||||
return $"{Celsius.ToString("F2", provider)} °C";
|
||||
case "F":
|
||||
return $"{Fahrenheit.ToString("F2", provider)} °F";
|
||||
case "K":
|
||||
return $"{Kelvin.ToString("F2", provider)} K";
|
||||
default:
|
||||
throw new FormatException($"The {format} format string is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,9 +83,9 @@ namespace Radzen.Blazor
|
||||
return;
|
||||
}
|
||||
|
||||
var step = string.IsNullOrEmpty(Step) || Step == "any" ? 1 : decimal.Parse(Step.Replace(",", "."), System.Globalization.CultureInfo.InvariantCulture);
|
||||
var step = decimal.TryParse(Step?.Replace(",", "."), out var stepVal) ? stepVal : 1;
|
||||
|
||||
var valueToUpdate = Value != null ? ConvertToDecimal(Value) : default;
|
||||
var valueToUpdate = ConvertToDecimal(Value);
|
||||
|
||||
var newValue = valueToUpdate + (stepUp ? step : -step);
|
||||
|
||||
@@ -135,6 +135,10 @@ namespace Radzen.Blazor
|
||||
{
|
||||
if (Format != null)
|
||||
{
|
||||
if (Value is IFormattable formattable)
|
||||
{
|
||||
return formattable.ToString(Format, Culture);
|
||||
}
|
||||
decimal decimalValue = ConvertToDecimal(Value);
|
||||
return decimalValue.ToString(Format, Culture);
|
||||
}
|
||||
@@ -297,7 +301,11 @@ namespace Radzen.Blazor
|
||||
TValue newValue;
|
||||
try
|
||||
{
|
||||
if (ConvertValue != null)
|
||||
if (value is TValue typedValue)
|
||||
{
|
||||
newValue = typedValue;
|
||||
}
|
||||
else if (ConvertValue != null)
|
||||
{
|
||||
newValue = ConvertValue($"{value}");
|
||||
}
|
||||
@@ -311,14 +319,14 @@ namespace Radzen.Blazor
|
||||
newValue = default(TValue);
|
||||
}
|
||||
|
||||
if (object.Equals(Value, newValue) && (!ValueChanged.HasDelegate || !string.IsNullOrEmpty(Format)))
|
||||
newValue = ApplyMinMax(newValue);
|
||||
|
||||
if (EqualityComparer<TValue>.Default.Equals(Value, newValue))
|
||||
{
|
||||
await JSRuntime.InvokeAsync<string>("Radzen.setInputValue", input, FormattedValue);
|
||||
return;
|
||||
}
|
||||
|
||||
newValue = CheckBounds(newValue);
|
||||
|
||||
Value = newValue;
|
||||
if (!ValueChanged.HasDelegate)
|
||||
{
|
||||
@@ -330,7 +338,7 @@ namespace Radzen.Blazor
|
||||
await Change.InvokeAsync(Value);
|
||||
}
|
||||
|
||||
private TValue CheckBounds(TValue newValue)
|
||||
private TValue ApplyMinMax(TValue newValue)
|
||||
{
|
||||
if (Max == null && Min == null)
|
||||
{
|
||||
@@ -343,6 +351,7 @@ namespace Radzen.Blazor
|
||||
return ConvertFromDecimal(Max.Value);
|
||||
if (Min.HasValue && c.CompareTo(Min.Value) < 0)
|
||||
return ConvertFromDecimal(Min.Value);
|
||||
return newValue;
|
||||
}
|
||||
|
||||
decimal? newValueAsDecimal;
|
||||
@@ -352,7 +361,7 @@ namespace Radzen.Blazor
|
||||
}
|
||||
catch
|
||||
{
|
||||
newValueAsDecimal = default(TValue) == null ? default(decimal?) : (decimal)ConvertType.ChangeType(default(TValue), typeof(decimal));
|
||||
newValueAsDecimal = default;
|
||||
}
|
||||
|
||||
if (newValueAsDecimal > Max)
|
||||
@@ -415,22 +424,14 @@ namespace Radzen.Blazor
|
||||
|
||||
await base.SetParametersAsync(parameters);
|
||||
|
||||
if (minChanged && Min.HasValue && Value != null && IsJSRuntimeAvailable)
|
||||
if (minChanged && IsJSRuntimeAvailable)
|
||||
{
|
||||
decimal decimalValue = (decimal)Convert.ChangeType(Value, typeof(decimal));
|
||||
if (decimalValue < Min.Value)
|
||||
{
|
||||
await InternalValueChanged(Min.Value);
|
||||
}
|
||||
await InternalValueChanged(Value);
|
||||
}
|
||||
|
||||
if (maxChanged && Max.HasValue && Value != null && IsJSRuntimeAvailable)
|
||||
if (maxChanged && IsJSRuntimeAvailable)
|
||||
{
|
||||
decimal decimalValue = (decimal)Convert.ChangeType(Value, typeof(decimal));
|
||||
if (decimalValue > Max.Value)
|
||||
{
|
||||
await InternalValueChanged(Max.Value);
|
||||
}
|
||||
await InternalValueChanged(Value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
58
RadzenBlazorDemos/Models/Dollars.cs
Normal file
58
RadzenBlazorDemos/Models/Dollars.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
|
||||
namespace RadzenBlazorDemos;
|
||||
|
||||
[TypeConverter(typeof(DollarsTypeConverter))]
|
||||
public readonly record struct Dollars(decimal Amount) : IComparable<decimal>
|
||||
{
|
||||
public int CompareTo(decimal other)
|
||||
{
|
||||
return Amount.CompareTo(other);
|
||||
}
|
||||
|
||||
public string ToString(string format, CultureInfo culture = null) => Amount.ToString(format, culture ?? CultureInfo.CreateSpecificCulture("en-US"));
|
||||
public override string ToString() => Amount.ToString("C2", CultureInfo.CreateSpecificCulture("en-US"));
|
||||
}
|
||||
|
||||
public class DollarsTypeConverter : TypeConverter
|
||||
{
|
||||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
|
||||
{
|
||||
if (sourceType == typeof(decimal) ||
|
||||
sourceType == typeof(string))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return base.CanConvertFrom(context, sourceType);
|
||||
}
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(decimal))
|
||||
return true;
|
||||
|
||||
return base.CanConvertTo(context, destinationType);
|
||||
}
|
||||
|
||||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
|
||||
{
|
||||
if (value is decimal d)
|
||||
return new Dollars(d);
|
||||
|
||||
if (value is string s)
|
||||
return decimal.TryParse(s, out var val) ? new Dollars(val) : null;
|
||||
|
||||
return base.ConvertFrom(context, culture, value);
|
||||
}
|
||||
|
||||
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
|
||||
{
|
||||
if (destinationType == typeof(decimal) && value is Dollars d)
|
||||
return d.Amount;
|
||||
|
||||
return base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
35
RadzenBlazorDemos/Models/Temperature.cs
Normal file
35
RadzenBlazorDemos/Models/Temperature.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
|
||||
namespace RadzenBlazorDemos;
|
||||
|
||||
public record struct Temperature(decimal DegreesCelsius)
|
||||
: IFormattable
|
||||
{
|
||||
public decimal Celsius => DegreesCelsius;
|
||||
public decimal Fahrenheit => DegreesCelsius * 9 / 5 + 32;
|
||||
public decimal Kelvin => DegreesCelsius + 273.15m;
|
||||
|
||||
public override string ToString() => ToString("G");
|
||||
public string ToString(string format) => ToString(format, CultureInfo.CurrentCulture);
|
||||
|
||||
public string ToString(string format, IFormatProvider provider)
|
||||
{
|
||||
provider ??= CultureInfo.CurrentCulture;
|
||||
if (string.IsNullOrEmpty(format))
|
||||
format = "G";
|
||||
|
||||
switch (format.ToUpperInvariant())
|
||||
{
|
||||
case "G":
|
||||
case "C":
|
||||
return $"{Celsius.ToString("F2", provider)} °C";
|
||||
case "F":
|
||||
return $"{Fahrenheit.ToString("F2", provider)} °F";
|
||||
case "K":
|
||||
return $"{Kelvin.ToString("F2", provider)} K";
|
||||
default:
|
||||
throw new FormatException($"The {format} format string is not supported.");
|
||||
}
|
||||
}
|
||||
}
|
||||
25
RadzenBlazorDemos/Pages/NumericCustomType.razor
Normal file
25
RadzenBlazorDemos/Pages/NumericCustomType.razor
Normal file
@@ -0,0 +1,25 @@
|
||||
<div class="rz-p-12 rz-text-align-center">
|
||||
<RadzenStack Orientation="Orientation.Horizontal">
|
||||
<RadzenStack>
|
||||
<div>A <code>Temperature</code> type that implements <code>IFormattable</code>. Value in Celsius: @value</div>
|
||||
<RadzenNumeric TValue="Temperature?" Format="F" @bind-Value=@value ConvertValue="ParseToTemperature" ShowUpDown="false"
|
||||
InputAttributes="@(new Dictionary<string, object>() { { "aria-label", "enter value" } })"/>
|
||||
</RadzenStack>
|
||||
<RadzenStack>
|
||||
<div>A <code>Dollars</code> type that provides a <code>TypeConverter</code>, therefore <code>Min</code>, <code>Max</code>, and <code>Step</code> are supported.</div>
|
||||
<RadzenNumeric TValue="Dollars?" @bind-Value="@dollarValue" Min="1" Max="250" Step="5"
|
||||
InputAttributes="@(new Dictionary<string, object>() { { "aria-label", "enter value" } })"/>
|
||||
</RadzenStack>
|
||||
</RadzenStack>
|
||||
</div>
|
||||
|
||||
@code{
|
||||
Temperature? value = new(50.5m);
|
||||
|
||||
Temperature? ParseToTemperature(string input)
|
||||
{
|
||||
return decimal.TryParse(input, out var val) ? new Temperature(val) : null;
|
||||
}
|
||||
|
||||
Dollars? dollarValue = new Dollars(2.50m);
|
||||
}
|
||||
@@ -67,4 +67,14 @@
|
||||
</RadzenText>
|
||||
<RadzenExample ComponentName="Numeric" Example="NumericConvertValue">
|
||||
<NumericConvertValue />
|
||||
</RadzenExample>
|
||||
|
||||
<RadzenText TextStyle="TextStyle.H5" TagName="TagName.H2" class="rz-pt-8">
|
||||
Custom Numeric Type Support
|
||||
</RadzenText>
|
||||
<RadzenText TextStyle="TextStyle.Body1" class="rz-mb-8">
|
||||
Types that can be converted from a string, via a <code>TypeConverter</code> or assigning <code>ConvertValue</code>, get basic support from <strong>Numeric</strong>. If the type implements a TypeConverter that can convert to/from decimal, then <code>Step</code> and <code>Min</code>/<code>Max</code> are supported. If the type implements <code>IFormattable</code>, <code>Format</code> strings will be passed to it.
|
||||
</RadzenText>
|
||||
<RadzenExample ComponentName="Numeric" Example="NumericCustomType">
|
||||
<NumericCustomType />
|
||||
</RadzenExample>
|
||||
Reference in New Issue
Block a user