Skip to content

Commit

Permalink
[Blazor] Add APIs for "enhanced refresh" (#50068)
Browse files Browse the repository at this point in the history
  • Loading branch information
MackinnonBuck committed Aug 17, 2023
1 parent fbfc66b commit 31a26e0
Show file tree
Hide file tree
Showing 18 changed files with 189 additions and 19 deletions.
11 changes: 11 additions & 0 deletions src/Components/Components/src/NavigationManager.cs
Expand Up @@ -166,6 +166,17 @@ protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)]
protected virtual void NavigateToCore([StringSyntax(StringSyntaxAttribute.Uri)] string uri, NavigationOptions options) =>
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).");

/// <summary>
/// Refreshes the current page via request to the server.
/// </summary>
/// <remarks>
/// If <paramref name="forceReload"/> is <c>true</c>, a full page reload will always be performed.
/// Otherwise, the response HTML may be merged with the document's existing HTML to preserve client-side state,
/// falling back on a full page reload if necessary.
/// </remarks>
public virtual void Refresh(bool forceReload = false)
=> NavigateTo(Uri, forceLoad: true, replace: true);

/// <summary>
/// Called to initialize BaseURI and current URI before these values are used for the first time.
/// Override <see cref="EnsureInitialized" /> and call this method to dynamically calculate these values.
Expand Down
1 change: 1 addition & 0 deletions src/Components/Components/src/PublicAPI.Unshipped.txt
Expand Up @@ -101,6 +101,7 @@ static Microsoft.AspNetCore.Components.SupplyParameterFromQueryProviderServiceCo
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!
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!
static Microsoft.Extensions.DependencyInjection.CascadingValueServiceCollectionExtensions.AddCascadingValue<TValue>(this Microsoft.Extensions.DependencyInjection.IServiceCollection! serviceCollection, System.Func<System.IServiceProvider!, TValue>! valueFactory) -> Microsoft.Extensions.DependencyInjection.IServiceCollection!
virtual Microsoft.AspNetCore.Components.NavigationManager.Refresh(bool forceReload = false) -> void
virtual Microsoft.AspNetCore.Components.Rendering.ComponentState.DisposeAsync() -> System.Threading.Tasks.ValueTask
virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void
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!
Expand Down
22 changes: 22 additions & 0 deletions src/Components/Server/src/Circuits/RemoteNavigationManager.cs
Expand Up @@ -117,6 +117,25 @@ async Task PerformNavigationAsync()
}
}

/// <inheritdoc />
public override void Refresh(bool forceReload = false)
{
_ = RefreshAsync();

async Task RefreshAsync()
{
try
{
await _jsRuntime.InvokeVoidAsync(Interop.Refresh, forceReload);
}
catch (Exception ex)
{
Log.RefreshFailed(_logger, ex);
UnhandledException?.Invoke(this, ex);
}
}
}

protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
{
Log.NavigationFailed(_logger, context.TargetLocation, ex);
Expand Down Expand Up @@ -162,5 +181,8 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp

[LoggerMessage(4, LogLevel.Error, "Navigation failed when changing the location to {Uri}", EventName = "NavigationFailed")]
public static partial void NavigationFailed(ILogger logger, string uri, Exception exception);

[LoggerMessage(5, LogLevel.Error, "Failed to refresh", EventName = "RefreshFailed")]
public static partial void RefreshFailed(ILogger logger, Exception exception);
}
}
2 changes: 2 additions & 0 deletions src/Components/Shared/src/BrowserNavigationManagerInterop.cs
Expand Up @@ -16,6 +16,8 @@ internal static class BrowserNavigationManagerInterop

public const string NavigateTo = Prefix + "navigateTo";

public const string Refresh = Prefix + "refresh";

public const string SetHasLocationChangingListeners = Prefix + "setHasLocationChangingListeners";

public const string ScrollToElement = Prefix + "scrollToElement";
Expand Down
2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.server.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.web.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Components/Web.JS/dist/Release/blazor.webview.js

Large diffs are not rendered by default.

Expand Up @@ -41,6 +41,8 @@ export function startIpcReceiver(): void {

'Navigate': navigationManagerFunctions.navigateTo,

'Refresh': navigationManagerFunctions.refresh,

'SetHasLocationChangingListeners': navigationManagerFunctions.setHasLocationChangingListeners,

'EndLocationChanging': navigationManagerFunctions.endLocationChanging,
Expand Down
8 changes: 2 additions & 6 deletions src/Components/Web.JS/src/Services/NavigationEnhancement.ts
Expand Up @@ -48,6 +48,8 @@ export function attachProgressivelyEnhancedNavigationListener(callbacks: Navigat
document.addEventListener('click', onDocumentClick);
document.addEventListener('submit', onDocumentSubmit);
window.addEventListener('popstate', onPopState);

attachProgrammaticEnhancedNavigationHandler(performProgrammaticEnhancedNavigation);
}

export function detachProgressivelyEnhancedNavigationListener() {
Expand All @@ -57,10 +59,6 @@ export function detachProgressivelyEnhancedNavigationListener() {
}

function performProgrammaticEnhancedNavigation(absoluteInternalHref: string, replace: boolean) {
if (hasInteractiveRouter()) {
return;
}

if (replace) {
history.replaceState(null, /* ignored title */ '', absoluteInternalHref);
} else {
Expand All @@ -70,8 +68,6 @@ function performProgrammaticEnhancedNavigation(absoluteInternalHref: string, rep
performEnhancedPageLoad(absoluteInternalHref);
}

attachProgrammaticEnhancedNavigationHandler(performProgrammaticEnhancedNavigation);

function onDocumentClick(event: MouseEvent) {
if (hasInteractiveRouter()) {
return;
Expand Down
9 changes: 9 additions & 0 deletions src/Components/Web.JS/src/Services/NavigationManager.ts
Expand Up @@ -25,6 +25,7 @@ export const internalFunctions = {
setHasLocationChangingListeners,
endLocationChanging,
navigateTo: navigateToFromDotNet,
refresh,
getBaseURI: (): string => document.baseURI,
getLocationHref: (): string => location.href,
scrollToElement,
Expand Down Expand Up @@ -93,6 +94,14 @@ function performScrollToElementOnTheSamePage(absoluteHref : string, replace: boo
scrollToElement(identifier);
}

function refresh(forceReload: boolean): void {
if (!forceReload && hasProgrammaticEnhancedNavigationHandler()) {
performProgrammaticEnhancedNavigation(location.href, /* replace */ true);
} else {
location.reload();
}
}

// For back-compat, we need to accept multiple overloads
export function navigateTo(uri: string, options: NavigationOptions): void;
export function navigateTo(uri: string, forceLoad: boolean): void;
Expand Down
Expand Up @@ -79,6 +79,12 @@ async Task PerformNavigationAsync()
}
}

/// <inheritdoc />
public override void Refresh(bool forceReload = false)
{
DefaultWebAssemblyJSRuntime.Instance.InvokeVoid(Interop.Refresh, forceReload);
}

protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
{
Log.NavigationFailed(_logger, context.TargetLocation, ex);
Expand Down
1 change: 1 addition & 0 deletions src/Components/WebView/WebView/src/IpcCommon.cs
Expand Up @@ -76,5 +76,6 @@ public enum OutgoingMessageType
SendByteArrayToJS,
SetHasLocationChangingListeners,
EndLocationChanging,
Refresh,
}
}
7 changes: 6 additions & 1 deletion src/Components/WebView/WebView/src/IpcSender.cs
Expand Up @@ -8,7 +8,7 @@

namespace Microsoft.AspNetCore.Components.WebView;

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

public void Refresh(bool forceReload)
{
DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.Refresh, forceReload));
}

public void AttachToDocument(int componentId, string selector)
{
DispatchMessageWithErrorHandling(IpcCommon.Serialize(IpcCommon.OutgoingMessageType.AttachToDocument, componentId, selector));
Expand Down
Expand Up @@ -84,6 +84,12 @@ async Task PerformNavigationAsync()
}
}

/// <inheritdoc />
public override void Refresh(bool forceReload = false)
{
_ipcSender.Refresh(forceReload);
}

protected override void HandleLocationChangingHandlerException(Exception ex, LocationChangingContext context)
{
Log.NavigationFailed(_logger, context.TargetLocation, ex);
Expand Down
Expand Up @@ -181,9 +181,11 @@ public void CanPerformProgrammaticEnhancedNavigation(string renderMode)
}

[Theory]
[InlineData("server")]
[InlineData("webassembly")]
public void CanPerformProgrammaticEnhancedRefresh(string renderMode)
[InlineData("server", "refresh-with-navigate-to")]
[InlineData("webassembly", "refresh-with-navigate-to")]
[InlineData("server", "refresh-with-refresh")]
[InlineData("webassembly", "refresh-with-refresh")]
public void CanPerformProgrammaticEnhancedRefresh(string renderMode, string refreshButtonId)
{
Navigate($"{ServerPathBase}/nav");
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
Expand All @@ -199,7 +201,7 @@ public void CanPerformProgrammaticEnhancedRefresh(string renderMode)
Browser.True(() => int.TryParse(renderIdElement.Text, out initialRenderId));
Assert.NotEqual(-1, initialRenderId);

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

Browser.Exists(By.Id("perform-page-reload")).Click();
Browser.Exists(By.Id("reload-with-navigate-to")).Click();
Browser.True(() => IsElementStale(initialRenderIdElement));

var finalRenderIdElement = Browser.Exists(By.Id("render-id"));
var finalRenderId = -1;
Browser.True(() => int.TryParse(finalRenderIdElement.Text, out finalRenderId));
Assert.NotEqual(-1, initialRenderId);
Assert.True(finalRenderId > initialRenderId);

// Ensure that the history stack was correctly updated
Browser.Navigate().Back();
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
Assert.EndsWith("/nav", Browser.Url);
}

[Theory]
[InlineData("server")]
[InlineData("webassembly")]
public void RefreshCanFallBackOnFullPageReload(string renderMode)
{
Navigate($"{ServerPathBase}/nav");
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);

Browser.Exists(By.TagName("nav")).FindElement(By.LinkText($"Interactive component navigation ({renderMode})")).Click();
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);

((IJavaScriptExecutor)Browser).ExecuteScript("sessionStorage.setItem('suppress-enhanced-navigation', 'true')");
Browser.Navigate().Refresh();
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);

// Normally, you shouldn't store references to elements because they could become stale references
// after the page re-renders. However, we want to explicitly test that the element becomes stale
// across renders to ensure that a full page reload occurs.
var initialRenderIdElement = Browser.Exists(By.Id("render-id"));
var initialRenderId = -1;
Browser.True(() => int.TryParse(initialRenderIdElement.Text, out initialRenderId));
Assert.NotEqual(-1, initialRenderId);

Browser.Exists(By.Id("refresh-with-refresh")).Click();
Browser.True(() => IsElementStale(initialRenderIdElement));

var finalRenderIdElement = Browser.Exists(By.Id("render-id"));
var finalRenderId = -1;
Browser.True(() => int.TryParse(finalRenderIdElement.Text, out finalRenderId));
Assert.NotEqual(-1, initialRenderId);
Assert.True(finalRenderId > initialRenderId);

// Ensure that the history stack was correctly updated
Browser.Navigate().Back();
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
Assert.EndsWith("/nav", Browser.Url);
}

[Theory]
[InlineData("server")]
[InlineData("webassembly")]
public void RefreshWithForceReloadDoesFullPageReload(string renderMode)
{
Navigate($"{ServerPathBase}/nav");
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);

Browser.Exists(By.TagName("nav")).FindElement(By.LinkText($"Interactive component navigation ({renderMode})")).Click();
Browser.Equal("Page with interactive components that navigate", () => Browser.Exists(By.TagName("h1")).Text);

// Normally, you shouldn't store references to elements because they could become stale references
// after the page re-renders. However, we want to explicitly test that the element becomes stale
// across renders to ensure that a full page reload occurs.
var initialRenderIdElement = Browser.Exists(By.Id("render-id"));
var initialRenderId = -1;
Browser.True(() => int.TryParse(initialRenderIdElement.Text, out initialRenderId));
Assert.NotEqual(-1, initialRenderId);

Browser.Exists(By.Id("reload-with-refresh")).Click();
Browser.True(() => IsElementStale(initialRenderIdElement));

var finalRenderIdElement = Browser.Exists(By.Id("render-id"));
Expand Down
12 changes: 12 additions & 0 deletions src/Components/test/E2ETest/Tests/RoutingTest.cs
Expand Up @@ -1288,6 +1288,18 @@ public void ResetsScrollPositionWhenPerformingInternalNavigation_ProgrammaticNav
Browser.Equal(0, () => BrowserScrollY);
}

[Fact]
public void Refresh_FullyReloadsTheCurrentPage()
{
SetUrlViaPushState("/");

Browser.MountTestComponent<NavigationManagerComponent>();
Browser.FindElement(By.Id("programmatic-refresh")).Click();

// If the page fully reloads, the NavigationManagerComponent will no longer be mounted
Browser.DoesNotExist(By.Id("programmatic-refresh"));
}

[Fact]
public void PreventDefault_CanBlockNavigation_ForInternalNavigation_PreventDefaultTarget()
=> PreventDefault_CanBlockNavigation("internal", "target");
Expand Down
Expand Up @@ -14,6 +14,10 @@
<button id="programmatic-navigation" @onclick="ProgrammaticNavigation">Programmatic navigation</button><br />
</p>

<p>
<button id="programmatic-refresh" @onclick="ProgrammaticRefresh">Programmatic refresh</button><br />
</p>

<p>
<a id="internal-link-navigation" href="some-path-@nextLinkNavigationIndex">/some-path-@nextLinkNavigationIndex</a>
<button id="increment-link-navigation-index" @onclick="IncrementLinkNavigationIndex">Increment path index</button><br />
Expand Down Expand Up @@ -100,4 +104,9 @@

nextProgrammaticNavigationIndex++;
}

void ProgrammaticRefresh()
{
NavigationManager.Refresh();
}
}
Expand Up @@ -2,23 +2,37 @@

<button type="button" id="navigate-to-another-page" @onclick="NavigateToAnotherPage">Navigate to another page</button>
<br />
<button type="button" id="perform-enhanced-refresh" @onclick="PerformEnhancedRefresh">Perform enhanced refresh</button>
<button type="button" id="refresh-with-navigate-to" @onclick="RefreshWithNavigateTo">Perform enhanced refresh with @(nameof(NavigationManager.NavigateTo))</button>
<br />
<button type="button" id="perform-page-reload" @onclick="PerformPageReload">Perform page reload</button>
<button type="button" id="reload-with-navigate-to" @onclick="ReloadWithNavigateTo">Perform page reload with @(nameof(NavigationManager.NavigateTo))</button>
<br />
<button type="button" id="refresh-with-refresh" @onclick="RefreshWithRefresh">Perform enhanced page refresh with @(nameof(NavigationManager.Refresh))</button>
<br />
<button type="button" id="reload-with-refresh" @onclick="ReloadWithRefresh">Perform page reload with @(nameof(NavigationManager.Refresh))</button>

@code {
private void NavigateToAnotherPage()
{
Navigation.NavigateTo("nav");
}

private void PerformEnhancedRefresh()
private void RefreshWithNavigateTo()
{
Navigation.NavigateTo(Navigation.Uri, replace: true);
}

private void PerformPageReload()
private void ReloadWithNavigateTo()
{
Navigation.NavigateTo(Navigation.Uri, forceLoad: true, replace: true);
}

private void RefreshWithRefresh()
{
Navigation.Refresh();
}

private void ReloadWithRefresh()
{
Navigation.Refresh(forceReload: true);
}
}

0 comments on commit 31a26e0

Please sign in to comment.