From c265876378d7485393dfde322bd7c2ef4141c538 Mon Sep 17 00:00:00 2001 From: Tyme Bleyaert Date: Thu, 14 Aug 2025 11:45:52 +0200 Subject: [PATCH 1/5] Remove boxing when formatting the property in property columns. --- .../DataGrid/Columns/PropertyColumn.cs | 75 +++++++++++++++---- .../PropertyColumnFormatterTests.razor | 57 ++++++++++++++ .../PropertyColumnFormatterTests.razor.cs | 41 ++++++++++ 3 files changed, 157 insertions(+), 16 deletions(-) create mode 100644 tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor create mode 100644 tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor.cs diff --git a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs index f791a84e35..345429cc56 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,62 @@ 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])!; + } + else + { + 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); + } +} From c6c5dea73c7660d337ea8520477711f1ac3b12bf Mon Sep 17 00:00:00 2001 From: Tyme Bleyaert Date: Thu, 14 Aug 2025 11:59:05 +0200 Subject: [PATCH 2/5] remove redundant else --- src/Core/Components/DataGrid/Columns/PropertyColumn.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs index 345429cc56..d19787d994 100644 --- a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs +++ b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs @@ -131,10 +131,8 @@ protected override void OnParametersSet() .MakeGenericMethod(typeof(TProp)); return (Func)method.Invoke(null, [getter, format])!; } - else - { - return CreateReferenceTypeFormatter((Func)(object)getter, format); - } + + 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)}'."); From 26682b3981d70cdefcd1e24dd96fd47c5fac8740 Mon Sep 17 00:00:00 2001 From: Tyme Bleyaert Date: Thu, 14 Aug 2025 13:29:07 +0200 Subject: [PATCH 3/5] resolve nitpicks --- .../Components/DataGrid/Columns/PropertyColumn.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs index d19787d994..18857cc502 100644 --- a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs +++ b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs @@ -64,7 +64,7 @@ protected override void OnParametersSet() if (!string.IsNullOrEmpty(Format)) { - _cellTextFunc = CreateFormatter(compiledPropertyExpression, Format); + _cellTextFunc = CreateFormatter(compiledPropertyExpression, Format); } else { @@ -138,22 +138,19 @@ protected override void OnParametersSet() 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 + 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) + 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) + private static Func CreateNullableValueTypeFormatter(Func getter, string format) where T : struct, IFormattable { return item => getter(item)?.ToString(format, null); From 19b8c84ad15ecc007a62389ebc2f2125979e29b6 Mon Sep 17 00:00:00 2001 From: Tyme Bleyaert Date: Thu, 14 Aug 2025 13:34:59 +0200 Subject: [PATCH 4/5] inlined the parameters of CreateFormat --- src/Core/Components/DataGrid/Columns/PropertyColumn.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs index 18857cc502..ef909917cb 100644 --- a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs +++ b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs @@ -105,8 +105,7 @@ protected override void OnParametersSet() } } - private static Func CreateFormatter( - Func getter, string format) + private static Func CreateFormatter(Func getter, string format) { var closedType = typeof(PropertyColumn<,>).MakeGenericType(typeof(TGridItem), typeof(TProp)); From 7e39e37b8f1e692dccb0741217666cb2a1fa84c9 Mon Sep 17 00:00:00 2001 From: Tyme Bleyaert Date: Thu, 14 Aug 2025 13:41:51 +0200 Subject: [PATCH 5/5] Add comment to explain why double cast is necessary --- src/Core/Components/DataGrid/Columns/PropertyColumn.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs index ef909917cb..e247de1541 100644 --- a/src/Core/Components/DataGrid/Columns/PropertyColumn.cs +++ b/src/Core/Components/DataGrid/Columns/PropertyColumn.cs @@ -131,6 +131,7 @@ protected override void OnParametersSet() 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); }