Skip to content

Commit 3418372

Browse files
committed
Merge in 'release/8.0' changes
2 parents a6191d5 + 303c6f9 commit 3418372

File tree

15 files changed

+167
-30
lines changed

15 files changed

+167
-30
lines changed

src/Components/QuickGrid/Microsoft.AspNetCore.Components.QuickGrid/src/Columns/ColumnBase.razor

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
{
2020
@if (ColumnOptions is not null && (Align != Align.Right && Align != Align.End))
2121
{
22-
<button class="col-options-button" type="button" @onclick="@(() => Grid.ShowColumnOptionsAsync(this))"></button>
22+
<button class="col-options-button" type="button" title="Column options" @onclick="@(() => Grid.ShowColumnOptionsAsync(this))"></button>
2323
}
2424

2525
if (Sortable.HasValue ? Sortable.Value : IsSortableByDefault())
@@ -38,7 +38,7 @@
3838

3939
@if (ColumnOptions is not null && (Align == Align.Right || Align == Align.End))
4040
{
41-
<button class="col-options-button" type="button" @onclick="@(() => Grid.ShowColumnOptionsAsync(this))"></button>
41+
<button class="col-options-button" type="button" title="Column options" @onclick="@(() => Grid.ShowColumnOptionsAsync(this))"></button>
4242
}
4343
}
4444
}

src/Components/Server/src/Circuits/RemoteJSRuntime.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ internal partial class RemoteJSRuntime : JSRuntime
3030

3131
public bool IsInitialized => _clientProxy is not null;
3232

33+
internal bool IsPermanentlyDisconnected => _permanentlyDisconnected;
34+
3335
/// <summary>
3436
/// Notifies when a runtime exception occurred.
3537
/// </summary>

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ async Task PerformNavigationAsync()
106106
}
107107

108108
await _jsRuntime.InvokeVoidAsync(Interop.NavigateTo, uri, options);
109+
Log.NavigationCompleted(_logger, uri);
110+
}
111+
catch (TaskCanceledException)
112+
when (_jsRuntime is RemoteJSRuntime remoteRuntime && remoteRuntime.IsPermanentlyDisconnected)
113+
{
114+
Log.NavigationStoppedSessionEnded(_logger, uri);
109115
}
110116
catch (Exception ex)
111117
{
@@ -190,5 +196,11 @@ public static void RequestingNavigation(ILogger logger, string uri, NavigationOp
190196

191197
[LoggerMessage(5, LogLevel.Error, "Failed to refresh", EventName = "RefreshFailed")]
192198
public static partial void RefreshFailed(ILogger logger, Exception exception);
199+
200+
[LoggerMessage(6, LogLevel.Debug, "Navigation completed when changing the location to {Uri}", EventName = "NavigationCompleted")]
201+
public static partial void NavigationCompleted(ILogger logger, string uri);
202+
203+
[LoggerMessage(7, LogLevel.Debug, "Navigation stopped because the session ended when navigating to {Uri}", EventName = "NavigationStoppedSessionEnded")]
204+
public static partial void NavigationStoppedSessionEnded(ILogger logger, string uri);
193205
}
194206
}

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/src/Rendering/StreamingRendering.ts

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,19 +53,24 @@ class BlazorStreamingUpdate extends HTMLElement {
5353
const isFormPost = node.getAttribute('from') === 'form-post';
5454
const isEnhancedNav = node.getAttribute('enhanced') === 'true';
5555
if (isEnhancedNav && isWithinBaseUriSpace(destinationUrl)) {
56+
// At this point the destinationUrl might be an opaque URL so we don't know whether it's internal/external or
57+
// whether it's even going to the same URL we're currently on. So we don't know how to update the history.
58+
// Defer that until the redirection is resolved by performEnhancedPageLoad.
59+
const treatAsRedirectionFromMethod = isFormPost ? 'post' : 'get';
60+
const fetchOptions = undefined;
61+
performEnhancedPageLoad(destinationUrl, /* interceptedLink */ false, fetchOptions, treatAsRedirectionFromMethod);
62+
} else {
5663
if (isFormPost) {
5764
// The URL is not yet updated. Push a whole new entry so that 'back' goes back to the pre-redirection location.
58-
history.pushState(null, '', destinationUrl);
65+
// WARNING: The following check to avoid duplicating history entries won't work if the redirection is to an opaque URL.
66+
// We could change the server-side logic to return URLs in plaintext if they match the current request URL already,
67+
// but it's arguably easier to understand that history non-duplication only works for enhanced nav, which is also the
68+
// case for non-streaming responses.
69+
if (destinationUrl !== location.href) {
70+
location.assign(destinationUrl);
71+
}
5972
} else {
6073
// The URL was already updated on the original link click. Replace so that 'back' goes to the pre-redirection location.
61-
history.replaceState(null, '', destinationUrl);
62-
}
63-
performEnhancedPageLoad(destinationUrl, /* interceptedLink */ false);
64-
} else {
65-
// Same reason for varying as above
66-
if (isFormPost) {
67-
location.assign(destinationUrl);
68-
} else {
6974
location.replace(destinationUrl);
7075
}
7176
}

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -132,10 +132,10 @@ function onDocumentSubmit(event: SubmitEvent) {
132132

133133
event.preventDefault();
134134

135-
const url = new URL(event.submitter?.getAttribute('formaction') || formElem.action, document.baseURI);
136-
const fetchOptions: RequestInit = { method: method};
135+
const url = new URL(event.submitter?.getAttribute('formaction') || formElem.action, document.baseURI);
136+
const fetchOptions: RequestInit = { method: method};
137137
const formData = new FormData(formElem);
138-
138+
139139
const submitterName = event.submitter?.getAttribute('name');
140140
const submitterValue = event.submitter!.getAttribute('value');
141141
if (submitterName && submitterValue) {
@@ -153,8 +153,8 @@ function onDocumentSubmit(event: SubmitEvent) {
153153
} else {
154154
// Setting request body and content-type header depending on enctype
155155
const enctype = event.submitter?.getAttribute('formenctype') || formElem.enctype;
156-
if (enctype === 'multipart/form-data') {
157-
// Content-Type header will be set to 'multipart/form-data'
156+
if (enctype === 'multipart/form-data') {
157+
// Content-Type header will be set to 'multipart/form-data'
158158
fetchOptions.body = formData;
159159
} else {
160160
fetchOptions.body = urlSearchParams;
@@ -170,7 +170,7 @@ function onDocumentSubmit(event: SubmitEvent) {
170170
}
171171
}
172172

173-
export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit) {
173+
export async function performEnhancedPageLoad(internalDestinationHref: string, interceptedLink: boolean, fetchOptions?: RequestInit, treatAsRedirectionFromMethod?: 'get' | 'post') {
174174
performingEnhancedPageLoad = true;
175175

176176
// First, stop any preceding enhanced page load
@@ -232,8 +232,9 @@ export async function performEnhancedPageLoad(internalDestinationHref: string, i
232232
// For 301/302/etc redirections to internal URLs, the browser will already have followed the chain of redirections
233233
// to the end, and given us the final content. We do still need to update the current URL to match the final location,
234234
// then let the rest of enhanced nav logic run to patch the new content into the DOM.
235-
if (response.redirected) {
236-
if (isGetRequest) {
235+
if (response.redirected || treatAsRedirectionFromMethod) {
236+
const treatAsGet = treatAsRedirectionFromMethod ? (treatAsRedirectionFromMethod === 'get') : isGetRequest;
237+
if (treatAsGet) {
237238
// For gets, the intermediate (redirecting) URL is already in the address bar, so we have to use 'replace'
238239
// so that 'back' would go to the page before the redirection
239240
history.replaceState(null, '', response.url);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
namespace Microsoft.AspNetCore.Components;
5+
6+
internal interface IInputRadioValueProvider
7+
{
8+
public object? CurrentValue { get; }
9+
}

src/Components/Web/src/Forms/InputRadioContext.cs

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,21 +8,19 @@ namespace Microsoft.AspNetCore.Components.Forms;
88
/// </summary>
99
internal sealed class InputRadioContext
1010
{
11+
private readonly IInputRadioValueProvider _valueProvider;
12+
1113
public InputRadioContext? ParentContext { get; }
1214
public EventCallback<ChangeEventArgs> ChangeEventCallback { get; }
15+
public object? CurrentValue => _valueProvider.CurrentValue;
1316

1417
// Mutable properties that may change any time an InputRadioGroup is rendered
1518
public string? GroupName { get; set; }
16-
public object? CurrentValue { get; set; }
1719
public string? FieldClass { get; set; }
1820

19-
/// <summary>
20-
/// Instantiates a new <see cref="InputRadioContext" />.
21-
/// </summary>
22-
/// <param name="parentContext">The parent context, if any.</param>
23-
/// <param name="changeEventCallback">The event callback to be invoked when the selected value is changed.</param>
24-
public InputRadioContext(InputRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback)
21+
public InputRadioContext(IInputRadioValueProvider valueProvider, InputRadioContext? parentContext, EventCallback<ChangeEventArgs> changeEventCallback)
2522
{
23+
_valueProvider = valueProvider;
2624
ParentContext = parentContext;
2725
ChangeEventCallback = changeEventCallback;
2826
}

src/Components/Web/src/Forms/InputRadioGroup.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Microsoft.AspNetCore.Components.Forms;
1010
/// <summary>
1111
/// Groups child <see cref="InputRadio{TValue}"/> components.
1212
/// </summary>
13-
public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : InputBase<TValue>
13+
public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] TValue> : InputBase<TValue>, IInputRadioValueProvider
1414
{
1515
private readonly string _defaultGroupName = Guid.NewGuid().ToString("N");
1616
private InputRadioContext? _context;
@@ -27,14 +27,16 @@ public class InputRadioGroup<[DynamicallyAccessedMembers(DynamicallyAccessedMemb
2727

2828
[CascadingParameter] private InputRadioContext? CascadedContext { get; set; }
2929

30+
object? IInputRadioValueProvider.CurrentValue => CurrentValue;
31+
3032
/// <inheritdoc />
3133
protected override void OnParametersSet()
3234
{
3335
// On the first render, we can instantiate the InputRadioContext
3436
if (_context is null)
3537
{
3638
var changeEventCallback = EventCallback.Factory.CreateBinder<string?>(this, __value => CurrentValueAsString = __value, CurrentValueAsString);
37-
_context = new InputRadioContext(CascadedContext, changeEventCallback);
39+
_context = new InputRadioContext(this, CascadedContext, changeEventCallback);
3840
}
3941
else if (_context.ParentContext != CascadedContext)
4042
{
@@ -59,7 +61,7 @@ protected override void OnParametersSet()
5961
// Otherwise, just use a GUID to disambiguate this group's radio inputs from any others on the page.
6062
_context.GroupName = _defaultGroupName;
6163
}
62-
_context.CurrentValue = CurrentValue;
64+
6365
_context.FieldClass = EditContext?.FieldCssClass(FieldIdentifier);
6466
}
6567

src/Components/WebView/WebView/src/Microsoft.AspNetCore.Components.WebView.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535

3636
<ItemGroup>
3737
<None Include="buildTransitive\Microsoft.AspNetCore.Components.WebView.props" Pack="true" PackagePath="%(Identity)" />
38+
<None Include="buildMultiTargeting\Microsoft.AspNetCore.Components.WebView.props" Pack="true" PackagePath="%(Identity)" />
39+
<None Include="build\Microsoft.AspNetCore.Components.WebView.props" Pack="true" PackagePath="%(Identity)" />
3840
</ItemGroup>
3941

4042
<ItemGroup>

src/Components/test/E2ETest/ServerRenderingTests/FormHandlingTests/FormWithParentBindingContextTest.cs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,6 +1466,8 @@ public void SubmitButtonFormenctypeAttributeOverridesEnhancedFormEnctype()
14661466
[Fact]
14671467
public void EnhancedFormThatCallsNavigationManagerRefreshDoesNotPushHistoryEntry()
14681468
{
1469+
GoTo("about:blank");
1470+
14691471
var startUrl = Browser.Url;
14701472
GoTo("forms/form-that-calls-navigation-manager-refresh");
14711473
var guid = Browser.Exists(By.Id("guid")).Text;
@@ -1482,6 +1484,30 @@ public void EnhancedFormThatCallsNavigationManagerRefreshDoesNotPushHistoryEntry
14821484
Browser.Navigate().Back();
14831485
Browser.Equal(startUrl, () => Browser.Url);
14841486
}
1487+
1488+
[Fact]
1489+
public void EnhancedFormThatCallsNavigationManagerRefreshDoesNotPushHistoryEntry_Streaming()
1490+
{
1491+
GoTo("about:blank");
1492+
1493+
var startUrl = Browser.Url;
1494+
GoTo("forms/form-that-calls-navigation-manager-refresh-streaming");
1495+
1496+
// Submit the form
1497+
Browser.FindElement(By.Id("some-text")).SendKeys("test string");
1498+
Browser.Equal("test string", () => Browser.FindElement(By.Id("some-text")).GetAttribute("value"));
1499+
Browser.Exists(By.Id("submit-button")).Click();
1500+
1501+
// Wait for the async/streaming process to complete. We know this happened
1502+
// if the loading indicator says we're done, and the textbox was cleared
1503+
// due to the refresh
1504+
Browser.Equal("False", () => Browser.FindElement(By.Id("loading-indicator")).Text);
1505+
Browser.Equal("", () => Browser.FindElement(By.Id("some-text")).GetAttribute("value"));
1506+
1507+
// Checking that the history entry was not pushed
1508+
Browser.Navigate().Back();
1509+
Browser.Equal(startUrl, () => Browser.Url);
1510+
}
14851511

14861512
// Can't just use GetAttribute or GetDomAttribute because they both auto-resolve it
14871513
// to an absolute URL. We want to be able to assert about the attribute's literal value.

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,30 @@ public void InputRadioGroupWorksWithMutatingSetter()
844844
Browser.Equal("False", () => tuesday.GetDomProperty("checked"));
845845
}
846846

847+
[Theory]
848+
[InlineData(0)]
849+
[InlineData(2)]
850+
public void InputRadioGroupWorksWithParentImplementingIHandleEvent(int n)
851+
{
852+
Browser.Url = new UriBuilder(Browser.Url) { Query = ($"?n={n}") }.ToString();
853+
var appElement = Browser.MountTestComponent<InputRadioParentImplementsIHandleEvent>();
854+
var zero = appElement.FindElement(By.Id("inputradiogroup-parent-ihandle-event-0"));
855+
var one = appElement.FindElement(By.Id("inputradiogroup-parent-ihandle-event-1"));
856+
857+
Browser.Equal(n == 0 ? "True" : "False", () => zero.GetDomProperty("checked"));
858+
Browser.Equal("False", () => one.GetDomProperty("checked"));
859+
860+
// Observe the changes after a click
861+
one.Click();
862+
Browser.Equal("False", () => zero.GetDomProperty("checked"));
863+
Browser.Equal("True", () => one.GetDomProperty("checked"));
864+
865+
// Ensure other options can be selected
866+
zero.Click();
867+
Browser.Equal("False", () => one.GetDomProperty("checked"));
868+
Browser.Equal("True", () => zero.GetDomProperty("checked"));
869+
}
870+
847871
[Fact]
848872
public void InputSelectWorksWithMutatingSetter()
849873
{
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
@using Microsoft.AspNetCore.Components.Forms
2+
@implements IHandleEvent
3+
4+
<InputRadioGroup @bind-Value="N">
5+
<InputRadio id="inputradiogroup-parent-ihandle-event-0" Value="0" />
6+
<InputRadio id="inputradiogroup-parent-ihandle-event-1" Value="1" />
7+
</InputRadioGroup>
8+
9+
@code {
10+
11+
[SupplyParameterFromQuery(Name = "n")] int? N { get; set; }
12+
13+
Task IHandleEvent.HandleEventAsync(EventCallbackWorkItem callback, object arg) => callback.InvokeAsync(arg);
14+
}

src/Components/test/testassets/BasicTestApp/Index.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
<option value="BasicTestApp.FormsTest.InputRangeComponent">Input range</option>
5050
<option value="BasicTestApp.FormsTest.InputsWithoutEditForm">Inputs without EditForm</option>
5151
<option value="BasicTestApp.FormsTest.InputsWithMutatingSetters">Inputs with mutating setters</option>
52+
<option value="BasicTestApp.FormsTest.InputRadioParentImplementsIHandleEvent">Input Radio Parent Implements IHandleEvent</option>
5253
<option value="BasicTestApp.NavigateOnSubmit">Navigate to submit</option>
5354
<option value="BasicTestApp.GlobalizationBindCases">Globalization Bind Cases</option>
5455
<option value="BasicTestApp.GracefulTermination">Graceful Termination</option>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
@page "/forms/form-that-calls-navigation-manager-refresh-streaming"
2+
@using Microsoft.AspNetCore.Components.Forms
3+
@attribute [StreamRendering]
4+
@inject NavigationManager Nav
5+
6+
<h3>Form That Calls NavigationManager.Refresh() with streaming</h3>
7+
8+
<form data-enhance @onsubmit="@RefreshAfterDelayAsync" @formname="form-refresh" method="post">
9+
<AntiforgeryToken />
10+
<input id="some-text" name="SomeText" value="@SomeText" />
11+
<button id="submit-button">Submit</button>
12+
</form>
13+
14+
<p>Loading: <span id="loading-indicator">@loading</span></p>
15+
16+
@if (missingText)
17+
{
18+
<p>Enter some text, so you can see it go back to blank after the refresh happened.</p>
19+
}
20+
21+
@code {
22+
[SupplyParameterFromForm]
23+
public string SomeText { get; set; }
24+
25+
bool loading;
26+
bool missingText;
27+
28+
async Task RefreshAfterDelayAsync()
29+
{
30+
if (string.IsNullOrEmpty(SomeText))
31+
{
32+
missingText = true;
33+
}
34+
else
35+
{
36+
loading = true;
37+
await Task.Delay(1000);
38+
Nav.Refresh();
39+
}
40+
}
41+
}

0 commit comments

Comments
 (0)