Skip to content

Commit 35127bf

Browse files
[release/8.0-rc1] [Blazor] Add APIs for "enhanced refresh" (#50124)
* Add NavigationManager.Refresh() + tests * PR feedback * Add `forceReload` parameter --------- Co-authored-by: Mackinnon Buck <[email protected]>
1 parent b86599e commit 35127bf

File tree

18 files changed

+189
-19
lines changed

18 files changed

+189
-19
lines changed

src/Components/Components/src/NavigationManager.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,17 @@ protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)]
166166
protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)] string uri, NavigationOptions options) =>
167167
throw new NotImplementedException($"The type {GetType().FullName} does not support supplying {nameof(NavigationOptions)}. To add support, that type should override {nameof(NavigateToCore)}(string uri, {nameof(NavigationOptions)} options).");
168168

169+
/// <summary>
170+
/// Refreshes the current page via request to the server.
171+
/// </summary>
172+
/// <remarks>
173+
/// If <paramref name="forceReload"/> is <c>true</c>, a full page reload will always be performed.
174+
/// Otherwise, the response HTML may be merged with the document's existing HTML to preserve client-side state,
175+
/// falling back on a full page reload if necessary.
176+
/// </remarks>
177+
public virtual void Refresh(bool forceReload = false)
178+
=> NavigateTo(Uri, forceLoad: true, replace: true);
179+
169180
/// <summary>
170181
/// Called to initialize BaseURI and current URI before these values are used for the first time.
171182
/// Override <see cref="EnsureInitialized" /> and call this method to dynamically calculate these values.

src/Components/Components/src/PublicAPI.Unshipped.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ static Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCo
101101
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, string! name, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
102102
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, Microsoft.AspNetCore.Components.CascadingValueSource<TValue>!>! sourceFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
103103
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
104+
virtual Microsoft.AspNetCore.Components.NavigationManager.Refresh(bool forceReload = false) -> void
104105
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask
105106
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
106107
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState!

src/Components/Server/src/Circuits/RemoteNavigationManager.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,25 @@ async Task PerformNavigationAsync()
117117
}
118118
}
119119

120+
/// <inheritdoc />
121+
public override void Refresh(bool forceReload = false)
122+
{
123+
_ = RefreshAsync();
124+
125+
async Task RefreshAsync()
126+
{
127+
try
128+
{
129+
await _jsRuntime.InvokeVoidAsync(Interop.Refresh, forceReload);
130+
}
131+
catch (Exception ex)
132+
{
133+
Log.RefreshFailed(_logger, ex);
134+
UnhandledException?.Invoke(this, ex);
135+
}
136+
}
137+
}
138+
120139
protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
121140
{
122141
Log.NavigationFailed(_logger, context.TargetLocation, ex);
@@ -162,5 +181,8 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp
162181

163182
[LoggerMessage(4, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")]
164183
public static partial void NavigationFailed(ILogger logger, string uri, Exception exception);
184+
185+
[LoggerMessage(5, LogLevel.Error, "Failed to refresh", EventName = "RefreshFailed")]
186+
public static partial void RefreshFailed(ILogger logger, Exception exception);
165187
}
166188
}

src/Components/Shared/src/BrowserNavigationManagerInterop.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ internal static class BrowserNavigationManagerInterop
1616

1717
public const string NavigateTo = Prefix + "navigateTo";
1818

19+
public const string Refresh = Prefix + "refresh";
20+
1921
public const string SetHasLocationChangingListeners = Prefix + "setHasLocationChangingListeners";
2022

2123
public const string ScrollToElement = Prefix + "scrollToElement";

src/Components/Web.JS/dist/Release/blazor.server.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.web.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/dist/Release/blazor.webview.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Web.JS/src/Platform/WebView/WebViewIpcReceiver.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ export function startIpcReceiver(): void {
4141

4242
'Navigate': navigationManagerFunctions.navigateTo,
4343

44+
'Refresh': navigationManagerFunctions.refresh,
45+
4446
'SetHasLocationChangingListeners': navigationManagerFunctions.setHasLocationChangingListeners,
4547

4648
'EndLocationChanging': navigationManagerFunctions.endLocationChanging,

src/Components/Web.JS/src/Services/NavigationEnhancement.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ export function attachProgressivelyEnhancedNavigationListener(callbacks: Navigat
4848
document.addEventListener('click', onDocumentClick);
4949
document.addEventListener('submit', onDocumentSubmit);
5050
window.addEventListener('popstate', onPopState);
51+
52+
attachProgrammaticEnhancedNavigationHandler(performProgrammaticEnhancedNavigation);
5153
}
5254

5355
export function detachProgressivelyEnhancedNavigationListener() {
@@ -57,10 +59,6 @@ export function detachProgressivelyEnhancedNavigationListener() {
5759
}
5860

5961
function performProgrammaticEnhancedNavigation(absoluteInternalHref: string, replace: boolean) {
60-
if (hasInteractiveRouter()) {
61-
return;
62-
}
63-
6462
if (replace) {
6563
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
6664
} else {
@@ -70,8 +68,6 @@ function performProgrammaticEnhancedNavigation(absoluteInternalHref: string, rep
7068
performEnhancedPageLoad(absoluteInternalHref);
7169
}
7270

73-
attachProgrammaticEnhancedNavigationHandler(performProgrammaticEnhancedNavigation);
74-
7571
function onDocumentClick(event: MouseEvent) {
7672
if (hasInteractiveRouter()) {
7773
return;

src/Components/Web.JS/src/Services/NavigationManager.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const internalFunctions = {
2525
setHasLocationChangingListeners,
2626
endLocationChanging,
2727
navigateTo: navigateToFromDotNet,
28+
refresh,
2829
getBaseURI: (): string => document.baseURI,
2930
getLocationHref: (): string => location.href,
3031
scrollToElement,
@@ -93,6 +94,14 @@ function performScrollToElementOnTheSamePage(absoluteHref : string, replace: boo
9394
scrollToElement(identifier);
9495
}
9596

97+
function refresh(forceReload: boolean): void {
98+
if (!forceReload && hasProgrammaticEnhancedNavigationHandler()) {
99+
performProgrammaticEnhancedNavigation(location.href, /* replace */ true);
100+
} else {
101+
location.reload();
102+
}
103+
}
104+
96105
// For back-compat, we need to accept multiple overloads
97106
export function navigateTo(uri: string, options: NavigationOptions): void;
98107
export function navigateTo(uri: string, forceLoad: boolean): void;

src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,12 @@ async Task PerformNavigationAsync()
7979
}
8080
}
8181

82+
/// <inheritdoc />
83+
public override void Refresh(bool forceReload = false)
84+
{
85+
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.Refresh, forceReload);
86+
}
87+
8288
protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
8389
{
8490
Log.NavigationFailed(_logger, context.TargetLocation, ex);

src/Components/WebView/WebView/src/IpcCommon.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,5 +76,6 @@ public enum OutgoingMessageType
7676
SendByteArrayToJS,
7777
SetHasLocationChangingListeners,
7878
EndLocationChanging,
79+
Refresh,
7980
}
8081
}

src/Components/WebView/WebView/src/IpcSender.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
namespace Microsoft.AspNetCore.Components.WebView;
1010

11-
// Handles comunication between the component abstractions (Renderer, NavigationManager, JSInterop, etc.)
11+
// Handles communication between the component abstractions (Renderer, NavigationManager, JSInterop, etc.)
1212
// and the underlying transport channel
1313
internal sealed class IpcSender
1414
{
@@ -39,6 +39,11 @@ public void Navigate(string uri, NavigationOptions options)
3939
DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.Navigate, uri, options));
4040
}
4141

42+
public void Refresh(bool forceReload)
43+
{
44+
DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.Refresh, forceReload));
45+
}
46+
4247
public void AttachToDocument(int componentId, string selector)
4348
{
4449
DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.AttachToDocument, componentId, selector));

src/Components/WebView/WebView/src/Services/WebViewNavigationManager.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,12 @@ async Task PerformNavigationAsync()
8484
}
8585
}
8686

87+
/// <inheritdoc />
88+
public override void Refresh(bool forceReload = false)
89+
{
90+
_ipcSender.Refresh(forceReload);
91+
}
92+
8793
protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
8894
{
8995
Log.NavigationFailed(_logger, context.TargetLocation, ex);

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs

Lines changed: 79 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,11 @@ public void CanPerformProgrammaticEnhancedNavigation(string renderMode)
181181
}
182182

183183
[Theory]
184-
[InlineData("server")]
185-
[InlineData("webassembly")]
186-
public void CanPerformProgrammaticEnhancedRefresh(string renderMode)
184+
[InlineData("server", "refresh-with-navigate-to")]
185+
[InlineData("webassembly", "refresh-with-navigate-to")]
186+
[InlineData("server", "refresh-with-refresh")]
187+
[InlineData("webassembly", "refresh-with-refresh")]
188+
public void CanPerformProgrammaticEnhancedRefresh(string renderMode, string refreshButtonId)
187189
{
188190
Navigate($"{ServerPathBase}/nav");
189191
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
@@ -199,7 +201,7 @@ public void CanPerformProgrammaticEnhancedRefresh(string renderMode)
199201
Browser.True(() => int.TryParse(renderIdElement.Text, out initialRenderId));
200202
Assert.NotEqual(-1, initialRenderId);
201203

202-
Browser.Exists(By.Id("perform-enhanced-refresh")).Click();
204+
Browser.Exists(By.Id(refreshButtonId)).Click();
203205
Browser.True(() =>
204206
{
205207
if (IsElementStale(renderIdElement) || !int.TryParse(renderIdElement.Text, out var newRenderId))
@@ -235,7 +237,79 @@ public void NavigateToCanFallBackOnFullPageReload(string renderMode)
235237
Browser.True(() => int.TryParse(initialRenderIdElement.Text, out initialRenderId));
236238
Assert.NotEqual(-1, initialRenderId);
237239

238-
Browser.Exists(By.Id("perform-page-reload")).Click();
240+
Browser.Exists(By.Id("reload-with-navigate-to")).Click();
241+
Browser.True(() => IsElementStale(initialRenderIdElement));
242+
243+
var finalRenderIdElement = Browser.Exists(By.Id("render-id"));
244+
var finalRenderId = -1;
245+
Browser.True(() => int.TryParse(finalRenderIdElement.Text, out finalRenderId));
246+
Assert.NotEqual(-1, initialRenderId);
247+
Assert.True(finalRenderId > initialRenderId);
248+
249+
// Ensure that the history stack was correctly updated
250+
Browser.Navigate().Back();
251+
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
252+
Assert.EndsWith("/nav", Browser.Url);
253+
}
254+
255+
[Theory]
256+
[InlineData("server")]
257+
[InlineData("webassembly")]
258+
public void RefreshCanFallBackOnFullPageReload(string renderMode)
259+
{
260+
Navigate($"{ServerPathBase}/nav");
261+
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
262+
263+
Browser.Exists(By.TagName("nav")).FindElement(By.LinkText($"Interactive component navigation ({renderMode})")).Click();
264+
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);
265+
266+
((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('suppress-enhanced-navigation', 'true')");
267+
Browser.Navigate().Refresh();
268+
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);
269+
270+
// Normally, you shouldn't store references to elements because they could become stale references
271+
// after the page re-renders. However, we want to explicitly test that the element becomes stale
272+
// across renders to ensure that a full page reload occurs.
273+
var initialRenderIdElement = Browser.Exists(By.Id("render-id"));
274+
var initialRenderId = -1;
275+
Browser.True(() => int.TryParse(initialRenderIdElement.Text, out initialRenderId));
276+
Assert.NotEqual(-1, initialRenderId);
277+
278+
Browser.Exists(By.Id("refresh-with-refresh")).Click();
279+
Browser.True(() => IsElementStale(initialRenderIdElement));
280+
281+
var finalRenderIdElement = Browser.Exists(By.Id("render-id"));
282+
var finalRenderId = -1;
283+
Browser.True(() => int.TryParse(finalRenderIdElement.Text, out finalRenderId));
284+
Assert.NotEqual(-1, initialRenderId);
285+
Assert.True(finalRenderId > initialRenderId);
286+
287+
// Ensure that the history stack was correctly updated
288+
Browser.Navigate().Back();
289+
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
290+
Assert.EndsWith("/nav", Browser.Url);
291+
}
292+
293+
[Theory]
294+
[InlineData("server")]
295+
[InlineData("webassembly")]
296+
public void RefreshWithForceReloadDoesFullPageReload(string renderMode)
297+
{
298+
Navigate($"{ServerPathBase}/nav");
299+
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
300+
301+
Browser.Exists(By.TagName("nav")).FindElement(By.LinkText($"Interactive component navigation ({renderMode})")).Click();
302+
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);
303+
304+
// Normally, you shouldn't store references to elements because they could become stale references
305+
// after the page re-renders. However, we want to explicitly test that the element becomes stale
306+
// across renders to ensure that a full page reload occurs.
307+
var initialRenderIdElement = Browser.Exists(By.Id("render-id"));
308+
var initialRenderId = -1;
309+
Browser.True(() => int.TryParse(initialRenderIdElement.Text, out initialRenderId));
310+
Assert.NotEqual(-1, initialRenderId);
311+
312+
Browser.Exists(By.Id("reload-with-refresh")).Click();
239313
Browser.True(() => IsElementStale(initialRenderIdElement));
240314

241315
var finalRenderIdElement = Browser.Exists(By.Id("render-id"));

src/Components/test/E2ETest/Tests/RoutingTest.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1288,6 +1288,18 @@ public void ResetsScrollPositionWhenPerformingInternalNavigation_ProgrammaticNav
12881288
Browser.Equal(0, () => BrowserScrollY);
12891289
}
12901290

1291+
[Fact]
1292+
public void Refresh_FullyReloadsTheCurrentPage()
1293+
{
1294+
SetUrlViaPushState("/");
1295+
1296+
Browser.MountTestComponent<NavigationManagerComponent>();
1297+
Browser.FindElement(By.Id("programmatic-refresh")).Click();
1298+
1299+
// If the page fully reloads, the NavigationManagerComponent will no longer be mounted
1300+
Browser.DoesNotExist(By.Id("programmatic-refresh"));
1301+
}
1302+
12911303
[Fact]
12921304
public void PreventDefault_CanBlockNavigation_ForInternalNavigation_PreventDefaultTarget()
12931305
=> PreventDefault_CanBlockNavigation("internal", "target");

src/Components/test/testassets/BasicTestApp/RouterTest/NavigationManagerComponent.razor

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
<button id="programmatic-navigation" @onclick="ProgrammaticNavigation">Programmatic navigation</button><br />
1515
</p>
1616

17+
<p>
18+
<button id="programmatic-refresh" @onclick="ProgrammaticRefresh">Programmatic refresh</button><br />
19+
</p>
20+
1721
<p>
1822
<a id="internal-link-navigation" href="some-path-@nextLinkNavigationIndex">/some-path-@nextLinkNavigationIndex</a>
1923
<button id="increment-link-navigation-index" @onclick="IncrementLinkNavigationIndex">Increment path index</button><br />
@@ -100,4 +104,9 @@
100104

101105
nextProgrammaticNavigationIndex++;
102106
}
107+
108+
void ProgrammaticRefresh()
109+
{
110+
NavigationManager.Refresh();
111+
}
103112
}

src/Components/test/testassets/TestContentPackage/InteractiveNavigationComponent.razor

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,37 @@
22

33
<button type="button" id="navigate-to-another-page" @onclick="NavigateToAnotherPage">Navigate to another page</button>
44
<br />
5-
<button type="button" id="perform-enhanced-refresh" @onclick="PerformEnhancedRefresh">Perform enhanced refresh</button>
5+
<button type="button" id="refresh-with-navigate-to" @onclick="RefreshWithNavigateTo">Perform enhanced refresh with @(nameof(NavigationManager.NavigateTo))</button>
66
<br />
7-
<button type="button" id="perform-page-reload" @onclick="PerformPageReload">Perform page reload</button>
7+
<button type="button" id="reload-with-navigate-to" @onclick="ReloadWithNavigateTo">Perform page reload with @(nameof(NavigationManager.NavigateTo))</button>
8+
<br />
9+
<button type="button" id="refresh-with-refresh" @onclick="RefreshWithRefresh">Perform enhanced page refresh with @(nameof(NavigationManager.Refresh))</button>
10+
<br />
11+
<button type="button" id="reload-with-refresh" @onclick="ReloadWithRefresh">Perform page reload with @(nameof(NavigationManager.Refresh))</button>
812

913
@code {
1014
private void NavigateToAnotherPage()
1115
{
1216
Navigation.NavigateTo("nav");
1317
}
1418

15-
private void PerformEnhancedRefresh()
19+
private void RefreshWithNavigateTo()
1620
{
1721
Navigation.NavigateTo(Navigation.Uri, replace: true);
1822
}
1923

20-
private void PerformPageReload()
24+
private void ReloadWithNavigateTo()
2125
{
2226
Navigation.NavigateTo(Navigation.Uri, forceLoad: true, replace: true);
2327
}
28+
29+
private void RefreshWithRefresh()
30+
{
31+
Navigation.Refresh();
32+
}
33+
34+
private void ReloadWithRefresh()
35+
{
36+
Navigation.Refresh(forceReload: true);
37+
}
2438
}

0 commit comments

Comments
 (0)