Ver código fonte

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

Mackinnon Buck 2 anos atrás
pai
commit
31a26e080a

+ 11 - 0
src/Components/Components/src/NavigationManager.cs

@@ -166,6 +166,17 @@ public abstract class NavigationManager
     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.

+ 1 - 0
src/Components/Components/src/PublicAPI.Unshipped.txt

@@ -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!

+ 22 - 0
src/Components/Server/src/Circuits/RemoteNavigationManager.cs

@@ -117,6 +117,25 @@ internal sealed partial class RemoteNavigationManager : NavigationManager, IHost
         }
     }
 
+    /// <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);
@@ -162,5 +181,8 @@ internal sealed partial class RemoteNavigationManager : NavigationManager, IHost
 
         [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 - 0
src/Components/Shared/src/BrowserNavigationManagerInterop.cs

@@ -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";

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.server.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.web.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
src/Components/Web.JS/dist/Release/blazor.webview.js


+ 2 - 0
src/Components/Web.JS/src/Platform/WebView/WebViewIpcReceiver.ts

@@ -41,6 +41,8 @@ export function startIpcReceiver(): void {
 
     'Navigate': navigationManagerFunctions.navigateTo,
 
+    'Refresh': navigationManagerFunctions.refresh,
+
     'SetHasLocationChangingListeners': navigationManagerFunctions.setHasLocationChangingListeners,
 
     'EndLocationChanging': navigationManagerFunctions.endLocationChanging,

+ 2 - 6
src/Components/Web.JS/src/Services/NavigationEnhancement.ts

@@ -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() {
@@ -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 {
@@ -70,8 +68,6 @@ function performProgrammaticEnhancedNavigation(absoluteInternalHref: string, rep
   performEnhancedPageLoad(absoluteInternalHref);
 }
 
-attachProgrammaticEnhancedNavigationHandler(performProgrammaticEnhancedNavigation);
-
 function onDocumentClick(event: MouseEvent) {
   if (hasInteractiveRouter()) {
     return;

+ 9 - 0
src/Components/Web.JS/src/Services/NavigationManager.ts

@@ -25,6 +25,7 @@ export const internalFunctions = {
   setHasLocationChangingListeners,
   endLocationChanging,
   navigateTo: navigateToFromDotNet,
+  refresh,
   getBaseURI: (): string => document.baseURI,
   getLocationHref: (): string => location.href,
   scrollToElement,
@@ -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;

+ 6 - 0
src/Components/WebAssembly/WebAssembly/src/Services/WebAssemblyNavigationManager.cs

@@ -79,6 +79,12 @@ internal sealed partial class WebAssemblyNavigationManager : NavigationManager
         }
     }
 
+    /// <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);

+ 1 - 0
src/Components/WebView/WebView/src/IpcCommon.cs

@@ -76,5 +76,6 @@ internal sealed class IpcCommon
         SendByteArrayToJS,
         SetHasLocationChangingListeners,
         EndLocationChanging,
+        Refresh,
     }
 }

+ 6 - 1
src/Components/WebView/WebView/src/IpcSender.cs

@@ -8,7 +8,7 @@ using Microsoft.JSInterop;
 
 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
 {
@@ -39,6 +39,11 @@ internal sealed class IpcSender
         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));

+ 6 - 0
src/Components/WebView/WebView/src/Services/WebViewNavigationManager.cs

@@ -84,6 +84,12 @@ internal sealed partial class WebViewNavigationManager : NavigationManager
         }
     }
 
+    /// <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);

+ 79 - 5
src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs

@@ -181,9 +181,11 @@ public class EnhancedNavigationTest : ServerTestBase<BasicTestAppServerSiteFixtu
     }
 
     [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);
@@ -199,7 +201,7 @@ public class EnhancedNavigationTest : ServerTestBase<BasicTestAppServerSiteFixtu
         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))
@@ -235,7 +237,79 @@ public class EnhancedNavigationTest : ServerTestBase<BasicTestAppServerSiteFixtu
         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"));

+ 12 - 0
src/Components/test/E2ETest/Tests/RoutingTest.cs

@@ -1288,6 +1288,18 @@ public class RoutingTest : ServerTestBase<ToggleExecutionModeServerFixture<Progr
         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");

+ 9 - 0
src/Components/test/testassets/BasicTestApp/RouterTest/NavigationManagerComponent.razor

@@ -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 />
@@ -100,4 +104,9 @@
 
         nextProgrammaticNavigationIndex++;
     }
+
+    void ProgrammaticRefresh()
+    {
+        NavigationManager.Refresh();
+    }
 }

+ 18 - 4
src/Components/test/testassets/TestContentPackage/InteractiveNavigationComponent.razor

@@ -2,9 +2,13 @@
 
 <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()
@@ -12,13 +16,23 @@
         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);
+    }
 }

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff