diff --git a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
index c0f0021db1..c5be64a70f 100644
--- a/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
+++ b/examples/Demo/Shared/Microsoft.FluentUI.AspNetCore.Components.xml
@@ -669,6 +669,12 @@
Gets the for the bound value.
+
+
+ Gets the display name of the field, using the specified display name if set; otherwise, uses the field
+ identifier's name if the field is bound.
+
+
Gets or sets the current value of the input.
@@ -767,6 +773,16 @@
+
+
+ Defines an interface for components with values that are sensitive to culture settings, eg : parsing to string.
+
+
+
+
+ Gets or sets the culture of the component.
+
+
Because of the limitation of the web component, the maximum value is set to 9999999999 for really large numbers.
@@ -779,6 +795,11 @@
The minimum value for the underlying type
+
+
+ Gets or sets the error message to show when the field can not be parsed.
+
+
Gets or sets the content to be rendered inside the component.
@@ -2999,6 +3020,9 @@
By default to display using the OS culture.
+
+
+
Function to know if a specific day must be disabled.
@@ -6703,6 +6727,9 @@
⚠️ Only available when Multiple = true.
+
+
+
@@ -8127,11 +8154,6 @@
An Id value must be set to use this property.
-
-
- Gets or sets the error message to show when the field can not be parsed.
-
-
Gets or sets the content to be rendered inside the component.
@@ -8143,6 +8165,9 @@
unless an explicit value for Min or Max is provided.
+
+
+
Formats the value as a string. Derived classes can override this to determine the formatting used for CurrentValueAsString.
@@ -9036,6 +9061,9 @@
Gets or sets the child content to be rendering inside the .
+
+
+
@@ -9091,6 +9119,9 @@
Fires when hovered value changes. Value will be null if no rating item is hovered.
+
+
+
diff --git a/src/Core/Components/Base/FluentInputBase.cs b/src/Core/Components/Base/FluentInputBase.cs
index fafe6c1762..5cef9e3f3c 100644
--- a/src/Core/Components/Base/FluentInputBase.cs
+++ b/src/Core/Components/Base/FluentInputBase.cs
@@ -145,6 +145,12 @@ public abstract partial class FluentInputBase : FluentComponentBase, IDi
internal virtual bool FieldBound => Field is not null || ValueExpression is not null || ValueChanged.HasDelegate;
+ ///
+ /// Gets the display name of the field, using the specified display name if set; otherwise, uses the field
+ /// identifier's name if the field is bound.
+ ///
+ internal string FieldDisplayName => DisplayName ?? (FieldBound ? FieldIdentifier.FieldName : UnknownBoundField);
+
protected async Task SetCurrentValueAsync(TValue? value)
{
var hasChanged = !EqualityComparer.Default.Equals(value, Value);
diff --git a/src/Core/Components/Base/ICultureSensitiveComponent.cs b/src/Core/Components/Base/ICultureSensitiveComponent.cs
new file mode 100644
index 0000000000..b49ed2d482
--- /dev/null
+++ b/src/Core/Components/Base/ICultureSensitiveComponent.cs
@@ -0,0 +1,18 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+using System.Globalization;
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Defines an interface for components with values that are sensitive to culture settings, eg : parsing to string.
+///
+public interface ICultureSensitiveComponent
+{
+ ///
+ /// Gets or sets the culture of the component.
+ ///
+ public CultureInfo Culture { get; set; }
+}
diff --git a/src/Core/Components/Base/IStringParsableComponent.cs b/src/Core/Components/Base/IStringParsableComponent.cs
new file mode 100644
index 0000000000..3b9cc72ec3
--- /dev/null
+++ b/src/Core/Components/Base/IStringParsableComponent.cs
@@ -0,0 +1,16 @@
+// ------------------------------------------------------------------------
+// This file is licensed to you under the MIT License.
+// ------------------------------------------------------------------------
+
+namespace Microsoft.FluentUI.AspNetCore.Components;
+
+///
+/// Defines an interface for components with values that can be parsed from a string.
+///
+public interface IStringParsableComponent
+{
+ ///
+ /// Gets or sets the error message to show when the field can not be parsed.
+ ///
+ public string ParsingErrorMessage { get; set; }
+}
diff --git a/src/Core/Components/DateTime/FluentCalendar.razor.cs b/src/Core/Components/DateTime/FluentCalendar.razor.cs
index 97d578f29c..9cdd3edaeb 100644
--- a/src/Core/Components/DateTime/FluentCalendar.razor.cs
+++ b/src/Core/Components/DateTime/FluentCalendar.razor.cs
@@ -293,9 +293,9 @@ private async Task PickerYearSelectAsync(DateTime? year)
///
protected override bool TryParseValueFromString(string? value, out DateTime? result, [NotNullWhen(false)] out string? validationErrorMessage)
{
- BindConverter.TryConvertTo(value, Culture, out result);
- validationErrorMessage = null;
- return true;
+ bool success = BindConverter.TryConvertTo(value, Culture, out result);
+ validationErrorMessage = success ? null : string.Format(ParsingErrorMessage, FieldDisplayName);
+ return success;
}
///
diff --git a/src/Core/Components/DateTime/FluentCalendarBase.cs b/src/Core/Components/DateTime/FluentCalendarBase.cs
index df0ae1ff8b..0b8a60dfb8 100644
--- a/src/Core/Components/DateTime/FluentCalendarBase.cs
+++ b/src/Core/Components/DateTime/FluentCalendarBase.cs
@@ -7,7 +7,7 @@
namespace Microsoft.FluentUI.AspNetCore.Components;
-public abstract class FluentCalendarBase : FluentInputBase
+public abstract class FluentCalendarBase : FluentInputBase , ICultureSensitiveComponent, IStringParsableComponent
{
///
/// Gets or sets the culture of the component.
@@ -16,6 +16,10 @@ public abstract class FluentCalendarBase : FluentInputBase
[Parameter]
public virtual CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture;
+ ///
+ [Parameter]
+ public virtual string ParsingErrorMessage { get; set; } = "The {0} field must have a valid format.";
+
///
/// Function to know if a specific day must be disabled.
///
diff --git a/src/Core/Components/DateTime/FluentDatePicker.razor.cs b/src/Core/Components/DateTime/FluentDatePicker.razor.cs
index ee6452c12d..35e018c657 100644
--- a/src/Core/Components/DateTime/FluentDatePicker.razor.cs
+++ b/src/Core/Components/DateTime/FluentDatePicker.razor.cs
@@ -145,10 +145,9 @@ protected override bool TryParseValueFromString(string? value, out DateTime? res
value = new DateTime(year, 1, 1).ToString(Culture.DateTimeFormat.ShortDatePattern);
}
- BindConverter.TryConvertTo(value, Culture, out result);
-
- validationErrorMessage = null;
- return true;
+ bool success = BindConverter.TryConvertTo(value, Culture, out result);
+ validationErrorMessage = success ? null : string.Format(ParsingErrorMessage, FieldDisplayName);
+ return success;
}
private string PlaceholderAccordingToView()
diff --git a/src/Core/Components/List/ListComponentBase.razor.cs b/src/Core/Components/List/ListComponentBase.razor.cs
index a21bef098b..7ae9a4218a 100644
--- a/src/Core/Components/List/ListComponentBase.razor.cs
+++ b/src/Core/Components/List/ListComponentBase.razor.cs
@@ -17,7 +17,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
/// Component that provides a list of options.
///
///
-public abstract partial class ListComponentBase : FluentInputBase, IAsyncDisposable where TOption : notnull
+public abstract partial class ListComponentBase : FluentInputBase, IAsyncDisposable, IStringParsableComponent where TOption : notnull
{
private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/List/ListComponentBase.razor.js";
@@ -212,6 +212,10 @@ protected string? InternalValue
[Parameter]
public Expression>>? SelectedOptionsExpression { get; set; }
+ ///
+ [Parameter]
+ public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";
+
///
protected ListComponentBase()
{
diff --git a/src/Core/Components/NumberField/FluentNumberField.razor.cs b/src/Core/Components/NumberField/FluentNumberField.razor.cs
index 3c2014fa49..9c725eb167 100644
--- a/src/Core/Components/NumberField/FluentNumberField.razor.cs
+++ b/src/Core/Components/NumberField/FluentNumberField.razor.cs
@@ -10,7 +10,7 @@
namespace Microsoft.FluentUI.AspNetCore.Components;
-public partial class FluentNumberField : FluentInputBase, IAsyncDisposable
+public partial class FluentNumberField : FluentInputBase, IAsyncDisposable, IStringParsableComponent
{
private const string JAVASCRIPT_FILE = "./_content/Microsoft.FluentUI.AspNetCore.Components/Components/TextField/FluentTextField.razor.js";
@@ -86,12 +86,6 @@ public partial class FluentNumberField : FluentInputBase, IAsync
[Parameter]
public string? AutoComplete { get; set; }
- ///
- /// Gets or sets the error message to show when the field can not be parsed.
- ///
- [Parameter]
- public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";
-
///
/// Gets or sets the content to be rendered inside the component.
///
@@ -105,6 +99,10 @@ public partial class FluentNumberField : FluentInputBase, IAsync
[Parameter]
public bool UseTypeConstraints { get; set; }
+ ///
+ [Parameter]
+ public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";
+
private static readonly string _stepAttributeValue = GetStepAttributeValue();
// If type constraints is true and min is null, set min to the minimum value of TValue.
diff --git a/src/Core/Components/Radio/FluentRadioGroup.razor.cs b/src/Core/Components/Radio/FluentRadioGroup.razor.cs
index 3fdaa59322..d4a5243b0a 100644
--- a/src/Core/Components/Radio/FluentRadioGroup.razor.cs
+++ b/src/Core/Components/Radio/FluentRadioGroup.razor.cs
@@ -15,7 +15,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components;
///
[CascadingTypeParameter(nameof(TValue))]
-public partial class FluentRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase
+public partial class FluentRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : FluentInputBase, IStringParsableComponent
{
private readonly string _defaultGroupName = Identifier.NewId();
private FluentRadioContext? _context;
@@ -40,6 +40,10 @@ public FluentRadioGroup()
[CascadingParameter]
private FluentRadioContext? CascadedContext { get; set; }
+ ///
+ [Parameter]
+ public string ParsingErrorMessage { get; set; } = "The {0} field must be a (valid) number.";
+
///
protected override void OnParametersSet()
{
diff --git a/src/Core/Components/Rating/FluentRating.razor.cs b/src/Core/Components/Rating/FluentRating.razor.cs
index 538a28e28b..86ef821410 100644
--- a/src/Core/Components/Rating/FluentRating.razor.cs
+++ b/src/Core/Components/Rating/FluentRating.razor.cs
@@ -10,7 +10,7 @@
namespace Microsoft.FluentUI.AspNetCore.Components;
-public partial class FluentRating : FluentInputBase
+public partial class FluentRating : FluentInputBase, IStringParsableComponent
{
private bool _updatingCurrentValue = false;
private int? _hoverValue = null;
@@ -74,6 +74,10 @@ public partial class FluentRating : FluentInputBase
[Parameter]
public EventCallback OnHoverValueChanged { get; set; }
+ ///
+ [Parameter]
+ public string ParsingErrorMessage { get; set; } = "The {0} field must be a number.";
+
///
private string GroupName => Id ?? $"rating-{Id}";
@@ -90,8 +94,7 @@ protected override bool TryParseValueFromString(string? value, [MaybeNullWhen(fa
}
else
{
- validationErrorMessage = string.Format(CultureInfo.InvariantCulture,
- "The {0} field must be a number.",
+ validationErrorMessage = string.Format(ParsingErrorMessage,
FieldBound ? FieldIdentifier.FieldName : UnknownBoundField);
return false;
}
diff --git a/src/Core/Extensions/FluentInputExtensions.cs b/src/Core/Extensions/FluentInputExtensions.cs
index 893e7353dc..e011d1b72e 100644
--- a/src/Core/Extensions/FluentInputExtensions.cs
+++ b/src/Core/Extensions/FluentInputExtensions.cs
@@ -10,13 +10,15 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Extensions;
internal static class FluentInputExtensions
{
- public static bool TryParseSelectableValueFromString<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue>(
- this FluentInputBase input, string? value,
+ public static bool TryParseSelectableValueFromString(
+ this TInput input, string? value,
[MaybeNullWhen(false)] out TValue result,
- [NotNullWhen(false)] out string? validationErrorMessage)
+ [NotNullWhen(false)] out string? validationErrorMessage) where TInput : FluentInputBase, IStringParsableComponent
{
try
{
+ var culture = (input as ICultureSensitiveComponent)?.Culture ?? CultureInfo.CurrentCulture;
+
// We special-case bool values because BindConverter reserves bool conversion for conditional attributes.
if (typeof(TValue) == typeof(bool))
{
@@ -34,7 +36,7 @@ internal static class FluentInputExtensions
return true;
}
}
- else if (BindConverter.TryConvertTo(value, CultureInfo.CurrentCulture, out var parsedValue))
+ else if (BindConverter.TryConvertTo(value, culture, out var parsedValue))
{
result = parsedValue;
validationErrorMessage = null;
@@ -42,7 +44,7 @@ internal static class FluentInputExtensions
}
result = default;
- validationErrorMessage = $"The {input.DisplayName ?? (input.FieldBound ? input.FieldIdentifier.FieldName : input.UnknownBoundField)} field is not valid.";
+ validationErrorMessage = string.Format(input.ParsingErrorMessage, input.FieldDisplayName);
return false;
}
catch (InvalidOperationException ex)
diff --git a/tests/Core/DateTime/FluentDatePickerTests.cs b/tests/Core/DateTime/FluentDatePickerTests.cs
index b0b06383eb..a680496858 100644
--- a/tests/Core/DateTime/FluentDatePickerTests.cs
+++ b/tests/Core/DateTime/FluentDatePickerTests.cs
@@ -2,6 +2,7 @@
// This file is licensed to you under the MIT License.
// ------------------------------------------------------------------------
+using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Bunit;
using Microsoft.AspNetCore.Components;
@@ -292,4 +293,42 @@ public void FluentDatePicker_OnDoubleClickEventTriggers()
// Assert
Assert.Equal(expected, actual);
}
+
+ [Theory]
+ [InlineData("02-22-2000", "en-GB", "02/22/2000")]
+ [InlineData("02-22-2000", "nl-BE", null)]
+ [InlineData("22-12-2000", "nl-BE", "22/12/2000")]
+ [InlineData("02/22/2000", "en-GB", "02/22/2000")]
+ [InlineData("abc", "en-GB", null)]
+ [InlineData("02022000", "en-GB", null)]
+ public void FluentDatePicker_TryParseValueFromString(string? value, string? cultureName, string? expectedValue)
+ {
+ // Arrange
+ var picker = new TestDatePicker();
+ var culture = cultureName != null ? CultureInfo.GetCultureInfo(cultureName) : CultureInfo.InvariantCulture;
+
+ // Act
+ picker.Culture = culture;
+ var successfullParse = picker.CallTryParseValueFromString(value, out var resultDate, out var validationErrorMessage);
+
+ // Assert
+ if (successfullParse)
+ {
+ Assert.Equal(expectedValue, resultDate?.ToString(culture.DateTimeFormat.ShortDatePattern, culture));
+ }
+ else
+ {
+ Assert.Null(resultDate);
+ Assert.NotNull(validationErrorMessage);
+ }
+ }
+
+ // Temporary class to expose protected method
+ private class TestDatePicker : FluentDatePicker
+ {
+ public bool CallTryParseValueFromString(string? value, out System.DateTime? result, [NotNullWhen(false)] out string? validationErrorMessage)
+ {
+ return base.TryParseValueFromString(value, out result, out validationErrorMessage);
+ }
+ }
}