Skip to content

Commit ecdbe6b

Browse files
committed
Fixed race condition in the disposing of the QuickGrid
1 parent 08cadfe commit ecdbe6b

File tree

4 files changed

+158
-3
lines changed

4 files changed

+158
-3
lines changed

src/Components/Components/src/Microsoft.AspNetCore.Components.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Tests" />
9090
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Endpoints.Tests" />
9191
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.Web.Tests" />
92+
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.QuickGrid.Tests" />
9293
<InternalsVisibleTo Include="Microsoft.AspNetCore.Components.ProtectedBrowserStorage.Tests" />
9394
<InternalsVisibleTo Include="Components.TestServer" />
9495
<InternalsVisibleTo Include="DynamicProxyGenAssembly2" Key="$(MoqPublicKey)" />

src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/QuickGrid.razor.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,8 @@ public partial class QuickGrid<TGridItem> : IAsyncDisposable
152152
// If the PaginationState mutates, it raises this event. We use it to trigger a re-render.
153153
private readonly EventCallbackSubscriber<PaginationState> _currentPageItemsChanged;
154154

155+
private bool _disposeBool;
156+
155157
/// <summary>
156158
/// Constructs an instance of <see cref="QuickGrid{TGridItem}"/>.
157159
/// </summary>
@@ -206,6 +208,11 @@ protected override async Task OnAfterRenderAsync(bool firstRender)
206208
if (firstRender)
207209
{
208210
_jsModule = await JS.InvokeAsync<IJSObjectReference>("import", "./_content/Microsoft.AspNetCore.Components.QuickGrid/QuickGrid.razor.js");
211+
if (_disposeBool)
212+
{
213+
// If the component has been disposed while JS module was being loaded, we don't need to continue
214+
return;
215+
}
209216
_jsEventDisposable = await _jsModule.InvokeAsync<IJSObjectReference>("init", _tableReference);
210217
}
211218

@@ -434,6 +441,7 @@ private string GridClass()
434441
/// <inheritdoc />
435442
public async ValueTask DisposeAsync()
436443
{
444+
_disposeBool = true;
437445
_currentPageItemsChanged.Dispose();
438446

439447
try
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.AspNetCore.Components.Rendering;
5+
using Microsoft.AspNetCore.Components.RenderTree;
6+
using Microsoft.AspNetCore.Components.QuickGrid;
7+
using Microsoft.AspNetCore.Components.Test.Helpers;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.JSInterop;
10+
11+
namespace Microsoft.AspNetCore.Components.QuickGrid.Test;
12+
13+
public class GridRaceConditionTest
14+
{
15+
private TestRenderer _renderer = new();
16+
private TaskCompletionSource _tcs = new();
17+
18+
public GridRaceConditionTest()
19+
{
20+
var testJsRuntime = new TestJsRuntime(_tcs);
21+
var serviceProvider = new ServiceCollection()
22+
.AddSingleton<IJSRuntime>(testJsRuntime)
23+
.BuildServiceProvider();
24+
_renderer = new(serviceProvider);
25+
}
26+
27+
[Fact]
28+
public async Task CanCorrectlyDisposeAsync()
29+
{
30+
var testComponent = new TestComponent();
31+
32+
var componentId = _renderer.AssignRootComponentId(testComponent);
33+
_renderer.RenderRootComponent(componentId);
34+
await Task.Delay(10);
35+
_renderer.RenderRootComponent(componentId);
36+
_tcs.SetResult();
37+
}
38+
}
39+
40+
internal class TestComponent : ComponentBase
41+
{
42+
private bool _firstRender = true;
43+
private PaginationState _pagination = new() { ItemsPerPage = 2 };
44+
45+
internal class Person
46+
{
47+
public int Id { get; set; }
48+
public string Name { get; set; } = string.Empty;
49+
public int Age { get; set; }
50+
}
51+
52+
private IQueryable<Person> _people = new List<Person>
53+
{
54+
new Person { Id = 1, Name = "John Doe", Age = 30 },
55+
new Person { Id = 2, Name = "Jane Smith", Age = 25 },
56+
new Person { Id = 3, Name = "Alice Johnson", Age = 22 }
57+
}.AsQueryable();
58+
59+
protected override void BuildRenderTree(RenderTreeBuilder builder)
60+
{
61+
if (_firstRender)
62+
{
63+
//Render the QuickGrid
64+
builder.OpenComponent<QuickGrid<Person>>(0);
65+
builder.AddAttribute(1, "Items", _people);
66+
builder.AddAttribute(2, "Pagination", _pagination);
67+
builder.AddAttribute(3, nameof(QuickGrid<Person>.ChildContent),
68+
(RenderFragment)(builder => BuildColumnsRenderFragment(builder)));
69+
builder.CloseComponent();
70+
71+
builder.OpenComponent<Paginator>(4);
72+
builder.AddAttribute(5, "State", _pagination);
73+
builder.CloseComponent();
74+
_firstRender = false;
75+
}
76+
}
77+
78+
protected void BuildColumnsRenderFragment(RenderTreeBuilder builder)
79+
{
80+
//Render the PropertyColumn for Id
81+
builder.OpenComponent<PropertyColumn<Person, int>>(0);
82+
builder.AddAttribute(1, nameof(PropertyColumn<Person, int>.Property),
83+
(System.Linq.Expressions.Expression<Func<Person, int>>)(p => p.Id));
84+
builder.AddAttribute(2, nameof(PropertyColumn<Person, int>.Sortable), true);
85+
builder.CloseComponent();
86+
87+
//Render the PropertyColumn for Name
88+
builder.OpenComponent<PropertyColumn<Person, string>>(3);
89+
builder.AddAttribute(4, nameof(PropertyColumn<Person, string>.Property),
90+
(System.Linq.Expressions.Expression<Func<Person, string>>)(p => p.Name));
91+
builder.AddAttribute(5, nameof(PropertyColumn<Person, string>.Sortable), true);
92+
builder.CloseComponent();
93+
94+
//Render the PropertyColumn for Age
95+
builder.OpenComponent<PropertyColumn<Person, int>>(6);
96+
builder.AddAttribute(7, nameof(PropertyColumn<Person, int>.Property),
97+
(System.Linq.Expressions.Expression<Func<Person, int>>)(p => p.Age));
98+
builder.AddAttribute(8, nameof(PropertyColumn<Person, int>.Sortable), true);
99+
builder.CloseComponent();
100+
}
101+
102+
public void TriggerRender()
103+
{
104+
InvokeAsync(StateHasChanged);
105+
}
106+
107+
protected override async Task OnAfterRenderAsync(bool firstRender)
108+
{
109+
if (firstRender)
110+
{
111+
await Task.Delay(1);
112+
StateHasChanged();
113+
}
114+
await base.OnAfterRenderAsync(firstRender);
115+
}
116+
}
117+
118+
internal class TestJsRuntime(TaskCompletionSource tcs) : IJSRuntime
119+
{
120+
private readonly TaskCompletionSource _tcs = tcs;
121+
122+
public async ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args = null)
123+
{
124+
if (identifier == "import" && args != null && args.Length > 0 && args[0] is string modulePath)
125+
{
126+
if (modulePath == "./_content/Microsoft.AspNetCore.Components.QuickGrid/QuickGrid.razor.js")
127+
{
128+
await _tcs.Task;
129+
return default!;
130+
}
131+
}
132+
throw new Exception("JS import was not correctly processed while disposing of the component.");
133+
}
134+
135+
public ValueTask<IJSObjectReference> InvokeAsync(string identifier, params object[] args)
136+
{
137+
throw new NotImplementedException();
138+
}
139+
140+
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args)
141+
{
142+
throw new NotImplementedException();
143+
}
144+
}

src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/test/Microsoft.AspNetCore.Components.QuickGrid.Tests.csproj

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@@ -8,8 +8,10 @@
88
<ItemGroup>
99
<Reference Include="Microsoft.AspNetCore.Components.QuickGrid" />
1010
</ItemGroup>
11-
12-
11+
12+
<ItemGroup>
13+
<Compile Include="$(ComponentsSharedSourceRoot)test\**\*.cs" LinkBase="Helpers" />
14+
</ItemGroup>
1315

1416
<ItemGroup>
1517
<Using Include="Xunit" />

0 commit comments

Comments
 (0)