Skip to content

Commit f781ef9

Browse files
For two-way bindings, enforce consistency between .NET model and DOM by patching old tree. Fixes #8204
1 parent cbf9735 commit f781ef9

25 files changed

+923
-88
lines changed

src/Components/Blazor/Blazor/src/Rendering/WebAssemblyRenderer.cs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ protected override void HandleException(Exception exception)
116116
}
117117

118118
/// <inheritdoc />
119-
public override Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArgs)
119+
public override Task DispatchEventAsync(int eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs eventArgs)
120120
{
121121
// Be sure we only run one event handler at once. Although they couldn't run
122122
// simultaneously anyway (there's only one thread), they could run nested on
@@ -135,7 +135,7 @@ public override Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArg
135135

136136
if (isDispatchingEvent)
137137
{
138-
var info = new IncomingEventInfo(eventHandlerId, eventArgs);
138+
var info = new IncomingEventInfo(eventHandlerId, eventFieldInfo, eventArgs);
139139
deferredIncomingEvents.Enqueue(info);
140140
return info.TaskCompletionSource.Task;
141141
}
@@ -144,7 +144,7 @@ public override Task DispatchEventAsync(int eventHandlerId, UIEventArgs eventArg
144144
try
145145
{
146146
isDispatchingEvent = true;
147-
return base.DispatchEventAsync(eventHandlerId, eventArgs);
147+
return base.DispatchEventAsync(eventHandlerId, eventFieldInfo, eventArgs);
148148
}
149149
finally
150150
{
@@ -168,7 +168,7 @@ private async Task ProcessNextDeferredEventAsync()
168168

169169
try
170170
{
171-
await DispatchEventAsync(info.EventHandlerId, info.EventArgs);
171+
await DispatchEventAsync(info.EventHandlerId, info.EventFieldInfo, info.EventArgs);
172172
taskCompletionSource.SetResult(null);
173173
}
174174
catch (Exception ex)
@@ -180,12 +180,14 @@ private async Task ProcessNextDeferredEventAsync()
180180
readonly struct IncomingEventInfo
181181
{
182182
public readonly int EventHandlerId;
183+
public readonly EventFieldInfo EventFieldInfo;
183184
public readonly UIEventArgs EventArgs;
184185
public readonly TaskCompletionSource<object> TaskCompletionSource;
185186

186-
public IncomingEventInfo(int eventHandlerId, UIEventArgs eventArgs)
187+
public IncomingEventInfo(int eventHandlerId, EventFieldInfo eventFieldInfo, UIEventArgs eventArgs)
187188
{
188189
EventHandlerId = eventHandlerId;
190+
EventFieldInfo = eventFieldInfo;
189191
EventArgs = eventArgs;
190192
TaskCompletionSource = new TaskCompletionSource<object>();
191193
}

src/Components/Browser.JS/dist/Release/blazor.server.js

Lines changed: 10 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/Components/Browser.JS/dist/Release/blazor.webassembly.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/Browser.JS/src/Rendering/BrowserRenderer.ts

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { EventDelegator } from './EventDelegator';
33
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
44
import { LogicalElement, PermutationListEntry, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement, getLogicalChildrenArray, getLogicalSiblingEnd, permuteLogicalChildren, getClosestDomElement } from './LogicalElements';
55
import { applyCaptureIdToElement } from './ElementReferenceCapture';
6+
import { EventFieldInfo } from './EventFieldInfo';
67
const selectValuePropname = '_blazorSelectValue';
78
const sharedTemplateElemForParsing = document.createElement('template');
89
const sharedSvgElemForParsing = document.createElementNS('http://www.w3.org/2000/svg', 'g');
@@ -18,8 +19,8 @@ export class BrowserRenderer {
1819

1920
public constructor(browserRendererId: number) {
2021
this.browserRendererId = browserRendererId;
21-
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs) => {
22-
raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs);
22+
this.eventDelegator = new EventDelegator((event, eventHandlerId, eventArgs, eventFieldInfo) => {
23+
raiseEvent(event, this.browserRendererId, eventHandlerId, eventArgs, eventFieldInfo);
2324
});
2425
}
2526

@@ -50,7 +51,7 @@ export class BrowserRenderer {
5051
const ownerDocument = getClosestDomElement(element).ownerDocument;
5152
const activeElementBefore = ownerDocument && ownerDocument.activeElement;
5253

53-
this.applyEdits(batch, element, 0, edits, referenceFrames);
54+
this.applyEdits(batch, componentId, element, 0, edits, referenceFrames);
5455

5556
// Try to restore focus in case it was lost due to an element move
5657
if ((activeElementBefore instanceof HTMLElement) && ownerDocument && ownerDocument.activeElement !== activeElementBefore) {
@@ -70,7 +71,7 @@ export class BrowserRenderer {
7071
this.childComponentLocations[componentId] = element;
7172
}
7273

73-
private applyEdits(batch: RenderBatch, parent: LogicalElement, childIndex: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
74+
private applyEdits(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, edits: ArraySegment<RenderTreeEdit>, referenceFrames: ArrayValues<RenderTreeFrame>) {
7475
let currentDepth = 0;
7576
let childIndexAtCurrentDepth = childIndex;
7677
let permutationList: PermutationListEntry[] | undefined;
@@ -91,7 +92,7 @@ export class BrowserRenderer {
9192
const frameIndex = editReader.newTreeIndex(edit);
9293
const frame = batch.referenceFramesEntry(referenceFrames, frameIndex);
9394
const siblingIndex = editReader.siblingIndex(edit);
94-
this.insertFrame(batch, parent, childIndexAtCurrentDepth + siblingIndex, referenceFrames, frame, frameIndex);
95+
this.insertFrame(batch, componentId, parent, childIndexAtCurrentDepth + siblingIndex, referenceFrames, frame, frameIndex);
9596
break;
9697
}
9798
case EditType.removeFrame: {
@@ -105,7 +106,7 @@ export class BrowserRenderer {
105106
const siblingIndex = editReader.siblingIndex(edit);
106107
const element = getLogicalChild(parent, childIndexAtCurrentDepth + siblingIndex);
107108
if (element instanceof Element) {
108-
this.applyAttribute(batch, element, frame);
109+
this.applyAttribute(batch, componentId, element, frame);
109110
} else {
110111
throw new Error('Cannot set attribute on non-element child');
111112
}
@@ -182,12 +183,12 @@ export class BrowserRenderer {
182183
}
183184
}
184185

185-
private insertFrame(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number): number {
186+
private insertFrame(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number): number {
186187
const frameReader = batch.frameReader;
187188
const frameType = frameReader.frameType(frame);
188189
switch (frameType) {
189190
case FrameType.element:
190-
this.insertElement(batch, parent, childIndex, frames, frame, frameIndex);
191+
this.insertElement(batch, componentId, parent, childIndex, frames, frame, frameIndex);
191192
return 1;
192193
case FrameType.text:
193194
this.insertText(batch, parent, childIndex, frame);
@@ -198,7 +199,7 @@ export class BrowserRenderer {
198199
this.insertComponent(batch, parent, childIndex, frame);
199200
return 1;
200201
case FrameType.region:
201-
return this.insertFrameRange(batch, parent, childIndex, frames, frameIndex + 1, frameIndex + frameReader.subtreeLength(frame));
202+
return this.insertFrameRange(batch, componentId, parent, childIndex, frames, frameIndex + 1, frameIndex + frameReader.subtreeLength(frame));
202203
case FrameType.elementReferenceCapture:
203204
if (parent instanceof Element) {
204205
applyCaptureIdToElement(parent, frameReader.elementReferenceCaptureId(frame)!);
@@ -215,7 +216,7 @@ export class BrowserRenderer {
215216
}
216217
}
217218

218-
private insertElement(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number) {
219+
private insertElement(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, frame: RenderTreeFrame, frameIndex: number) {
219220
const frameReader = batch.frameReader;
220221
const tagName = frameReader.elementName(frame)!;
221222
const newDomElementRaw = tagName === 'svg' || isSvgElement(parent) ?
@@ -229,11 +230,11 @@ export class BrowserRenderer {
229230
for (let descendantIndex = frameIndex + 1; descendantIndex < descendantsEndIndexExcl; descendantIndex++) {
230231
const descendantFrame = batch.referenceFramesEntry(frames, descendantIndex);
231232
if (frameReader.frameType(descendantFrame) === FrameType.attribute) {
232-
this.applyAttribute(batch, newDomElementRaw, descendantFrame);
233+
this.applyAttribute(batch, componentId, newDomElementRaw, descendantFrame);
233234
} else {
234235
// As soon as we see a non-attribute child, all the subsequent child frames are
235236
// not attributes, so bail out and insert the remnants recursively
236-
this.insertFrameRange(batch, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl);
237+
this.insertFrameRange(batch, componentId, newElement, 0, frames, descendantIndex, descendantsEndIndexExcl);
237238
break;
238239
}
239240
}
@@ -265,7 +266,7 @@ export class BrowserRenderer {
265266
}
266267
}
267268

268-
private applyAttribute(batch: RenderBatch, toDomElement: Element, attributeFrame: RenderTreeFrame) {
269+
private applyAttribute(batch: RenderBatch, componentId: number, toDomElement: Element, attributeFrame: RenderTreeFrame) {
269270
const frameReader = batch.frameReader;
270271
const attributeName = frameReader.attributeName(attributeFrame)!;
271272
const browserRendererId = this.browserRendererId;
@@ -277,7 +278,7 @@ export class BrowserRenderer {
277278
if (firstTwoChars !== 'on' || !eventName) {
278279
throw new Error(`Attribute has nonzero event handler ID, but attribute name '${attributeName}' does not start with 'on'.`);
279280
}
280-
this.eventDelegator.setListener(toDomElement, eventName, eventHandlerId);
281+
this.eventDelegator.setListener(toDomElement, eventName, eventHandlerId, componentId);
281282
return;
282283
}
283284

@@ -352,11 +353,11 @@ export class BrowserRenderer {
352353
}
353354
}
354355

355-
private insertFrameRange(batch: RenderBatch, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, startIndex: number, endIndexExcl: number): number {
356+
private insertFrameRange(batch: RenderBatch, componentId: number, parent: LogicalElement, childIndex: number, frames: ArrayValues<RenderTreeFrame>, startIndex: number, endIndexExcl: number): number {
356357
const origChildIndex = childIndex;
357358
for (let index = startIndex; index < endIndexExcl; index++) {
358359
const frame = batch.referenceFramesEntry(frames, index);
359-
const numChildrenInserted = this.insertFrame(batch, parent, childIndex, frames, frame, index);
360+
const numChildrenInserted = this.insertFrame(batch, componentId, parent, childIndex, frames, frame, index);
360361
childIndex += numChildrenInserted;
361362

362363
// Skip over any descendants, since they are already dealt with recursively
@@ -397,7 +398,7 @@ function countDescendantFrames(batch: RenderBatch, frame: RenderTreeFrame): numb
397398
}
398399
}
399400

400-
function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>) {
401+
function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>, eventFieldInfo: EventFieldInfo | null) {
401402
if (preventDefaultEvents[event.type]) {
402403
event.preventDefault();
403404
}
@@ -406,6 +407,7 @@ function raiseEvent(event: Event, browserRendererId: number, eventHandlerId: num
406407
browserRendererId,
407408
eventHandlerId,
408409
eventArgsType: eventArgs.type,
410+
eventFieldInfo: eventFieldInfo,
409411
};
410412

411413
return DotNet.invokeMethodAsync(

src/Components/Browser.JS/src/Rendering/EventDelegator.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
2+
import { EventFieldInfo } from './EventFieldInfo';
23

34
const nonBubblingEvents = toLookup([
45
'abort',
@@ -21,7 +22,7 @@ const nonBubblingEvents = toLookup([
2122
]);
2223

2324
export interface OnEventCallback {
24-
(event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>): void;
25+
(event: Event, eventHandlerId: number, eventArgs: EventForDotNet<UIEventArgs>, eventFieldInfo: EventFieldInfo | null): void;
2526
}
2627

2728
// Responsible for adding/removing the eventInfo on an expando property on DOM elements, and
@@ -40,7 +41,7 @@ export class EventDelegator {
4041
this.eventInfoStore = new EventInfoStore(this.onGlobalEvent.bind(this));
4142
}
4243

43-
public setListener(element: Element, eventName: string, eventHandlerId: number) {
44+
public setListener(element: Element, eventName: string, eventHandlerId: number, renderingComponentId: number) {
4445
// Ensure we have a place to store event info for this element
4546
let infoForElement: EventHandlerInfosForElement = element[this.eventsCollectionKey];
4647
if (!infoForElement) {
@@ -53,7 +54,7 @@ export class EventDelegator {
5354
this.eventInfoStore.update(oldInfo.eventHandlerId, eventHandlerId);
5455
} else {
5556
// Go through the whole flow which might involve registering a new global handler
56-
const newInfo = { element, eventName, eventHandlerId };
57+
const newInfo = { element, eventName, eventHandlerId, renderingComponentId };
5758
this.eventInfoStore.add(newInfo);
5859
infoForElement[eventName] = newInfo;
5960
}
@@ -89,15 +90,16 @@ export class EventDelegator {
8990
const eventIsNonBubbling = nonBubblingEvents.hasOwnProperty(evt.type);
9091
while (candidateElement) {
9192
if (candidateElement.hasOwnProperty(this.eventsCollectionKey)) {
92-
const handlerInfos = candidateElement[this.eventsCollectionKey];
93+
const handlerInfos: EventHandlerInfosForElement = candidateElement[this.eventsCollectionKey];
9394
if (handlerInfos.hasOwnProperty(evt.type)) {
9495
// We are going to raise an event for this element, so prepare info needed by the .NET code
9596
if (!eventArgs) {
9697
eventArgs = EventForDotNet.fromDOMEvent(evt);
9798
}
9899

99100
const handlerInfo = handlerInfos[evt.type];
100-
this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs);
101+
const eventFieldInfo = EventFieldInfo.fromEvent(handlerInfo.renderingComponentId, evt);
102+
this.onEvent(evt, handlerInfo.eventHandlerId, eventArgs, eventFieldInfo);
101103
}
102104
}
103105

@@ -180,6 +182,11 @@ interface EventHandlerInfo {
180182
element: Element;
181183
eventName: string;
182184
eventHandlerId: number;
185+
186+
// The component whose tree includes the event handler attribute frame, *not* necessarily the
187+
// same component that will be re-rendered after the event is handled (since we re-render the
188+
// component that supplied the delegate, not the one that rendered the event handler frame)
189+
renderingComponentId: number;
183190
}
184191

185192
function toLookup(items: string[]): { [key: string]: boolean } {
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
export class EventFieldInfo {
2+
constructor(public componentId: number, public fieldValue: string | boolean) {
3+
}
4+
5+
public static fromEvent(componentId: number, event: Event): EventFieldInfo | null {
6+
const elem = event.target;
7+
if (elem instanceof Element) {
8+
const fieldData = getFormFieldData(elem);
9+
if (fieldData) {
10+
return new EventFieldInfo(componentId, fieldData.value);
11+
}
12+
}
13+
14+
// This event isn't happening on a form field that we can reverse-map back to some incoming attribute
15+
return null;
16+
}
17+
}
18+
19+
function getFormFieldData(elem: Element) {
20+
// The logic in here should be the inverse of the logic in BrowserRenderer's tryApplySpecialProperty.
21+
// That is, we're doing the reverse mapping, starting from an HTML property and reconstructing which
22+
// "special" attribute would have been mapped to that property.
23+
if (elem instanceof HTMLInputElement) {
24+
return (elem.type && elem.type.toLowerCase() === 'checkbox')
25+
? { value: elem.checked }
26+
: { value: elem.value };
27+
}
28+
29+
if (elem instanceof HTMLSelectElement || elem instanceof HTMLTextAreaElement) {
30+
return { value: elem.value };
31+
}
32+
33+
return null;
34+
}

src/Components/Browser/src/RendererRegistryEventDispatcher.cs

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,38 @@ public static class RendererRegistryEventDispatcher
2121
public static Task DispatchEvent(
2222
BrowserEventDescriptor eventDescriptor, string eventArgsJson)
2323
{
24+
InterpretEventDescriptor(eventDescriptor);
2425
var eventArgs = ParseEventArgsJson(eventDescriptor.EventArgsType, eventArgsJson);
2526
var renderer = RendererRegistry.Current.Find(eventDescriptor.BrowserRendererId);
26-
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventArgs);
27+
return renderer.DispatchEventAsync(eventDescriptor.EventHandlerId, eventDescriptor.EventFieldInfo, eventArgs);
28+
}
29+
30+
private static void InterpretEventDescriptor(BrowserEventDescriptor eventDescriptor)
31+
{
32+
// The incoming field value can be either a bool or a string, but since the .NET property
33+
// type is 'object', it will deserialize initially as a JsonElement
34+
var fieldInfo = eventDescriptor.EventFieldInfo;
35+
if (fieldInfo != null)
36+
{
37+
if (fieldInfo.FieldValue is JsonElement attributeValueJsonElement)
38+
{
39+
switch (attributeValueJsonElement.Type)
40+
{
41+
case JsonValueType.True:
42+
case JsonValueType.False:
43+
fieldInfo.FieldValue = attributeValueJsonElement.GetBoolean();
44+
break;
45+
default:
46+
fieldInfo.FieldValue = attributeValueJsonElement.GetString();
47+
break;
48+
}
49+
}
50+
else
51+
{
52+
// Unanticipated value type. Ensure we don't do anything with it.
53+
eventDescriptor.EventFieldInfo = null;
54+
}
55+
}
2756
}
2857

2958
private static UIEventArgs ParseEventArgsJson(string eventArgsType, string eventArgsJson)
@@ -105,6 +134,11 @@ public class BrowserEventDescriptor
105134
/// For framework use only.
106135
/// </summary>
107136
public string EventArgsType { get; set; }
137+
138+
/// <summary>
139+
/// For framework use only.
140+
/// </summary>
141+
public EventFieldInfo EventFieldInfo { get; set; }
108142
}
109143
}
110144
}

0 commit comments

Comments
 (0)