Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 54 additions & 16 deletions src/Core/Components/DataGrid/Columns/PropertyColumn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,18 +64,7 @@

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<U>(Func<TGridItem, U> 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
{
Expand All @@ -87,10 +76,8 @@
{
return (value as Enum)?.GetDisplayName();
}
else
{
return value?.ToString();
}

return value?.ToString();
};
}
if (Sortable.HasValue)
Expand Down Expand Up @@ -118,6 +105,57 @@
}
}

private static Func<TGridItem, string?> CreateFormatter(Func<TGridItem, TProp> 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<TGridItem, string?>)method.Invoke(null, [getter, format])!;
}

Check warning on line 121 in src/Core/Components/DataGrid/Columns/PropertyColumn.cs

View workflow job for this annotation

GitHub Actions / Build and Test Core Lib

Avoid multiple blank lines (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000)

Check warning on line 121 in src/Core/Components/DataGrid/Columns/PropertyColumn.cs

View workflow job for this annotation

GitHub Actions / Build and Test Core Lib

Avoid multiple blank lines (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000)

Check warning on line 121 in src/Core/Components/DataGrid/Columns/PropertyColumn.cs

View workflow job for this annotation

GitHub Actions / Build and Deploy Demo site

Avoid multiple blank lines (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000)

Check warning on line 121 in src/Core/Components/DataGrid/Columns/PropertyColumn.cs

View workflow job for this annotation

GitHub Actions / Build and Deploy Demo site

Avoid multiple blank lines (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/style-rules/ide2000)

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<TGridItem, string?>)method.Invoke(null, [getter, format])!;
}

//Double cast required because CreateReferenceTypeFormatter required the TProp to be a reference type which implements IFormattable.
return CreateReferenceTypeFormatter((Func<TGridItem, IFormattable?>)(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<TGridItem, string?> CreateReferenceTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
where T : class, IFormattable
{
return item => getter(item)?.ToString(format, null);
}

private static Func<TGridItem, string?> CreateValueTypeFormatter<T>(Func<TGridItem, T> getter, string format)
where T : struct, IFormattable
{
return item => getter(item).ToString(format, null);
}

private static Func<TGridItem, string?> CreateNullableValueTypeFormatter<T>(Func<TGridItem, T?> getter, string format)
where T : struct, IFormattable
{
return item => getter(item)?.ToString(format, null);
}

/// <inheritdoc />
protected internal override void CellContent(RenderTreeBuilder builder, TGridItem item)
=> builder.AddContent(0, _cellTextFunc?.Invoke(item));
Expand Down
57 changes: 57 additions & 0 deletions tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
ο»Ώ@using Xunit;
@inherits TestContext
@code
{
[Fact]
public void PropertyGrid_Should_Format_ValueTypes()
{
var cut = Render(
@<FluentDataGrid Items="@People" TGridItem="Person">
<PropertyColumn Property="@(p => p.PersonId)" Format="D4" />
</FluentDataGrid>
);

var firstCellText = cut.Find("td").TextContent;
Assert.Equal("0001", firstCellText);
}

[Fact]
public void PropertyGrid_Should_Format_NullableValueTypes()
{
var cut = Render(
@<FluentDataGrid Items="@People" TGridItem="Person">
<PropertyColumn Property="@(p => p.BirthDate)" Format="m" />
</FluentDataGrid>
);

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(
@<FluentDataGrid Items="@People" TGridItem="Person">
<PropertyColumn Property="@(p => p.Name)" Format="{0} test" />
</FluentDataGrid>
);

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<InvalidOperationException>(() => Render(
@<FluentDataGrid Items="@People" TGridItem="Person">
<PropertyColumn Property="@(p => p.NickName)" Format="{0} test" />
</FluentDataGrid>
));
}


}

41 changes: 41 additions & 0 deletions tests/Core/PropertyColumn/PropertyColumnFormatterTests.razor.cs
Original file line number Diff line number Diff line change
@@ -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<Person> _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<Person> People => _people.AsQueryable();

public PropertyColumnFormatterTests()
{
JSInterop.Mode = JSRuntimeMode.Loose;
Services.AddSingleton(LibraryConfiguration.ForUnitTests);

var keycodeService = new KeyCodeService();
Services.AddScoped<IKeyCodeService>(factory => keycodeService);
}
}
Loading