Skip to content

[Blazor] Add APIs for "enhanced refresh" #50068

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions src/Components/Components/src/NavigationManager.cs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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);
}
}
Original file line number Diff line number Diff line change
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.

Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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();
}
}
Original file line number Diff line number Diff line change
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);
}
}