diff --git a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs index f791a84e35..e247de1541 100644 --- a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs +++ b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs @@ -64,18 +64,7 @@ protected override void OnParametersSet() if (!string.IsNullOrEmpty(Format)) { - // TODO: Consider using reflection to avoid having to box every value just to call IFormattable.ToString - // For example, define a method "string Type(Func property) where U: IFormattable", and - // then construct the closed type here with U=TProp when we know TProp implements IFormattable - - // If the type is nullable, we're interested in formatting the underlying type - var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp)); - if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp))) - { - throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'."); - } - - _cellTextFunc = item => ((IFormattable?)compiledPropertyExpression!(item))?.ToString(Format, null); + _cellTextFunc = CreateFormatter(compiledPropertyExpression, Format); } else { @@ -87,10 +76,8 @@ protected override void OnParametersSet() { return (value as Enum)?.GetDisplayName(); } - else - { - return value?.ToString(); - } + + return value?.ToString(); }; } if (Sortable.HasValue) @@ -118,6 +105,57 @@ protected override void OnParametersSet() } } + private static Func CreateFormatter(Func getter, string format) + { + var closedType = typeof(PropertyColumn<,>).MakeGenericType(typeof(TGridItem), typeof(TProp)); + + //Nullable struct + if (Nullable.GetUnderlyingType(typeof(TProp)) is Type underlying && + typeof(IFormattable).IsAssignableFrom(underlying)) + { + var method = closedType + .GetMethod(nameof(CreateNullableValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(underlying); + return (Func)method.Invoke(null, [getter, format])!; + } + + + if (typeof(IFormattable).IsAssignableFrom(typeof(TProp))) + { + //Struct + if (typeof(TProp).IsValueType) + { + var method = closedType + .GetMethod(nameof(CreateValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)! + .MakeGenericMethod(typeof(TProp)); + return (Func)method.Invoke(null, [getter, format])!; + } + + //Double cast required because CreateReferenceTypeFormatter required the TProp to be a reference type which implements IFormattable. + return CreateReferenceTypeFormatter((Func)(object)getter, format); + } + + throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'."); + } + + private static Func CreateReferenceTypeFormatter(Func getter, string format) + where T : class, IFormattable + { + return item => getter(item)?.ToString(format, null); + } + + private static Func CreateValueTypeFormatter(Func getter, string format) + where T : struct, IFormattable + { + return item => getter(item).ToString(format, null); + } + + private static Func CreateNullableValueTypeFormatter(Func getter, string format) + where T : struct, IFormattable + { + return item => getter(item)?.ToString(format, null); + } + /// protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item) => builder.AddContent(0, _cellTextFunc?.Invoke(item)); diff --git a/tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor b/tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor new file mode 100644 index 0000000000..b66e9d1f54 --- /dev/null +++ b/tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor @@ -0,0 +1,57 @@ +@using Xunit; +@inherits TestContext +@code +{ + [Fact] + public void PropertyGrid_Should_Format_ValueTypes() + { + var cut = Render( + @ + + + ); + + var firstCellText = cut.Find("td").TextContent; + Assert.Equal("0001", firstCellText); + } + + [Fact] + public void PropertyGrid_Should_Format_NullableValueTypes() + { + var cut = Render( + @ + + + ); + + var firstCellText = cut.Find("td").TextContent; + Assert.Equal(_people[0].BirthDate!.Value.ToString("m"), firstCellText); + } + + [Fact] + public void PropertyGrid_Should_Format_ReferenceTypes() + { + var cut = Render( + @ + + + ); + + var firstCellText = cut.Find("td").TextContent; + Assert.Equal(string.Format("{0} test", _people[0].Name), firstCellText); + } + + + [Fact] + public void PropertyGrid_Should_Throw_When_FormatUsedOnNonFormattableProperty() + { + Assert.Throws(() => Render( + @ + + + )); + } + + +} + diff --git a/tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor.cs b/tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor.cs new file mode 100644 index 0000000000..8e8a3b27c7 --- /dev/null +++ b/tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Bunit; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.FluentUI.AspNetCore.Components.Tests.PropertyColumn; +public partial class PropertyColumnFormatterTests +{ + private protected record Person(int PersonId, CustomFormattable Name, DateOnly? BirthDate, string NickName) + { + public bool Selected { get; set; } + }; + + private protected class CustomFormattable : IFormattable + { + private readonly string _value; + public CustomFormattable(string value) => _value = value; + public string ToString(string? format, IFormatProvider? provider) => + string.IsNullOrEmpty(format) ? _value : string.Format(format, _value); + } + + private readonly IList _people = + [ + new Person(1, new("Jean Martin"), new DateOnly(1985, 3, 16), string.Empty), + new Person(2, new("Kenji Sato"), new DateOnly(2004, 1, 9), string.Empty), + new Person(3, new("Julie Smith"), new DateOnly(1958, 10, 10), string.Empty), + ]; + + private protected IQueryable People => _people.AsQueryable(); + + public PropertyColumnFormatterTests() + { + JSInterop.Mode = JSRuntimeMode.Loose; + Services.AddSingleton(LibraryConfiguration.ForUnitTests); + + var keycodeService = new KeyCodeService(); + Services.AddScoped(factory => keycodeService); + } +}