Skip to content

Commit c265876

Browse files
Tyme BleyaertTyme Bleyaert
authored andcommitted
Remove boxing when formatting the property in property columns.
1 parent 6599cc8 commit c265876

File tree

3 files changed

+157
-16
lines changed

3 files changed

+157
-16
lines changed

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

Lines changed: 59 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,62 @@ protected override void OnParametersSet()
118105
}
119106
}
120107

108+
private static Func<TGridItem, string?> CreateFormatter(
109+
Func<TGridItem, TProp> getter, string format)
110+
{
111+
var closedType = typeof(PropertyColumn<,>).MakeGenericType(typeof(TGridItem), typeof(TProp));
112+
113+
//Nullable struct
114+
if (Nullable.GetUnderlyingType(typeof(TProp)) is Type underlying &&
115+
typeof(IFormattable).IsAssignableFrom(underlying))
116+
{
117+
var method = closedType
118+
.GetMethod(nameof(CreateNullableValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
119+
.MakeGenericMethod(underlying);
120+
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
121+
}
122+
123+
124+
if (typeof(IFormattable).IsAssignableFrom(typeof(TProp)))
125+
{
126+
//Struct
127+
if (typeof(TProp).IsValueType)
128+
{
129+
var method = closedType
130+
.GetMethod(nameof(CreateValueTypeFormatter), BindingFlags.NonPublic | BindingFlags.Static)!
131+
.MakeGenericMethod(typeof(TProp));
132+
return (Func<TGridItem, string?>)method.Invoke(null, [getter, format])!;
133+
}
134+
else
135+
{
136+
return CreateReferenceTypeFormatter((Func<TGridItem, IFormattable?>)(object)getter, format);
137+
}
138+
}
139+
140+
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
141+
}
142+
143+
private static Func<TGridItem, string?> CreateReferenceTypeFormatter<T>(
144+
Func<TGridItem, T?> getter, string format)
145+
where T : class, IFormattable
146+
{
147+
return item => getter(item)?.ToString(format, null);
148+
}
149+
150+
private static Func<TGridItem, string?> CreateValueTypeFormatter<T>(
151+
Func<TGridItem, T> getter, string format)
152+
where T : struct, IFormattable
153+
{
154+
return item => getter(item).ToString(format, null);
155+
}
156+
157+
private static Func<TGridItem, string?> CreateNullableValueTypeFormatter<T>(
158+
Func<TGridItem, T?> getter, string format)
159+
where T : struct, IFormattable
160+
{
161+
return item => getter(item)?.ToString(format, null);
162+
}
163+
121164
/// <inheritdoc />
122165
protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item)
123166
=> 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)