Skip to content

Commit a516910

Browse files
vnbaaijdvoituron
andauthored
[dev-v5] Pull in v4 PRs (part 2) (#4218)
* Implement #4036 * Implement #4070 * Implement #4112 * Implement #4116 * Add extra test. Brings back code coverage to 100% for Row and Cell * Implement #4172 * Implement #4177 * - Remove NoTabbing parameter (not being used) - Exclude ErrorContent from code coverage - Add (partial) ErrorContentTest, add IsFixed test, update tests * Implement #4178 Related Work Items: #41 * Add CustomIcon and IconsExtensions + tests --------- Co-authored-by: Denis Voituron <[email protected]>
1 parent 34e4ce5 commit a516910

12 files changed

+757
-347
lines changed

src/Core/Components/DataGrid/FluentDataGrid.razor

Lines changed: 284 additions & 231 deletions
Large diffs are not rendered by default.

src/Core/Components/DataGrid/FluentDataGrid.razor.cs

Lines changed: 67 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
2626

2727
internal const string EMPTY_CONTENT_ROW_CLASS = "empty-content-row";
2828
internal const string LOADING_CONTENT_ROW_CLASS = "loading-content-row";
29+
internal const string ERROR_CONTENT_ROW_CLASS = "error-content-row";
2930

3031
private ElementReference? _gridReference;
3132
private Virtualize<(int, TGridItem)>? _virtualizeComponent;
@@ -44,11 +45,13 @@ public partial class FluentDataGrid<TGridItem> : FluentComponentBase, IHandleEve
4445
private readonly RenderFragment _renderNonVirtualizedRows;
4546
private readonly RenderFragment _renderEmptyContent;
4647
private readonly RenderFragment _renderLoadingContent;
48+
private readonly RenderFragment _renderErrorContent;
4749
private string? _internalGridTemplateColumns;
4850
private PaginationState? _lastRefreshedPaginationState;
4951
private IQueryable<TGridItem>? _lastAssignedItems;
5052
private GridItemsProvider<TGridItem>? _lastAssignedItemsProvider;
5153
private CancellationTokenSource? _pendingDataLoadCancellationTokenSource;
54+
private Exception? _lastError;
5255
private GridItemsProviderRequest<TGridItem>? _lastRequest;
5356
private bool _forceRefreshData;
5457
private readonly EventCallbackSubscriber<PaginationState> _currentPageItemsChanged;
@@ -64,11 +67,12 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
6467
_renderNonVirtualizedRows = RenderNonVirtualizedRows;
6568
_renderEmptyContent = RenderEmptyContent;
6669
_renderLoadingContent = RenderLoadingContent;
70+
_renderErrorContent = RenderErrorContent;
6771

68-
// As a special case, we don't issue the first data load request until we've collected the initial set of columns
69-
// This is so we can apply default sort order (or any future per-column options) before loading data
70-
// We use EventCallbackSubscriber to safely hook this async operation into the synchronous rendering flow
71-
EventCallbackSubscriber<object?>? columnsFirstCollectedSubscriber = new(
72+
// As a special case, we don't issue the first data load request until we've collected the initial set of columns
73+
// This is so we can apply default sort order (or any future per-column options) before loading data
74+
// We use EventCallbackSubscriber to safely hook this async operation into the synchronous rendering flow
75+
EventCallbackSubscriber<object?>? columnsFirstCollectedSubscriber = new(
7276
EventCallback.Factory.Create<object?>(this, RefreshDataCoreAsync));
7377
columnsFirstCollectedSubscriber.SubscribeOrMove(_internalGridContext.ColumnsFirstCollected);
7478
}
@@ -231,13 +235,6 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
231235
[Parameter]
232236
public PaginationState? Pagination { get; set; }
233237

234-
/// <summary>
235-
/// Gets or sets a value indicating whether the component will not add itself to the tab queue.
236-
/// Default is false.
237-
/// </summary>
238-
[Parameter]
239-
public bool NoTabbing { get; set; }
240-
241238
/// <summary>
242239
/// Gets or sets a value indicating whether the grid should automatically generate a header row and its type.
243240
/// See <see cref="DataGridGeneratedHeaderType"/>
@@ -323,6 +320,27 @@ public FluentDataGrid(LibraryConfiguration configuration) : base(configuration)
323320
[Parameter]
324321
public RenderFragment? LoadingContent { get; set; }
325322

323+
/// <summary>
324+
/// Gets or sets the callback that is invoked when the asynchronous loading state of items changes and <see cref="IAsyncQueryExecutor"/> is used.
325+
/// </summary>
326+
/// <remarks>The callback receives a <see langword="true"/> value when items start loading
327+
/// and a <see langword="false"/> value when the loading process completes.</remarks>
328+
[ExcludeFromCodeCoverage(Justification = "This method requires a db connection and is to complex to be tested with bUnit.")]
329+
[Parameter]
330+
public EventCallback<bool> OnItemsLoading { get; set; }
331+
332+
/// <summary>
333+
/// Gets or sets a delegate that determines whether a given exception should be handled.
334+
/// </summary>
335+
[Parameter]
336+
public Func<Exception, bool>? HandleLoadingError { get; set; }
337+
338+
/// <summary>
339+
/// Gets or sets the content to render when an error occurs.
340+
/// </summary>
341+
[Parameter]
342+
public RenderFragment<Exception>? ErrorContent { get; set; }
343+
326344
/// <summary>
327345
/// Sets <see cref="GridTemplateColumns"/> to automatically fit the columns to the available width as best it can.
328346
/// </summary>
@@ -732,9 +750,6 @@ private async Task RefreshDataCoreAsync()
732750
// (2) We won't know what slice of data to query for
733751
await _virtualizeComponent.RefreshDataAsync();
734752
_pendingDataLoadCancellationTokenSource = null;
735-
736-
StateHasChanged();
737-
return;
738753
}
739754

740755
// If we're not using Virtualize, we build and execute a request against the items provider directly
@@ -824,7 +839,7 @@ private async Task RefreshDataCoreAsync()
824839
Pagination?.SetTotalItemCountAsync(_internalGridContext.TotalItemCount);
825840
}
826841

827-
if (_internalGridContext.TotalItemCount > 0 && Loading is null)
842+
if ((_internalGridContext.TotalItemCount > 0 && Loading is null) || _lastError != null)
828843
{
829844
Loading = false;
830845
_ = InvokeAsync(StateHasChanged);
@@ -844,6 +859,7 @@ private async Task RefreshDataCoreAsync()
844859
// Normalizes all the different ways of configuring a data source so they have common GridItemsProvider-shaped API
845860
private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestAsync(GridItemsProviderRequest<TGridItem> request)
846861
{
862+
CheckAndResetLastError();
847863
try
848864
{
849865
if (ItemsProvider is not null)
@@ -860,6 +876,11 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
860876

861877
if (Items is not null)
862878
{
879+
if (_asyncQueryExecutor is not null)
880+
{
881+
await OnItemsLoading.InvokeAsync(true);
882+
}
883+
863884
var totalItemCount = _asyncQueryExecutor is null ? Items.Count() : await _asyncQueryExecutor.CountAsync(Items, request.CancellationToken);
864885
_internalGridContext.TotalItemCount = totalItemCount;
865886
IQueryable<TGridItem>? result;
@@ -880,14 +901,43 @@ private async ValueTask<GridItemsProviderResult<TGridItem>> ResolveItemsRequestA
880901
return GridItemsProviderResult.From(resultArray, totalItemCount);
881902
}
882903
}
883-
catch (OperationCanceledException oce) when (oce.CancellationToken == request.CancellationToken)
904+
catch (OperationCanceledException oce) when (oce.CancellationToken == request.CancellationToken) // No-op; we canceled the operation, so it's fine to suppress this exception.
905+
{
906+
}
907+
catch (Exception ex) when (HandleLoadingError?.Invoke(ex) == true)
908+
{
909+
_lastError = ex.GetBaseException();
910+
}
911+
finally
884912
{
885-
// No-op; we canceled the operation, so it's fine to suppress this exception.
913+
if (Items is not null && _asyncQueryExecutor is not null)
914+
{
915+
CheckAndResetLoading();
916+
await OnItemsLoading.InvokeAsync(false);
917+
}
886918
}
887919

888920
return GridItemsProviderResult.From(Array.Empty<TGridItem>(), 0);
889921
}
890922

923+
private void CheckAndResetLoading()
924+
{
925+
if (Loading == true)
926+
{
927+
Loading = false;
928+
StateHasChanged();
929+
}
930+
}
931+
932+
private void CheckAndResetLastError()
933+
{
934+
if (_lastError != null)
935+
{
936+
_lastError = null;
937+
StateHasChanged();
938+
}
939+
}
940+
891941
private string AriaSortValue(ColumnBase<TGridItem> column)
892942
=> _sortByColumn == column
893943
? (_sortByAscending ? "ascending" : "descending")

src/Core/Components/DataGrid/FluentDataGridCell.razor.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,15 @@ public FluentDataGridCell(LibraryConfiguration configuration) : base(configurati
3636

3737
/// <summary />
3838
protected string? StyleValue => DefaultStyleBuilder
39-
.AddStyle("grid-column", GridColumn.ToString(CultureInfo.InvariantCulture), () => !Grid.EffectiveLoadingValue && (Grid.Items is not null || Grid.ItemsProvider is not null) && Grid.DisplayMode == DataGridDisplayMode.Grid)
39+
.AddStyle("grid-column", GridColumn.ToString(CultureInfo.InvariantCulture), () => !Grid.EffectiveLoadingValue && (Grid.Items is not null || Grid.ItemsProvider is not null) && InternalGridContext.TotalItemCount > 0 && Grid.DisplayMode == DataGridDisplayMode.Grid)
4040
.AddStyle("text-align", "center", Column is SelectColumn<TGridItem>)
4141
.AddStyle("align-content", "center", Column is SelectColumn<TGridItem>)
4242
.AddStyle("min-width", Column?.MinWidth, Owner.RowType is DataGridRowType.Header or DataGridRowType.StickyHeader)
4343
.AddStyle("padding-top", "10px", Column is SelectColumn<TGridItem> && (Grid.RowSize == DataGridRowSize.Medium || Owner.RowType == DataGridRowType.Header))
4444
.AddStyle("padding-top", "6px", Column is SelectColumn<TGridItem> && Grid.RowSize == DataGridRowSize.Small && Owner.RowType == DataGridRowType.Default)
4545
.AddStyle("width", Column?.Width, !string.IsNullOrEmpty(Column?.Width) && Grid.DisplayMode == DataGridDisplayMode.Table)
4646
.AddStyle("height", $"{Grid.ItemSize.ToString(CultureInfo.InvariantCulture):0}px", () => !Grid.EffectiveLoadingValue && Grid.Virtualize)
47-
.AddStyle("height", $"{((int)Grid.RowSize).ToString(CultureInfo.InvariantCulture)}px", () => !Grid.EffectiveLoadingValue && !Grid.Virtualize && !Grid.MultiLine && (Grid.Items is not null || Grid.ItemsProvider is not null))
47+
.AddStyle("height", $"{((int)Grid.RowSize).ToString(CultureInfo.InvariantCulture)}px", () => !Grid.EffectiveLoadingValue && !Grid.Virtualize && !Grid.MultiLine && (Grid.Items is not null || Grid.ItemsProvider is not null) && InternalGridContext.TotalItemCount > 0)
4848
.AddStyle("height", "100%", Grid.MultiLine)
4949
.AddStyle("min-height", "44px", Owner.RowType != DataGridRowType.Default)
5050
.AddStyle("z-index", ZIndex.DataGridHeaderPopup.ToString(CultureInfo.InvariantCulture), CellType == DataGridCellType.ColumnHeader && Grid._columns.Count > 0 && Grid.UseMenuService)
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// ------------------------------------------------------------------------
2+
// This file is licensed to you under the MIT License.
3+
// ------------------------------------------------------------------------
4+
5+
namespace Microsoft.FluentUI.AspNetCore.Components;
6+
7+
/// <summary>
8+
/// Custom icon loaded from <see cref="IconsExtensions.GetInstance(IconInfo, bool?)"/>
9+
/// </summary>
10+
public class CustomIcon : Icon
11+
{
12+
/// <summary>
13+
/// Initializes a new instance of the <see cref="CustomIcon"/> class.
14+
/// </summary>
15+
public CustomIcon()
16+
: base(string.Empty, IconVariant.Regular, IconSize.Size24, string.Empty)
17+
{ }
18+
19+
/// <summary>
20+
/// Initializes a new instance of the <see cref="CustomIcon"/> class.
21+
/// </summary>
22+
/// <param name="icon"></param>
23+
public CustomIcon(Icon icon)
24+
: base(icon.Name, icon.Variant, icon.Size, icon.Content)
25+
{ }
26+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
// ------------------------------------------------------------------------
2+
// This file is licensed to you under the MIT License.
3+
// ------------------------------------------------------------------------
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Reflection;
7+
8+
namespace Microsoft.FluentUI.AspNetCore.Components;
9+
10+
/// <summary />
11+
public static partial class IconsExtensions
12+
{
13+
private const string Namespace = "Microsoft.FluentUI.AspNetCore.Components";
14+
private const string LibraryName = "Microsoft.FluentUI.AspNetCore.Components.Icons.{0}"; // {0} must be replaced with the "Variant": Regular, Filled, etc.
15+
16+
/// <summary>
17+
/// Returns a new instance of the icon.
18+
/// </summary>
19+
/// <param name="icon">The <see cref="IconInfo"/> to instantiate.</param>
20+
/// <param name="throwOnError">true to throw an exception if the type is not found (default); false to return null.</param>
21+
/// <remarks>
22+
/// This method requires dynamic access to code. This code may be removed by the trimmer.
23+
/// If the assembly is not yet loaded, it will be loaded by the method `Assembly.Load`.
24+
/// To avoid any issues, the assembly must be loaded before calling this method (e.g. adding an icon in your code).
25+
/// </remarks>
26+
/// <returns></returns>
27+
/// <exception cref="ArgumentException">Raised when the <see cref="IconInfo.Name"/> is not found in predefined icons.</exception>
28+
[ExcludeFromCodeCoverage(Justification = "We can't test the Icon.* DLLs here")]
29+
[RequiresUnreferencedCode("This method requires dynamic access to code. This code may be removed by the trimmer.")]
30+
public static CustomIcon GetInstance(this IconInfo icon, bool? throwOnError = true)
31+
{
32+
var assemblyName = string.Format(System.Globalization.CultureInfo.InvariantCulture, LibraryName, icon.Variant);
33+
var assembly = GetAssembly(assemblyName);
34+
35+
if (assembly != null)
36+
{
37+
var allIcons = assembly.GetTypes().Where(i => i.BaseType == typeof(Icon));
38+
39+
// Ex. Microsoft.FluentUI.AspNetCore.Components.Icons.Filled.Size10+PresenceAvailable
40+
var iconFullName = $"{Namespace}.Icons.{icon.Variant}.Size{(int)icon.Size}+{icon.Name}";
41+
var iconType = allIcons.FirstOrDefault(i => string.Equals(i.FullName, iconFullName, StringComparison.InvariantCultureIgnoreCase));
42+
43+
if (iconType != null)
44+
{
45+
var newIcon = Activator.CreateInstance(iconType);
46+
if (newIcon != null)
47+
{
48+
return new CustomIcon((Icon)newIcon);
49+
}
50+
}
51+
}
52+
53+
if (throwOnError == true || throwOnError == null)
54+
{
55+
throw new ArgumentException(
56+
string.Format(
57+
System.Globalization.CultureInfo.InvariantCulture,
58+
"Icon 'Icons.{0}.Size{1}.{2}' not found.",
59+
icon.Variant.ToString(),
60+
((int)icon.Size).ToString(System.Globalization.CultureInfo.InvariantCulture),
61+
icon.Name),
62+
nameof(icon));
63+
}
64+
65+
return default!;
66+
}
67+
68+
/// <summary>
69+
/// Tries to return a new instance of the icon.
70+
/// </summary>
71+
/// <param name="icon">The <see cref="IconInfo"/> to instantiate.</param>
72+
/// <param name="result">When this method returns, contains the <see cref="CustomIcon"/> value if the conversion succeeded, or null if the conversion failed. This parameter is passed uninitialized; any value originally supplied in result will be overwritten.</param>
73+
/// <remarks>
74+
/// This method requires dynamic access to code. This code may be removed by the trimmer.
75+
/// If the assembly is not yet loaded, it will be loaded by the method `Assembly.Load`.
76+
/// To avoid any issues, the assembly must be loaded before calling this method (e.g. adding an icon in your code).
77+
/// </remarks>
78+
/// <returns>True if the icon was found and created; otherwise, false.</returns>
79+
[RequiresUnreferencedCode("This method requires dynamic access to code. This code may be removed by the trimmer.")]
80+
public static bool TryGetInstance(this IconInfo icon, out CustomIcon? result)
81+
{
82+
result = GetInstance(icon, throwOnError: false);
83+
return result != null;
84+
}
85+
86+
/// <summary>
87+
/// Returns a new instance of the icon.
88+
/// </summary>
89+
/// <remarks>
90+
/// This method requires dynamic access to code. This code may be removed by the trimmer.
91+
/// </remarks>
92+
/// <returns></returns>
93+
/// <exception cref="ArgumentException">Raised when the <see cref="IconInfo.Name"/> is not found in predefined icons.</exception>
94+
[ExcludeFromCodeCoverage(Justification = "We can't test the Icon.* DLLs here.")]
95+
[RequiresUnreferencedCode("This method requires dynamic access to code. This code may be removed by the trimmer.")]
96+
public static IEnumerable<IconInfo> GetAllIcons()
97+
{
98+
var allIcons = new List<IconInfo>();
99+
100+
foreach (var variant in Enum.GetValues(typeof(IconVariant)).Cast<IconVariant>())
101+
{
102+
var assemblyName = string.Format(System.Globalization.CultureInfo.InvariantCulture, LibraryName, variant);
103+
var assembly = GetAssembly(assemblyName);
104+
105+
if (assembly != null)
106+
{
107+
var allTypes = assembly.GetTypes().Where(i => i.BaseType == typeof(Icon) && !string.Equals(i.Name, nameof(CustomIcon), StringComparison.OrdinalIgnoreCase));
108+
109+
allIcons.AddRange(allTypes.Select(type => Activator.CreateInstance(type) as IconInfo ?? new IconInfo()));
110+
}
111+
}
112+
113+
return allIcons;
114+
}
115+
116+
/// <summary />
117+
public static IEnumerable<IconInfo> AllIcons
118+
{
119+
[RequiresUnreferencedCode("This method requires dynamic access to code. This code may be removed by the trimmer.")]
120+
get
121+
{
122+
return GetAllIcons();
123+
}
124+
}
125+
126+
/// <summary />
127+
private static Assembly? GetAssembly(string assemblyName)
128+
{
129+
try
130+
{
131+
return AppDomain.CurrentDomain
132+
.GetAssemblies()
133+
.FirstOrDefault(i => string.Equals(i.ManifestModule.Name, assemblyName + ".dll", StringComparison.OrdinalIgnoreCase))
134+
?? Assembly.Load(assemblyName);
135+
136+
}
137+
catch (Exception)
138+
{
139+
return null;
140+
}
141+
}
142+
}

0 commit comments

Comments
 (0)