Skip to content

Commit c4a900d

Browse files
Tyme-BleyaertTyme Bleyaertvnbaaij
authored
[Property Column] Remove boxing when formatting the property. (#4070)
* Remove boxing when formatting the property in property columns. * remove redundant else * resolve nitpicks * inlined the parameters of CreateFormat * Add comment to explain why double cast is necessary --------- Co-authored-by: Tyme Bleyaert <Tyme.Blyaert@kenze.be> Co-authored-by: Vincent Baaij <vnbaaij@outlook.com>
1 parent c758526 commit c4a900d

File tree

3 files changed

+152
-16
lines changed

3 files changed

+152
-16
lines changed

src/Core/Components/DataGrid/Columns/PropertyColumn.cs

Lines changed: 54 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -64,18 +64,7 @@ protected override void OnParametersSet()
6464

6565
if (!string.IsNullOrEmpty(Format))
6666
{
67-
// TODO: Consider using reflection to avoid having to box every value just to call IFormattable.ToString
68-
// For example, define a method "string Type<U>(Func<TGridItem, U> property) where U: IFormattable", and
69-
// then construct the closed type here with U=TProp when we know TProp implements IFormattable
70-
71-
// If the type is nullable, we're interested in formatting the underlying type
72-
var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp));
73-
if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp)))
74-
{
75-
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
76-
}
77-
78-
_cellTextFunc = item => ((IFormattable?)compiledPropertyExpression!(item))?.ToString(Format, null);
67+
_cellTextFunc = CreateFormatter(compiledPropertyExpression, Format);
7968
}
8069
else
8170
{
@@ -87,10 +76,8 @@ protected override void OnParametersSet()
8776
{
8877
return (value as Enum)?.GetDisplayName();
8978
}
90-
else
91-
{
92-
return value?.ToString();
93-
}
79+
80+
return value?.ToString();
9481
};
9582
}
9683
if (Sortable.HasValue)
@@ -118,6 +105,57 @@ protected override void OnParametersSet()
118105
}
119106
}
120107

108+
private static Func<TGridItem, string?> CreateFormatter(Func<TGridItem, TProp> getter, string format)
109+
{
110+
var closedType = typeof(PropertyColumn<,>).MakeGenericType(typeof(TGridItem), typeof(TProp));
111+
112+
//Nullable struct
113+
if (Nullable.GetUnderlyingType(typeof(TProp)) is Type underlying &&
114+
typeof(IFormattable).IsAssignableFrom(underlying))
115+
{
116+
var method = closedType
117+
.GetMethod(nameof(CreateNullableValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
118+
.MakeGenericMethod(underlying);
119+
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
120+
}
121+
122+
123+
if (typeof(IFormattable).IsAssignableFrom(typeof(TProp)))
124+
{
125+
//Struct
126+
if (typeof(TProp).IsValueType)
127+
{
128+
var method = closedType
129+
.GetMethod(nameof(CreateValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
130+
.MakeGenericMethod(typeof(TProp));
131+
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
132+
}
133+
134+
//Double cast required because CreateReferenceTypeFormatter required the TProp to be a reference type which implements IFormattable.
135+
return CreateReferenceTypeFormatter((Func<TGridItem, IFormattable?>)(object)getter, format);
136+
}
137+
138+
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
139+
}
140+
141+
private static Func<TGridItem, string?> CreateReferenceTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
142+
where T : class, IFormattable
143+
{
144+
return item => getter(item)?.ToString(format, null);
145+
}
146+
147+
private static Func<TGridItem, string?> CreateValueTypeFormatter<T>(Func<TGridItem, T> getter, string format)
148+
where T : struct, IFormattable
149+
{
150+
return item => getter(item).ToString(format, null);
151+
}
152+
153+
private static Func<TGridItem, string?> CreateNullableValueTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
154+
where T : struct, IFormattable
155+
{
156+
return item => getter(item)?.ToString(format, null);
157+
}
158+
121159
/// <inheritdoc />
122160
protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item)
123161
=> builder.AddContent(0, _cellTextFunc?.Invoke(item));
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
@using Xunit;
2+
@inherits TestContext
3+
@code
4+
{
5+
[Fact]
6+
public void PropertyGrid_Should_Format_ValueTypes()
7+
{
8+
var cut = Render(
9+
@<FluentDataGrid Items="@People" TGridItem="Person">
10+
<PropertyColumn Property="@(p => p.PersonId)" Format="D4" />
11+
</FluentDataGrid>
12+
);
13+
14+
var firstCellText = cut.Find("td").TextContent;
15+
Assert.Equal("0001", firstCellText);
16+
}
17+
18+
[Fact]
19+
public void PropertyGrid_Should_Format_NullableValueTypes()
20+
{
21+
var cut = Render(
22+
@<FluentDataGrid Items="@People" TGridItem="Person">
23+
<PropertyColumn Property="@(p => p.BirthDate)" Format="m" />
24+
</FluentDataGrid>
25+
);
26+
27+
var firstCellText = cut.Find("td").TextContent;
28+
Assert.Equal(_people[0].BirthDate!.Value.ToString("m"), firstCellText);
29+
}
30+
31+
[Fact]
32+
public void PropertyGrid_Should_Format_ReferenceTypes()
33+
{
34+
var cut = Render(
35+
@<FluentDataGrid Items="@People" TGridItem="Person">
36+
<PropertyColumn Property="@(p => p.Name)" Format="{0} test" />
37+
</FluentDataGrid>
38+
);
39+
40+
var firstCellText = cut.Find("td").TextContent;
41+
Assert.Equal(string.Format("{0} test", _people[0].Name), firstCellText);
42+
}
43+
44+
45+
[Fact]
46+
public void PropertyGrid_Should_Throw_When_FormatUsedOnNonFormattableProperty()
47+
{
48+
Assert.Throws<InvalidOperationException>(() => Render(
49+
@<FluentDataGrid Items="@People" TGridItem="Person">
50+
<PropertyColumn Property="@(p => p.NickName)" Format="{0} test" />
51+
</FluentDataGrid>
52+
));
53+
}
54+
55+
56+
}
57+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
// ------------------------------------------------------------------------
2+
// This file is licensed to you under the MIT License.
3+
// ------------------------------------------------------------------------
4+
5+
using Bunit;
6+
using Microsoft.Extensions.DependencyInjection;
7+
8+
namespace Microsoft.FluentUI.AspNetCore.Components.Tests.PropertyColumn;
9+
public partial class PropertyColumnFormatterTests
10+
{
11+
private protected record Person(int PersonId, CustomFormattable Name, DateOnly? BirthDate, string NickName)
12+
{
13+
public bool Selected { get; set; }
14+
};
15+
16+
private protected class CustomFormattable : IFormattable
17+
{
18+
private readonly string _value;
19+
public CustomFormattable(string value) => _value = value;
20+
public string ToString(string? format, IFormatProvider? provider) =>
21+
string.IsNullOrEmpty(format) ? _value : string.Format(format, _value);
22+
}
23+
24+
private readonly IList<Person> _people =
25+
[
26+
new Person(1, new("Jean Martin"), new DateOnly(1985, 3, 16), string.Empty),
27+
new Person(2, new("Kenji Sato"), new DateOnly(2004, 1, 9), string.Empty),
28+
new Person(3, new("Julie Smith"), new DateOnly(1958, 10, 10), string.Empty),
29+
];
30+
31+
private protected IQueryable<Person> People => _people.AsQueryable();
32+
33+
public PropertyColumnFormatterTests()
34+
{
35+
JSInterop.Mode = JSRuntimeMode.Loose;
36+
Services.AddSingleton(LibraryConfiguration.ForUnitTests);
37+
38+
var keycodeService = new KeyCodeService();
39+
Services.AddScoped<IKeyCodeService>(factory => keycodeService);
40+
}
41+
}

0 commit comments

Comments
 (0)