Skip to content

Commit 8be16ae

Browse files
Factor event dispatch out into separate file
1 parent 322a832 commit 8be16ae

File tree

2 files changed

+141
-128
lines changed

2 files changed

+141
-128
lines changed
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
using Microsoft.AspNetCore.Components.Rendering;
5+
using Microsoft.AspNetCore.Components.RenderTree;
6+
using System.Text;
7+
8+
namespace Microsoft.AspNetCore.Components.Endpoints;
9+
10+
internal partial class EndpointHtmlRenderer
11+
{
12+
private const string OnSubmitNameAttribute = "@onsubmit:name";
13+
private readonly Dictionary<(int ComponentId, int FrameIndex), string> _namedEventsByLocation = new();
14+
private readonly Dictionary<string, (int ComponentId, int FrameIndex)> _namedEventsByAssignedName = new(StringComparer.Ordinal);
15+
16+
internal Task DispatchSubmitEventAsync(string? handlerName)
17+
{
18+
if (string.IsNullOrEmpty(handlerName))
19+
{
20+
// Currently this also happens if you forget to add the hidden field, but soon we'll do that automatically, so the
21+
// message is designed around that.
22+
throw new InvalidOperationException($"Cannot dispatch the POST request to the Razor Component endpoint, because the POST data does not specify which form is being submitted. To fix this, ensure form elements have an @onsubmit:name attribute with any unique value, or pass a Name parameter if using EditForm.");
23+
}
24+
25+
if (!_namedEventsByAssignedName.TryGetValue(handlerName, out var frameLocation))
26+
{
27+
// This might only be possible if you deploy an app update and someone tries to submit
28+
// an old version of a form, and your new app no longer has a matching name
29+
throw new InvalidOperationException($"Cannot submit the form '{handlerName}' because no submit handler was found with that name. Ensure forms have a unique @onsubmit:name attribute, or pass the Name parameter if using EditForm.");
30+
}
31+
32+
var eventHandlerId = FindEventHandlerIdForNamedEvent(frameLocation.ComponentId, frameLocation.FrameIndex);
33+
return DispatchEventAsync(eventHandlerId, null, EventArgs.Empty, quiesce: true);
34+
}
35+
36+
private void UpdateNamedEvents(in RenderBatch renderBatch)
37+
{
38+
if (renderBatch.RemovedNamedValues is { } removed)
39+
{
40+
var removedCount = removed.Count;
41+
var removedArray = removed.Array;
42+
for (var i = 0; i < removedCount; i++)
43+
{
44+
ref var removedEntry = ref removedArray[i];
45+
if (string.Equals(removedEntry.Name, OnSubmitNameAttribute, StringComparison.Ordinal))
46+
{
47+
var location = (removedEntry.ComponentId, removedEntry.FrameIndex);
48+
if (_namedEventsByLocation.Remove(location, out var assignedName))
49+
{
50+
_namedEventsByAssignedName.Remove(assignedName);
51+
}
52+
}
53+
}
54+
}
55+
56+
if (renderBatch.AddedNamedValues is { } added)
57+
{
58+
var addedCount = added.Count;
59+
var addedArray = added.Array;
60+
for (var i = 0; i < addedCount; i++)
61+
{
62+
ref var addedEntry = ref addedArray[i];
63+
if (string.Equals(addedEntry.Name, OnSubmitNameAttribute, StringComparison.Ordinal) && addedEntry.Value is string assignedName)
64+
{
65+
var location = (addedEntry.ComponentId, addedEntry.FrameIndex);
66+
if (_namedEventsByAssignedName.TryAdd(assignedName, location))
67+
{
68+
_namedEventsByLocation.Add(location, assignedName);
69+
}
70+
else
71+
{
72+
// We could allow multiple events with the same name, since they are all tracked separately. However
73+
// this is most likely a mistake on the developer's part so we will consider it an error.
74+
var existingEntry = _namedEventsByAssignedName[assignedName];
75+
throw new InvalidOperationException($"There is more than one named event with the name '{assignedName}'. Ensure named events have unique names. The following components both use this name: "
76+
+ $"\n - {GenerateComponentPath(existingEntry.ComponentId)}"
77+
+ $"\n - {GenerateComponentPath(addedEntry.ComponentId)}");
78+
}
79+
}
80+
}
81+
}
82+
}
83+
84+
private ulong FindEventHandlerIdForNamedEvent(int componentId, int frameIndex)
85+
{
86+
var frames = GetCurrentRenderTreeFrames(componentId);
87+
ref var frame = ref frames.Array[frameIndex];
88+
89+
if (frame.FrameType != RenderTreeFrameType.NamedValue)
90+
{
91+
// This should not be possible, as the system doesn't create a way that the location could be wrong. But if it happens, we want to know.
92+
throw new InvalidOperationException($"The named value frame for component '{componentId}' at index '{frameIndex}' unexpectedly matches a frame of type '{frame.FrameType}'.");
93+
}
94+
95+
if (!string.Equals(frame.NamedValueName, OnSubmitNameAttribute, StringComparison.Ordinal))
96+
{
97+
// This should not be possible, as currently we are only tracking name-values with the expected name. But if it happens, we want to know.
98+
throw new InvalidOperationException($"Expected a named value with name '{OnSubmitNameAttribute}' but found the name '{frame.NamedValueName}'.");
99+
}
100+
101+
for (var i = frameIndex - 1; i >= 0; i--)
102+
{
103+
ref var candidate = ref frames.Array[i];
104+
if (candidate.FrameType == RenderTreeFrameType.Attribute)
105+
{
106+
if (candidate.AttributeEventHandlerId > 0 && string.Equals(candidate.AttributeName, "onsubmit", StringComparison.OrdinalIgnoreCase))
107+
{
108+
return candidate.AttributeEventHandlerId;
109+
}
110+
}
111+
else if (candidate.FrameType == RenderTreeFrameType.Element)
112+
{
113+
break;
114+
}
115+
}
116+
117+
// This won't be possible if the Razor compiler requires @onsubmit:name to be used only when there's an @onsubmit.
118+
throw new InvalidOperationException($"The {frame.NamedValueName} value in component {componentId} at index {frameIndex} does not match a preceding event handler.");
119+
}
120+
121+
private string GenerateComponentPath(int componentId)
122+
{
123+
// We are generating a path from the root component with component type names like:
124+
// App > Router > RouteView > LayoutView > Index > PartA
125+
// App > Router > RouteView > LayoutView > MainLayout > NavigationMenu
126+
// To help developers identify when they have multiple forms with the same handler.
127+
Stack<string> stack = new();
128+
129+
for (var current = GetComponentState(componentId); current != null; current = current.ParentComponentState)
130+
{
131+
stack.Push(GetName(current));
132+
}
133+
134+
var builder = new StringBuilder();
135+
builder.AppendJoin(" > ", stack);
136+
return builder.ToString();
137+
138+
static string GetName(ComponentState current) => current.Component.GetType().Name;
139+
}
140+
}

src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.cs

Lines changed: 1 addition & 128 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,8 @@ namespace Microsoft.AspNetCore.Components.Endpoints;
3636
/// </summary>
3737
internal partial class EndpointHtmlRenderer : StaticHtmlRenderer, IComponentPrerenderer
3838
{
39-
private const string OnSubmitNameAttribute = "@onsubmit:name";
4039
private readonly IServiceProvider _services;
41-
private readonly Dictionary<(int ComponentId, int FrameIndex), string> _namedEventsByLocation = new();
42-
private readonly Dictionary<string, (int ComponentId, int FrameIndex)> _namedEventsByAssignedName = new(StringComparer.Ordinal);
43-
4440
private Task? _servicesInitializedTask;
45-
4641
private HttpContext _httpContext = default!; // Always set at the start of an inbound call
4742

4843
// The underlying Renderer always tracks the pending tasks representing *full* quiescence, i.e.,
@@ -108,83 +103,6 @@ internal static async Task InitializeStandardComponentServicesAsync(
108103
}
109104
}
110105

111-
internal Task DispatchSubmitEventAsync(string? handlerName)
112-
{
113-
if (string.IsNullOrEmpty(handlerName))
114-
{
115-
// Currently this also happens if you forget to add the hidden field, but soon we'll do that automatically, so the
116-
// message is designed around that.
117-
throw new InvalidOperationException($"Cannot dispatch the POST request to the Razor Component endpoint, because the POST data does not specify which form is being submitted. To fix this, ensure form elements have an @onsubmit:name attribute with any unique value, or pass a Name parameter if using EditForm.");
118-
}
119-
120-
if (!_namedEventsByAssignedName.TryGetValue(handlerName, out var frameLocation))
121-
{
122-
// This might only be possible if you deploy an app update and someone tries to submit
123-
// an old version of a form, and your new app no longer has a matching name
124-
throw new InvalidOperationException($"Cannot submit the form '{handlerName}' because no submit handler was found with that name. Ensure forms have a unique @onsubmit:name attribute, or pass the Name parameter if using EditForm.");
125-
}
126-
127-
var eventHandlerId = FindEventHandlerIdForNamedEvent(frameLocation.ComponentId, frameLocation.FrameIndex);
128-
return DispatchEventAsync(eventHandlerId, null, EventArgs.Empty, quiesce: true);
129-
}
130-
131-
private ulong FindEventHandlerIdForNamedEvent(int componentId, int frameIndex)
132-
{
133-
var frames = GetCurrentRenderTreeFrames(componentId);
134-
ref var frame = ref frames.Array[frameIndex];
135-
136-
if (frame.FrameType != RenderTreeFrameType.NamedValue)
137-
{
138-
// This should not be possible, as the system doesn't create a way that the location could be wrong. But if it happens, we want to know.
139-
throw new InvalidOperationException($"The named value frame for component '{componentId}' at index '{frameIndex}' unexpectedly matches a frame of type '{frame.FrameType}'.");
140-
}
141-
142-
if (!string.Equals(frame.NamedValueName, OnSubmitNameAttribute, StringComparison.Ordinal))
143-
{
144-
// This should not be possible, as currently we are only tracking name-values with the expected name. But if it happens, we want to know.
145-
throw new InvalidOperationException($"Expected a named value with name '{OnSubmitNameAttribute}' but found the name '{frame.NamedValueName}'.");
146-
}
147-
148-
for (var i = frameIndex - 1; i >= 0; i--)
149-
{
150-
ref var candidate = ref frames.Array[i];
151-
if (candidate.FrameType == RenderTreeFrameType.Attribute)
152-
{
153-
if (candidate.AttributeEventHandlerId > 0 && string.Equals(candidate.AttributeName, "onsubmit", StringComparison.OrdinalIgnoreCase))
154-
{
155-
return candidate.AttributeEventHandlerId;
156-
}
157-
}
158-
else if (candidate.FrameType == RenderTreeFrameType.Element)
159-
{
160-
break;
161-
}
162-
}
163-
164-
// This won't be possible if the Razor compiler requires @onsubmit:name to be used only when there's an @onsubmit.
165-
throw new InvalidOperationException($"The {frame.NamedValueName} value in component {componentId} at index {frameIndex} does not match a preceding event handler.");
166-
}
167-
168-
private static string GenerateComponentPath(ComponentState state)
169-
{
170-
// We are generating a path from the root component with component type names like:
171-
// App > Router > RouteView > LayoutView > Index > PartA
172-
// App > Router > RouteView > LayoutView > MainLayout > NavigationMenu
173-
// To help developers identify when they have multiple forms with the same handler.
174-
Stack<string> stack = new();
175-
176-
for (var current = state; current != null; current = current.ParentComponentState)
177-
{
178-
stack.Push(GetName(current));
179-
}
180-
181-
var builder = new StringBuilder();
182-
builder.AppendJoin(" > ", stack);
183-
return builder.ToString();
184-
185-
static string GetName(ComponentState current) => current.Component.GetType().Name;
186-
}
187-
188106
protected override ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState)
189107
=> new EndpointComponentState(this, componentId, component, parentComponentState);
190108

@@ -208,7 +126,7 @@ protected override void AddPendingTask(ComponentState? componentState, Task task
208126

209127
protected override Task UpdateDisplayAsync(in RenderBatch renderBatch)
210128
{
211-
UpdateNamedEventCapture(in renderBatch);
129+
UpdateNamedEvents(in renderBatch);
212130

213131
if (_streamingUpdatesWriter is { } writer)
214132
{
@@ -229,51 +147,6 @@ static async Task FlushThenComplete(TextWriter writerToFlush, Task completion)
229147
}
230148
}
231149

232-
private void UpdateNamedEventCapture(in RenderBatch renderBatch)
233-
{
234-
if (renderBatch.RemovedNamedValues is {} removed)
235-
{
236-
var removedCount = removed.Count;
237-
var removedArray = removed.Array;
238-
for (var i = 0; i < removedCount; i++)
239-
{
240-
ref var removedEntry = ref removedArray[i];
241-
if (string.Equals(removedEntry.Name, OnSubmitNameAttribute, StringComparison.Ordinal))
242-
{
243-
var location = (removedEntry.ComponentId, removedEntry.FrameIndex);
244-
if (_namedEventsByLocation.Remove(location, out var assignedName))
245-
{
246-
_namedEventsByAssignedName.Remove(assignedName);
247-
}
248-
}
249-
}
250-
}
251-
252-
if (renderBatch.AddedNamedValues is { } added)
253-
{
254-
var addedCount = added.Count;
255-
var addedArray = added.Array;
256-
for (var i = 0; i < addedCount; i++)
257-
{
258-
ref var addedEntry = ref addedArray[i];
259-
if (string.Equals(addedEntry.Name, OnSubmitNameAttribute, StringComparison.Ordinal) && addedEntry.Value is string assignedName)
260-
{
261-
var location = (addedEntry.ComponentId, addedEntry.FrameIndex);
262-
if (_namedEventsByAssignedName.TryAdd(assignedName, location))
263-
{
264-
_namedEventsByLocation.Add(location, assignedName);
265-
}
266-
else
267-
{
268-
// We could allow multiple events with the same name, since they are all tracked separately. However
269-
// this is most likely a mistake on the developer's part so we will consider it an error.
270-
throw new InvalidOperationException($"There is more than one named event with the name '{assignedName}'. Ensure named events have unique names.");
271-
}
272-
}
273-
}
274-
}
275-
}
276-
277150
private static string GetFullUri(HttpRequest request)
278151
{
279152
return UriHelper.BuildAbsolute(

0 commit comments

Comments
 (0)