Skip to content

Commit 9dea44a

Browse files
committed
Fix infinite loop in RadzenChartTooltip.DisposeAsync when a chart tooltip is opened during prerendering. Open chart tooltips only when the mouse is inside the chart.
1 parent 894c738 commit 9dea44a

3 files changed

Lines changed: 135 additions & 11 deletions

File tree

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Bunit;
7+
using Microsoft.AspNetCore.Components;
8+
using Microsoft.AspNetCore.Components.Web;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Microsoft.Extensions.Logging.Abstractions;
11+
using Microsoft.JSInterop;
12+
using Xunit;
13+
using static Radzen.Blazor.Tests.ChartTestHelper;
14+
15+
namespace Radzen.Blazor.Tests
16+
{
17+
public class ChartTooltipPrerenderTests
18+
{
19+
class PrerenderJSRuntime : IJSRuntime
20+
{
21+
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, object[] args) =>
22+
throw new InvalidOperationException("JavaScript interop calls cannot be issued at this time. This is because the component is being statically rendered.");
23+
24+
public ValueTask<TValue> InvokeAsync<TValue>(string identifier, CancellationToken cancellationToken, object[] args) =>
25+
InvokeAsync<TValue>(identifier, args);
26+
}
27+
28+
class TestNavigationManager : NavigationManager
29+
{
30+
public TestNavigationManager()
31+
{
32+
Initialize("http://localhost/", "http://localhost/");
33+
}
34+
35+
protected override void NavigateToCore(string uri, bool forceLoad)
36+
{
37+
}
38+
}
39+
40+
[Fact]
41+
public async Task DisposeAsync_Completes_WhenChartTooltipIsOpenedDuringPrerendering()
42+
{
43+
var services = new ServiceCollection();
44+
services.AddSingleton<IJSRuntime, PrerenderJSRuntime>();
45+
services.AddSingleton<NavigationManager, TestNavigationManager>();
46+
services.AddScoped<TooltipService>();
47+
48+
var provider = services.BuildServiceProvider();
49+
var scope = provider.CreateAsyncScope();
50+
51+
var renderer = new HtmlRenderer(scope.ServiceProvider, NullLoggerFactory.Instance);
52+
var service = scope.ServiceProvider.GetRequiredService<TooltipService>();
53+
54+
await renderer.Dispatcher.InvokeAsync(async () =>
55+
{
56+
await renderer.RenderComponentAsync<RadzenChartTooltip>();
57+
58+
service.OpenChartTooltip(default, 10, 10, _ => builder => builder.AddContent(0, "tooltip"), new ChartTooltipOptions());
59+
});
60+
61+
var dispose = Task.Run(async () =>
62+
{
63+
await renderer.DisposeAsync();
64+
await scope.DisposeAsync();
65+
await provider.DisposeAsync();
66+
});
67+
68+
var completed = await Task.WhenAny(dispose, Task.Delay(TimeSpan.FromSeconds(10)));
69+
70+
Assert.True(completed == dispose, "RadzenChartTooltip.DisposeAsync did not complete after a chart tooltip was opened during prerendering.");
71+
72+
await dispose;
73+
}
74+
75+
[Fact]
76+
public void Sparkline_RenderedWithoutMouseInteraction_DoesNotOpenTooltip()
77+
{
78+
using var ctx = CreateChartContext();
79+
var tooltipService = ctx.Services.GetRequiredService<TooltipService>();
80+
var opens = new List<(double x, double y)>();
81+
tooltipService.OnOpenChartTooltip += (element, x, y, options) => opens.Add((x, y));
82+
83+
var data = new[]
84+
{
85+
new DataItem { Category = "Quiz 1", Value = 92 },
86+
new DataItem { Category = "Quiz 2", Value = 58 },
87+
new DataItem { Category = "Quiz 3", Value = 52 },
88+
new DataItem { Category = "Quiz 4", Value = 61 },
89+
};
90+
91+
ctx.RenderComponent<RadzenSparkline>(p => p
92+
.Add(c => c.Style, "width: 100px; height: 20px")
93+
.AddChildContent<RadzenLineSeries<DataItem>>(s => s
94+
.Add(x => x.Smooth, true)
95+
.Add(x => x.CategoryProperty, nameof(DataItem.Category))
96+
.Add(x => x.ValueProperty, nameof(DataItem.Value))
97+
.Add(x => x.Data, data))
98+
.AddChildContent<RadzenCategoryAxis>(a => a
99+
.Add(x => x.Visible, false)
100+
.Add(x => x.Padding, -20)));
101+
102+
Assert.Empty(opens);
103+
}
104+
}
105+
}

Radzen.Blazor/RadzenChart.razor.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1299,7 +1299,7 @@ private static bool IsRegionSeries(IChartSeries series)
12991299

13001300
internal async Task DisplayTooltip()
13011301
{
1302-
if (Tooltip.Visible)
1302+
if (Tooltip.Visible && MouseInside)
13031303
{
13041304
var queryX = mouseX - MarginLeft;
13051305
var queryY = mouseY - MarginTop;

Radzen.Blazor/RadzenChartTooltip.razor

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,20 @@
3030

3131
var tooltip = tooltips.LastOrDefault();
3232

33-
if (tooltip != null)
33+
if (tooltip != null && IsJSRuntimeAvailable)
3434
{
35-
await JSRuntime.InvokeVoidAsync("Radzen.openChartTooltip",
36-
tooltip.Chart,
37-
x,
38-
y,
39-
UniqueID);
35+
try
36+
{
37+
await JSRuntime.InvokeVoidAsync("Radzen.openChartTooltip",
38+
tooltip.Chart,
39+
x,
40+
y,
41+
UniqueID);
42+
}
43+
catch
44+
{
45+
// ignored
46+
}
4047
}
4148
}
4249

@@ -93,29 +100,41 @@ private DotNetObjectReference<RadzenChartTooltip>? reference;
93100
var lastTooltip = tooltips.LastOrDefault();
94101
if (lastTooltip != null)
95102
{
103+
tooltips.Remove(lastTooltip);
104+
96105
if (IsJSRuntimeAvailable)
97106
{
98107
try
99108
{
100-
tooltips.Remove(lastTooltip);
101109
await JSRuntime.InvokeVoidAsync("Radzen.closeTooltip", UniqueID);
102110
}
103111
catch
104112
{
105113
// ignored
106114
}
107115
}
108-
109116
}
110117

111118
await InvokeAsync(StateHasChanged);
112119
}
113120

114121
public async ValueTask DisposeAsync()
115122
{
116-
while (tooltips.Count != 0)
123+
if (tooltips.Count != 0)
117124
{
118-
await Close();
125+
tooltips.Clear();
126+
127+
if (IsJSRuntimeAvailable)
128+
{
129+
try
130+
{
131+
await JSRuntime.InvokeVoidAsync("Radzen.closeTooltip", UniqueID);
132+
}
133+
catch
134+
{
135+
// ignored
136+
}
137+
}
119138
}
120139
reference?.Dispose();
121140
reference = null;

0 commit comments

Comments
 (0)