diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts index 1087f182db..506846af88 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts @@ -52,7 +52,7 @@ export class BrowserRenderer { const frame = getTreeFramePtr(referenceTree, frameIndex); const siblingIndex = renderTreeEdit.siblingIndex(edit); const element = parent.childNodes[childIndexAtCurrentDepth + siblingIndex] as HTMLElement; - this.applyAttribute(componentId, element, frame, frameIndex); + this.applyAttribute(componentId, element, frame); break; } case EditType.removeAttribute: { @@ -119,7 +119,7 @@ export class BrowserRenderer { for (let descendantIndex = frameIndex + 1; descendantIndex < descendantsEndIndexExcl; descendantIndex++) { const descendantFrame = getTreeFramePtr(frames, descendantIndex); if (renderTreeFrame.frameType(descendantFrame) === FrameType.attribute) { - this.applyAttribute(componentId, newDomElement, descendantFrame, descendantIndex); + this.applyAttribute(componentId, newDomElement, descendantFrame); } else { // As soon as we see a non-attribute child, all the subsequent child frames are // not attributes, so bail out and insert the remnants recursively @@ -160,16 +160,17 @@ export class BrowserRenderer { insertNodeIntoDOM(newDomTextNode, parent, childIndex); } - applyAttribute(componentId: number, toDomElement: Element, attributeFrame: RenderTreeFramePointer, attributeFrameIndex: number) { + applyAttribute(componentId: number, toDomElement: Element, attributeFrame: RenderTreeFramePointer) { const attributeName = renderTreeFrame.attributeName(attributeFrame)!; const browserRendererId = this.browserRendererId; + const eventHandlerId = renderTreeFrame.attributeEventHandlerId(attributeFrame); // TODO: Instead of applying separate event listeners to each DOM element, use event delegation // and remove all the _blazor*Listener hacks switch (attributeName) { case 'onclick': { toDomElement.removeEventListener('click', toDomElement['_blazorClickListener']); - const listener = () => raiseEvent(browserRendererId, componentId, attributeFrameIndex, 'mouse', { Type: 'click' }); + const listener = () => raiseEvent(browserRendererId, componentId, eventHandlerId, 'mouse', { Type: 'click' }); toDomElement['_blazorClickListener'] = listener; toDomElement.addEventListener('click', listener); break; @@ -181,7 +182,7 @@ export class BrowserRenderer { // just to establish that we can pass parameters when raising events. // We use C#-style PascalCase on the eventInfo to simplify deserialization, but this could // change if we introduced a richer JSON library on the .NET side. - raiseEvent(browserRendererId, componentId, attributeFrameIndex, 'keyboard', { Type: evt.type, Key: (evt as any).key }); + raiseEvent(browserRendererId, componentId, eventHandlerId, 'keyboard', { Type: evt.type, Key: (evt as any).key }); }; toDomElement['_blazorKeypressListener'] = listener; toDomElement.addEventListener('keypress', listener); @@ -229,7 +230,7 @@ function removeAttributeFromDOM(parent: Element, childIndex: number, attributeNa element.removeAttribute(attributeName); } -function raiseEvent(browserRendererId: number, componentId: number, referenceTreeFrameIndex: number, eventInfoType: EventInfoType, eventInfo: any) { +function raiseEvent(browserRendererId: number, componentId: number, eventHandlerId: number, eventInfoType: EventInfoType, eventInfo: any) { if (!raiseEventMethod) { raiseEventMethod = platform.findMethod( 'Microsoft.AspNetCore.Blazor.Browser', 'Microsoft.AspNetCore.Blazor.Browser.Rendering', 'BrowserRendererEventDispatcher', 'DispatchEvent' @@ -239,7 +240,7 @@ function raiseEvent(browserRendererId: number, componentId: number, referenceTre const eventDescriptor = { BrowserRendererId: browserRendererId, ComponentId: componentId, - ReferenceTreeFrameIndex: referenceTreeFrameIndex, + EventHandlerId: eventHandlerId, EventArgsType: eventInfoType }; diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts index 0780dec46f..9e735a3146 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts +++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/RenderTreeFrame.ts @@ -19,6 +19,7 @@ export const renderTreeFrame = { textContent: (frame: RenderTreeFramePointer) => platform.readStringField(frame, 16), attributeName: (frame: RenderTreeFramePointer) => platform.readStringField(frame, 16), attributeValue: (frame: RenderTreeFramePointer) => platform.readStringField(frame, 24), + attributeEventHandlerId: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 8), }; export enum FrameType { diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs index 947af8a3e4..7f6c09c1d7 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRenderer.cs @@ -32,8 +32,8 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering _browserRendererId = BrowserRendererRegistry.Add(this); } - internal void DispatchBrowserEvent(int componentId, int referenceTreeIndex, UIEventArgs eventArgs) - => DispatchEvent(componentId, referenceTreeIndex, eventArgs); + internal void DispatchBrowserEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs) + => DispatchEvent(componentId, eventHandlerId, eventArgs); internal void RenderNewBatchInternal(int componentId) => RenderNewBatch(componentId); diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs index 51cab8fa7c..4799e94c65 100644 --- a/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs +++ b/src/Microsoft.AspNetCore.Blazor.Browser/Rendering/BrowserRendererEventDispatcher.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering var browserRenderer = BrowserRendererRegistry.Find(eventDescriptor.BrowserRendererId); browserRenderer.DispatchBrowserEvent( eventDescriptor.ComponentId, - eventDescriptor.ReferenceTreeFrameIndex, + eventDescriptor.EventHandlerId, eventArgs); } @@ -45,7 +45,7 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Rendering { public int BrowserRendererId { get; set; } public int ComponentId { get; set; } - public int ReferenceTreeFrameIndex { get; set; } + public int EventHandlerId { get; set; } public string EventArgsType { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs index 138462758f..5bd5e44481 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffComputer.cs @@ -134,49 +134,15 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (treatAsInsert) { - ref var newFrame = ref newTree[newStartIndex]; - var newFrameType = newFrame.FrameType; - if (newFrameType == RenderTreeFrameType.Attribute) - { - var referenceFrameIndex = _referenceFrames.Append(newFrame); - Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); - newStartIndex++; - } - else - { - if (newFrameType == RenderTreeFrameType.Element || newFrameType == RenderTreeFrameType.Component) - { - InstantiateChildComponents(batchBuilder, newTree, newStartIndex); - } - - var referenceFrameIndex = AppendSubtreeToReferenceFrames(newTree, newStartIndex); - Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); - newStartIndex = NextSiblingIndex(newFrame, newStartIndex); - siblingIndex++; - } - + InsertNewFrame(batchBuilder, newTree, newStartIndex, ref siblingIndex); + newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex); hasMoreNew = newEndIndexExcl > newStartIndex; prevNewSeq = newSeq; } else { - ref var oldFrame = ref oldTree[oldStartIndex]; - var oldFrameType = oldFrame.FrameType; - if (oldFrameType == RenderTreeFrameType.Attribute) - { - Append(RenderTreeEdit.RemoveAttribute(siblingIndex, oldFrame.AttributeName)); - oldStartIndex++; - } - else - { - if (oldFrameType == RenderTreeFrameType.Element || oldFrameType == RenderTreeFrameType.Component) - { - DisposeChildComponents(batchBuilder, oldTree, oldStartIndex); - } - - Append(RenderTreeEdit.RemoveFrame(siblingIndex)); - oldStartIndex = NextSiblingIndex(oldFrame, oldStartIndex); - } + RemoveOldFrame(batchBuilder, oldTree, oldStartIndex, siblingIndex); + oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex); hasMoreOld = oldEndIndexExcl > oldStartIndex; prevOldSeq = oldSeq; } @@ -184,35 +150,6 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } } - public UIEventHandler TemporaryGetEventHandlerMethod(int referenceFrameIndex) - { - // TODO: Change how event handlers are referenced - // The approach used here is invalid. We can't really assume that the event handler exists - // in the most recent diff's _referenceFrames. Nor can we assume that its index in the - // ComponentState's _renderTreeBuilderCurrent frames array is unchanged (any number of rerenders - // might have taken place since then, inserting frames before it, but not actually changing - // the delegate instance and therefore not including it in any _referenceFrames). - // Need some way of referencing event handlers that is independent of this. - var eventHandler = _referenceFrames.Buffer[referenceFrameIndex].AttributeValue as UIEventHandler; - return eventHandler - ?? throw new ArgumentException($"The reference frame at index {referenceFrameIndex} does not specify a {nameof(UIEventHandler)}."); - } - - private int AppendSubtreeToReferenceFrames(RenderTreeFrame[] fromTree, int fromIndex) - { - ref var rootFrame = ref fromTree[fromIndex]; - var subtreeLength = rootFrame.ElementSubtreeLength; - if (subtreeLength == 0) - { - // It's just a single frame (e.g., a text frame) - return _referenceFrames.Append(rootFrame); - } - else - { - return _referenceFrames.Append(fromTree, fromIndex, subtreeLength); - } - } - private void UpdateRetainedChildComponent( RenderBatchBuilder batchBuilder, RenderTreeFrame[] oldTree, int oldComponentIndex, @@ -365,6 +302,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // We can assume that the old and new frames are of the same type, because they correspond // to the same sequence number (and if not, the behaviour is undefined). + // TODO: Consider supporting dissimilar types at same sequence for custom IComponent implementations. + // It should only be a matter of calling RemoveOldFrame+InsertNewFrame switch (newTree[newFrameIndex].FrameType) { case RenderTreeFrameType.Text: @@ -374,7 +313,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree if (!string.Equals(oldText, newText, StringComparison.Ordinal)) { var referenceFrameIndex = _referenceFrames.Append(newFrame); - Append(RenderTreeEdit.UpdateText(siblingIndex, referenceFrameIndex)); + _entries.Append(RenderTreeEdit.UpdateText(siblingIndex, referenceFrameIndex)); } siblingIndex++; break; @@ -404,14 +343,14 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree newFrameChildrenEndIndexExcl > newFrameAttributesEndIndexExcl; if (hasChildrenToProcess) { - Append(RenderTreeEdit.StepIn(siblingIndex)); + _entries.Append(RenderTreeEdit.StepIn(siblingIndex)); var childSiblingIndex = 0; AppendDiffEntriesForRange( batchBuilder, oldTree, oldFrameAttributesEndIndexExcl, oldFrameChildrenEndIndexExcl, newTree, newFrameAttributesEndIndexExcl, newFrameChildrenEndIndexExcl, ref childSiblingIndex); - Append(RenderTreeEdit.StepOut()); + AppendStepOut(); siblingIndex++; } else @@ -422,40 +361,27 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree else { // Elements with different names are treated as completely unrelated - InstantiateChildComponents(batchBuilder, newTree, newFrameIndex); - DisposeChildComponents(batchBuilder, oldTree, oldFrameIndex); - - var referenceFrameIndex = AppendSubtreeToReferenceFrames(newTree, newFrameIndex); - Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); - siblingIndex++; - Append(RenderTreeEdit.RemoveFrame(siblingIndex)); + RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); + InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); } break; } case RenderTreeFrameType.Component: { - var oldComponentType = oldFrame.ComponentType; - var newComponentType = newFrame.ComponentType; - if (oldComponentType == newComponentType) + if (oldFrame.ComponentType == newFrame.ComponentType) { UpdateRetainedChildComponent( batchBuilder, oldTree, oldFrameIndex, newTree, newFrameIndex); - siblingIndex++; } else { // Child components of different types are treated as completely unrelated - InstantiateChildComponents(batchBuilder, newTree, newFrameIndex); - DisposeChildComponents(batchBuilder, oldTree, oldFrameIndex); - - var referenceFrameIndex = AppendSubtreeToReferenceFrames(newTree, newFrameIndex); - Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); - siblingIndex++; - Append(RenderTreeEdit.RemoveFrame(siblingIndex)); + RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); + InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); } break; } @@ -470,8 +396,18 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree var valueChanged = !Equals(oldFrame.AttributeValue, newFrame.AttributeValue); if (valueChanged) { + if (oldFrame.AttributeEventHandlerId > 0) + { + batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); + } + InitializeNewAttributeFrame(ref newFrame); var referenceFrameIndex = _referenceFrames.Append(newFrame); - Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); + _entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); + } + else if (oldFrame.AttributeEventHandlerId > 0) + { + // Retain the event handler ID + newFrame = oldFrame; } } else @@ -479,9 +415,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // Since this code path is never reachable for Razor components (because you // can't have two different attribute names from the same source sequence), we // could consider removing the 'name equality' check entirely for perf - var referenceFrameIndex = _referenceFrames.Append(newFrame); - Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); - Append(RenderTreeEdit.RemoveAttribute(siblingIndex, oldName)); + RemoveOldFrame(batchBuilder, oldTree, oldFrameIndex, siblingIndex); + InsertNewFrame(batchBuilder, newTree, newFrameIndex, ref siblingIndex); } break; } @@ -491,7 +426,68 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree } } - private int GetAttributesEndIndexExclusive(RenderTreeFrame[] tree, int rootIndex) + private void InsertNewFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] newTree, int newFrameIndex, ref int siblingIndex) + { + ref var newFrame = ref newTree[newFrameIndex]; + switch (newFrame.FrameType) + { + case RenderTreeFrameType.Attribute: + { + InitializeNewAttributeFrame(ref newFrame); + var referenceFrameIndex = _referenceFrames.Append(newFrame); + _entries.Append(RenderTreeEdit.SetAttribute(siblingIndex, referenceFrameIndex)); + break; + } + case RenderTreeFrameType.Component: + case RenderTreeFrameType.Element: + { + InitializeNewSubtree(batchBuilder, newTree, newFrameIndex); + var referenceFrameIndex = _referenceFrames.Append(newTree, newFrameIndex, newFrame.ElementSubtreeLength); + _entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); + siblingIndex++; + break; + } + case RenderTreeFrameType.Text: + { + var referenceFrameIndex = _referenceFrames.Append(newFrame); + _entries.Append(RenderTreeEdit.PrependFrame(siblingIndex, referenceFrameIndex)); + siblingIndex++; + break; + } + } + } + + private void RemoveOldFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] oldTree, int oldFrameIndex, int siblingIndex) + { + ref var oldFrame = ref oldTree[oldFrameIndex]; + switch (oldFrame.FrameType) + { + case RenderTreeFrameType.Attribute: + { + _entries.Append(RenderTreeEdit.RemoveAttribute(siblingIndex, oldFrame.AttributeName)); + if (oldFrame.AttributeEventHandlerId > 0) + { + batchBuilder.AddDisposedEventHandlerId(oldFrame.AttributeEventHandlerId); + } + break; + } + case RenderTreeFrameType.Component: + case RenderTreeFrameType.Element: + { + var endIndexExcl = oldFrameIndex + oldFrame.ElementSubtreeLength; + DisposeFramesInRange(batchBuilder, oldTree, oldFrameIndex, endIndexExcl); + _entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex)); + break; + } + case RenderTreeFrameType.Text: + { + _entries.Append(RenderTreeEdit.RemoveFrame(siblingIndex)); + break; + } + } + } + + private static int GetAttributesEndIndexExclusive(RenderTreeFrame[] tree, int rootIndex) { var descendantsEndIndexExcl = rootIndex + tree[rootIndex].ElementSubtreeLength; var index = rootIndex + 1; @@ -506,54 +502,72 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree return index; } - private void Append(in RenderTreeEdit entry) + private void AppendStepOut() { - if (entry.Type == RenderTreeEditType.StepOut) + // If the preceding frame is a StepIn, then the StepOut cancels it out + var previousIndex = _entries.Count - 1; + if (previousIndex >= 0 && _entries.Buffer[previousIndex].Type == RenderTreeEditType.StepIn) { - // If the preceding frame is a StepIn, then the StepOut cancels it out - var previousIndex = _entries.Count - 1; - if (previousIndex >= 0 && _entries.Buffer[previousIndex].Type == RenderTreeEditType.StepIn) - { - _entries.RemoveLast(); - return; - } + _entries.RemoveLast(); + } + else + { + _entries.Append(RenderTreeEdit.StepOut()); } - - _entries.Append(entry); } - private void InstantiateChildComponents(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int elementOrComponentIndex) + private void InitializeNewSubtree(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int frameIndex) { - var endIndexExcl = elementOrComponentIndex + frames[elementOrComponentIndex].ElementSubtreeLength; - for (var i = elementOrComponentIndex; i < endIndexExcl; i++) + var endIndexExcl = frameIndex + frames[frameIndex].ElementSubtreeLength; + for (var i = frameIndex; i < endIndexExcl; i++) { ref var frame = ref frames[i]; - if (frame.FrameType == RenderTreeFrameType.Component) + switch (frame.FrameType) { - if (frame.Component != null) - { - throw new InvalidOperationException($"Child component already exists during {nameof(InstantiateChildComponents)}"); - } - - _renderer.InstantiateChildComponent(frames, i); - var childComponentInstance = frame.Component; - - // All descendants of a component are its properties - var componentDescendantsEndIndexExcl = i + frame.ComponentSubtreeLength; - for (var attributeFrameIndex = i + 1; attributeFrameIndex < componentDescendantsEndIndexExcl; attributeFrameIndex++) - { - ref var attributeFrame = ref frames[attributeFrameIndex]; - SetChildComponentProperty( - childComponentInstance, - attributeFrame.AttributeName, - attributeFrame.AttributeValue); - } - - TriggerChildComponentRender(batchBuilder, frame); + case RenderTreeFrameType.Component: + InitializeNewComponentFrame(batchBuilder, frames, i); + break; + case RenderTreeFrameType.Attribute: + InitializeNewAttributeFrame(ref frame); + break; } } } + private void InitializeNewComponentFrame(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int frameIndex) + { + ref var frame = ref frames[frameIndex]; + + if (frame.Component != null) + { + throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); + } + + _renderer.InstantiateChildComponent(ref frame); + var childComponentInstance = frame.Component; + + // All descendants of a component are its properties + var componentDescendantsEndIndexExcl = frameIndex + frame.ComponentSubtreeLength; + for (var attributeFrameIndex = frameIndex + 1; attributeFrameIndex < componentDescendantsEndIndexExcl; attributeFrameIndex++) + { + ref var attributeFrame = ref frames[attributeFrameIndex]; + SetChildComponentProperty( + childComponentInstance, + attributeFrame.AttributeName, + attributeFrame.AttributeValue); + } + + TriggerChildComponentRender(batchBuilder, frame); + } + + private void InitializeNewAttributeFrame(ref RenderTreeFrame newFrame) + { + if (newFrame.AttributeValue is UIEventHandler) + { + _renderer.AssignEventHandlerId(ref newFrame); + } + } + private void TriggerChildComponentRender(RenderBatchBuilder batchBuilder, in RenderTreeFrame frame) { if (frame.Component is IHandlePropertiesChanged notifyableComponent) @@ -571,16 +585,22 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree _renderer.RenderInExistingBatch(batchBuilder, frame.ComponentId); } - private void DisposeChildComponents(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int elementOrComponentIndex) + internal void DisposeFrames(RenderBatchBuilder batchBuilder, ArrayRange frames) + => DisposeFramesInRange(batchBuilder, frames.Array, 0, frames.Count); + + private void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl) { - var endIndexExcl = elementOrComponentIndex + frames[elementOrComponentIndex].ElementSubtreeLength; - for (var i = elementOrComponentIndex; i < endIndexExcl; i++) + for (var i = startIndex; i < endIndexExcl; i++) { ref var frame = ref frames[i]; - if (frame.FrameType == RenderTreeFrameType.Component) + if (frame.FrameType == RenderTreeFrameType.Component && frame.Component != null) { _renderer.DisposeInExistingBatch(batchBuilder, frame.ComponentId); } + else if (frame.FrameType == RenderTreeFrameType.Attribute && frame.AttributeEventHandlerId > 0) + { + batchBuilder.AddDisposedEventHandlerId(frame.AttributeEventHandlerId); + } } } } diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs index 6aae012526..2f113b6b46 100644 --- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs +++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeFrame.cs @@ -71,6 +71,12 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree // RenderTreeFrameType.Attribute // -------------------------------------------------------------------------------- + /// + /// If the property equals + /// gets the ID of the corresponding event handler, if any. + /// + [FieldOffset(8)] public readonly int AttributeEventHandlerId; + /// /// If the property equals , /// gets the attribute name. Otherwise, the value is . @@ -154,6 +160,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree AttributeValue = attributeValue; } + private RenderTreeFrame(int sequence, string attributeName, object attributeValue, int eventHandlerId) + : this() + { + FrameType = RenderTreeFrameType.Attribute; + Sequence = sequence; + AttributeName = attributeName; + AttributeValue = attributeValue; + AttributeEventHandlerId = eventHandlerId; + } + internal static RenderTreeFrame Element(int sequence, string elementName) => new RenderTreeFrame(sequence, elementName: elementName, elementSubtreeLength: 0); @@ -180,5 +196,8 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree internal RenderTreeFrame WithComponentInstance(int componentId, IComponent component) => new RenderTreeFrame(Sequence, ComponentType, ComponentSubtreeLength, componentId, component); + + internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId) + => new RenderTreeFrame(Sequence, AttributeName, AttributeValue, eventHandlerId); } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs index 119fb3e058..673dcbef99 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/ComponentState.cs @@ -55,36 +55,18 @@ namespace Microsoft.AspNetCore.Blazor.Rendering _renderTreeBuilderCurrent.GetFrames()); } - /// - /// Invokes the handler corresponding to an event. - /// - /// The index of the frame in the latest diff reference tree that holds the event handler to be invoked. - /// Arguments to be passed to the event handler. - public void DispatchEvent(int referenceTreeIndex, UIEventArgs eventArgs) - { - if (eventArgs == null) - { - throw new ArgumentNullException(nameof(eventArgs)); - } - - var eventHandler = _diffComputer.TemporaryGetEventHandlerMethod(referenceTreeIndex); - eventHandler.Invoke(eventArgs); - - // After any event, we synchronously re-render. Most of the time this means that - // developers don't need to call Render() on their components explicitly. - _renderer.RenderNewBatch(_componentId); - } - /// /// Notifies the component that it is being disposed. /// - public void NotifyDisposed() + public void NotifyDisposed(RenderBatchBuilder batchBuilder) { // TODO: Handle components throwing during dispose. Shouldn't break the whole render batch. if (_component is IDisposable disposable) { disposable.Dispose(); } + + _diffComputer.DisposeFrames(batchBuilder, _renderTreeBuilderCurrent.GetFrames()); } } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs index 9018c9e5fe..3b09fc9295 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs @@ -10,6 +10,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering { private ArrayBuilder _updatedComponentDiffs = new ArrayBuilder(); private ArrayBuilder _disposedComponentIds = new ArrayBuilder(); + private ArrayBuilder _disposedEventHandlerIds = new ArrayBuilder(); public int ReserveUpdatedComponentSlotId() { @@ -21,10 +22,14 @@ namespace Microsoft.AspNetCore.Blazor.Rendering public void SetUpdatedComponent(int updatedComponentSlotId, RenderTreeDiff diff) => _updatedComponentDiffs.Overwrite(updatedComponentSlotId, diff); + public ArrayRange GetDisposedEventHandlerIds() + => _disposedEventHandlerIds.ToRange(); + public void Clear() { _updatedComponentDiffs.Clear(); _disposedComponentIds.Clear(); + _disposedEventHandlerIds.Clear(); } public RenderBatch ToBatch() @@ -34,5 +39,8 @@ namespace Microsoft.AspNetCore.Blazor.Rendering public void AddDisposedComponent(int componentId) => _disposedComponentIds.Append(componentId); + + public void AddDisposedEventHandlerId(int attributeEventHandlerId) + => _disposedEventHandlerIds.Append(attributeEventHandlerId); } } diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs index 75902d6a85..4a9883bff2 100644 --- a/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs +++ b/src/Microsoft.AspNetCore.Blazor/Rendering/Renderer.cs @@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.RenderTree; using System; +using System.Collections.Generic; using System.Runtime.CompilerServices; using System.Threading; @@ -30,6 +31,10 @@ namespace Microsoft.AspNetCore.Blazor.Rendering private readonly RenderBatchBuilder _sharedRenderBatchBuilder = new RenderBatchBuilder(); private int _renderBatchLock = 0; + private int _lastEventHandlerId = 0; + private readonly Dictionary _eventHandlersById + = new Dictionary(); + /// /// Associates the with the , assigning /// an identifier that is unique within the scope of the . @@ -83,6 +88,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering { RenderInExistingBatch(_sharedRenderBatchBuilder, componentId); UpdateDisplay(_sharedRenderBatchBuilder.ToBatch()); + RemoveEventHandlerIds(_sharedRenderBatchBuilder.GetDisposedEventHandlerIds()); } finally { @@ -98,7 +104,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering internal void DisposeInExistingBatch(RenderBatchBuilder batchBuilder, int componentId) { - GetRequiredComponentState(componentId).NotifyDisposed(); + GetRequiredComponentState(componentId).NotifyDisposed(batchBuilder); batchBuilder.AddDisposedComponent(componentId); } @@ -106,14 +112,26 @@ namespace Microsoft.AspNetCore.Blazor.Rendering /// Notifies the specified component that an event has occurred. /// /// The unique identifier for the component within the scope of this . - /// The index into the component's latest diff reference tree that specifies which event handler to invoke. + /// The value from the original event attribute. /// Arguments to be passed to the event handler. - protected void DispatchEvent(int componentId, int referenceTreeIndex, UIEventArgs eventArgs) - => GetRequiredComponentState(componentId).DispatchEvent(referenceTreeIndex, eventArgs); - - internal void InstantiateChildComponent(RenderTreeFrame[] frames, int componentFrameIndex) + protected void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs eventArgs) + { + if (_eventHandlersById.TryGetValue(eventHandlerId, out var handler)) + { + handler.Invoke(eventArgs); + + // After any event, we synchronously re-render. Most of the time this means that + // developers don't need to call Render() on their components explicitly. + RenderNewBatch(componentId); + } + else + { + throw new ArgumentException($"There is no event handler with ID {eventHandlerId}"); + } + } + + internal void InstantiateChildComponent(ref RenderTreeFrame frame) { - ref var frame = ref frames[componentFrameIndex]; if (frame.FrameType != RenderTreeFrameType.Component) { throw new ArgumentException($"The frame's {nameof(RenderTreeFrame.FrameType)} property must equal {RenderTreeFrameType.Component}", nameof(frame)); @@ -129,6 +147,23 @@ namespace Microsoft.AspNetCore.Blazor.Rendering frame = frame.WithComponentInstance(newComponentId, newComponent); } + internal void AssignEventHandlerId(ref RenderTreeFrame frame) + { + var id = ++_lastEventHandlerId; + _eventHandlersById.Add(id, (UIEventHandler)frame.AttributeValue); + frame = frame.WithAttributeEventHandlerId(id); + } + + private void RemoveEventHandlerIds(ArrayRange eventHandlerIds) + { + var array = eventHandlerIds.Array; + var count = eventHandlerIds.Count; + for (var i = 0; i < count; i++) + { + _eventHandlersById.Remove(array[i]); + } + } + private ComponentState GetRequiredComponentState(int componentId) => _componentStateById.TryGetValue(componentId, out var componentState) ? componentState diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs index cdcd043f03..0c2b1d1a2c 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffComputerTest.cs @@ -310,12 +310,12 @@ namespace Microsoft.AspNetCore.Blazor.Test // Assert Assert.Collection(result.Edits, + entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 0), entry => { AssertEdit(entry, RenderTreeEditType.PrependFrame, 0); Assert.Equal(0, entry.ReferenceFrameIndex); - }, - entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 1)); + }); } [Fact] @@ -324,14 +324,14 @@ namespace Microsoft.AspNetCore.Blazor.Test // Arrange oldTree.OpenComponent(123); oldTree.CloseComponent(); + GetRenderedBatch(new RenderTreeBuilder(renderer), oldTree); // Assign initial IDs newTree.OpenComponent(123); newTree.CloseComponent(); // Act var renderBatch = GetRenderedBatch(); - // Assert: Even though we didn't assign IDs to the components, this - // shows that FakeComponent was disposed + // Assert Assert.Collection(renderBatch.DisposedComponentIDs, disposedComponentId => Assert.Equal(0, disposedComponentId)); @@ -340,13 +340,13 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Equal(2, renderBatch.UpdatedComponents.Count); var updatedComponent1 = renderBatch.UpdatedComponents.Array[0]; Assert.Collection(updatedComponent1.Edits, + entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 0), entry => { AssertEdit(entry, RenderTreeEditType.PrependFrame, 0); Assert.Equal(0, entry.ReferenceFrameIndex); Assert.IsType(updatedComponent1.ReferenceFrames.Array[0].Component); - }, - entry => AssertEdit(entry, RenderTreeEditType.RemoveFrame, 1)); + }); // Assert: Second updated component is the new FakeComponent2 var updatedComponent2 = renderBatch.UpdatedComponents.Array[1]; @@ -483,13 +483,13 @@ namespace Microsoft.AspNetCore.Blazor.Test Assert.Collection(result.Edits, entry => { - AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); - Assert.Equal(0, entry.ReferenceFrameIndex); + AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0); + Assert.Equal("oldname", entry.RemovedAttributeName); }, entry => { - AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0); - Assert.Equal("oldname", entry.RemovedAttributeName); + AssertEdit(entry, RenderTreeEditType.SetAttribute, 0); + Assert.Equal(0, entry.ReferenceFrameIndex); }); Assert.Collection(result.ReferenceFrames, frame => AssertFrame.Attribute(frame, "newname", "same value")); diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs index 2429cc1411..10e32eb954 100644 --- a/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs +++ b/test/Microsoft.AspNetCore.Blazor.Test/RendererTest.cs @@ -173,16 +173,17 @@ namespace Microsoft.AspNetCore.Blazor.Test var componentId = renderer.AssignComponentId(component); renderer.RenderNewBatch(componentId); - var (eventHandlerFrameIndex, _) = FirstWithIndex( - renderer.Batches.Single().DiffsByComponentId[componentId].Single().ReferenceFrames, - frame => frame.AttributeValue != null); + var eventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; // Assert: Event not yet fired Assert.Null(receivedArgs); // Act/Assert: Event can be fired var eventArgs = new UIEventArgs(); - renderer.DispatchEvent(componentId, eventHandlerFrameIndex, eventArgs); + renderer.DispatchEvent(componentId, eventHandlerId, eventArgs); Assert.Same(eventArgs, receivedArgs); } @@ -211,17 +212,18 @@ namespace Microsoft.AspNetCore.Blazor.Test var nestedComponentId = nestedComponentFrame.ComponentId; renderer.RenderNewBatch(nestedComponentId); - // Find nested component's event handler ndoe - var (eventHandlerFrameIndex, _) = FirstWithIndex( - renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single().ReferenceFrames, - frame => frame.AttributeValue != null); + // Find nested component's event handler ID + var eventHandlerId = renderer.Batches[1].DiffsByComponentId[nestedComponentId].Single() + .ReferenceFrames + .First(frame => frame.AttributeValue != null) + .AttributeEventHandlerId; // Assert: Event not yet fired Assert.Null(receivedArgs); // Act/Assert: Event can be fired var eventArgs = new UIEventArgs(); - renderer.DispatchEvent(nestedComponentId, eventHandlerFrameIndex, eventArgs); + renderer.DispatchEvent(nestedComponentId, eventHandlerId, eventArgs); Assert.Same(eventArgs, receivedArgs); } @@ -474,9 +476,9 @@ namespace Microsoft.AspNetCore.Blazor.Test builder.OpenElement(7, "some element"); if (firstRender) { - builder.OpenComponent(100); - builder.CloseComponent(); - builder.OpenComponent(150); + // Nested descendants + builder.OpenComponent>(100); + builder.AddAttribute(101, nameof(ConditionalParentComponent.IncludeChild), true); builder.CloseComponent(); } builder.OpenComponent(200); @@ -494,14 +496,162 @@ namespace Microsoft.AspNetCore.Blazor.Test .Where(frame => frame.FrameType == RenderTreeFrameType.Component) .Select(frame => frame.ComponentId) .ToList(); - Assert.Equal(childComponentIds, new[] { 1, 2, 3 }); + Assert.Equal(new[] { 1, 3 }, childComponentIds); // Act: Second render firstRender = false; renderer.RenderNewBatch(rootComponentId); // Assert: Applicable children are included in disposal list - Assert.Equal(renderer.Batches[1].DisposedComponentIDs, new[] { 1, 2 }); + Assert.Equal(new[] { 2, 1 }, renderer.Batches[1].DisposedComponentIDs); + } + + [Fact] + public void DisposesEventHandlersWhenAttributeValueChanged() + { + // Arrange + var renderer = new TestRenderer(); + var eventCount = 0; + UIEventHandler origEventHandler = args => { eventCount++; }; + var component = new EventComponent { Handler = origEventHandler }; + var componentId = renderer.AssignComponentId(component); + renderer.RenderNewBatch(componentId); + var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + .ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Attribute) + .Single(f => f.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act/Assert 1: Event handler fires when we trigger it + Assert.Equal(0, eventCount); + renderer.DispatchEvent(componentId, origEventHandlerId, args: null); + Assert.Equal(1, eventCount); + + // Now change the attribute value + var newEventCount = 0; + component.Handler = args => { newEventCount++; }; + renderer.RenderNewBatch(componentId); + + // Act/Assert 2: Can no longer fire the original event, but can fire the new event + Assert.Throws(() => + { + renderer.DispatchEvent(componentId, origEventHandlerId, args: null); + }); + Assert.Equal(1, eventCount); + Assert.Equal(0, newEventCount); + renderer.DispatchEvent(componentId, origEventHandlerId + 1, args: null); + Assert.Equal(1, newEventCount); + } + + [Fact] + public void DisposesEventHandlersWhenAttributeRemoved() + { + // Arrange + var renderer = new TestRenderer(); + var eventCount = 0; + UIEventHandler origEventHandler = args => { eventCount++; }; + var component = new EventComponent { Handler = origEventHandler }; + var componentId = renderer.AssignComponentId(component); + renderer.RenderNewBatch(componentId); + var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + .ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Attribute) + .Single(f => f.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act/Assert 1: Event handler fires when we trigger it + Assert.Equal(0, eventCount); + renderer.DispatchEvent(componentId, origEventHandlerId, args: null); + Assert.Equal(1, eventCount); + + // Now remove the event attribute + component.Handler = null; + renderer.RenderNewBatch(componentId); + + // Act/Assert 2: Can no longer fire the original event + Assert.Throws(() => + { + renderer.DispatchEvent(componentId, origEventHandlerId, args: null); + }); + Assert.Equal(1, eventCount); + } + + [Fact] + public void DisposesEventHandlersWhenOwnerComponentRemoved() + { + // Arrange + var renderer = new TestRenderer(); + var eventCount = 0; + UIEventHandler origEventHandler = args => { eventCount++; }; + var component = new ConditionalParentComponent + { + IncludeChild = true, + ChildParameters = new Dictionary + { + { nameof(EventComponent.Handler), origEventHandler } + } + }; + var rootComponentId = renderer.AssignComponentId(component); + renderer.RenderNewBatch(rootComponentId); + var childComponentId = renderer.Batches.Single().DiffsByComponentId[rootComponentId].Single() + .ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Component) + .Single() + .ComponentId; + var eventHandlerId = renderer.Batches.Single().DiffsByComponentId[childComponentId].Single() + .ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Attribute) + .Single(f => f.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act/Assert 1: Event handler fires when we trigger it + Assert.Equal(0, eventCount); + renderer.DispatchEvent(childComponentId, eventHandlerId, args: null); + Assert.Equal(1, eventCount); + + // Now remove the EventComponent + component.IncludeChild = false; + renderer.RenderNewBatch(rootComponentId); + + // Act/Assert 2: Can no longer fire the original event + Assert.Throws(() => + { + renderer.DispatchEvent(eventHandlerId, eventHandlerId, args: null); + }); + Assert.Equal(1, eventCount); + } + + [Fact] + public void DisposesEventHandlersWhenAncestorElementRemoved() + { + // Arrange + var renderer = new TestRenderer(); + var eventCount = 0; + UIEventHandler origEventHandler = args => { eventCount++; }; + var component = new EventComponent { Handler = origEventHandler }; + var componentId = renderer.AssignComponentId(component); + renderer.RenderNewBatch(componentId); + var origEventHandlerId = renderer.Batches.Single().DiffsByComponentId[componentId].Single() + .ReferenceFrames + .Where(f => f.FrameType == RenderTreeFrameType.Attribute) + .Single(f => f.AttributeEventHandlerId != 0) + .AttributeEventHandlerId; + + // Act/Assert 1: Event handler fires when we trigger it + Assert.Equal(0, eventCount); + renderer.DispatchEvent(componentId, origEventHandlerId, args: null); + Assert.Equal(1, eventCount); + + // Now remove the ancestor element + component.SkipElement = true; + renderer.RenderNewBatch(componentId); + + // Act/Assert 2: Can no longer fire the original event + Assert.Throws(() => + { + renderer.DispatchEvent(componentId, origEventHandlerId, args: null); + }); + Assert.Equal(1, eventCount); } private class NoOpRenderer : Renderer @@ -528,8 +678,8 @@ namespace Microsoft.AspNetCore.Blazor.Test public new void RenderNewBatch(int componentId) => base.RenderNewBatch(componentId); - public new void DispatchEvent(int componentId, int renderTreeIndex, UIEventArgs args) - => base.DispatchEvent(componentId, renderTreeIndex, args); + public new void DispatchEvent(int componentId, int eventHandlerId, UIEventArgs args) + => base.DispatchEvent(componentId, eventHandlerId, args); protected internal override void UpdateDisplay(RenderBatch renderBatch) { @@ -601,15 +751,51 @@ namespace Microsoft.AspNetCore.Blazor.Test private class EventComponent : IComponent { public UIEventHandler Handler { get; set; } + public bool SkipElement { get; set; } public void BuildRenderTree(RenderTreeBuilder builder) { - builder.OpenElement(0, "some element"); - builder.AddAttribute(1, "some event", Handler); + builder.OpenElement(0, "grandparent"); + if (!SkipElement) + { + builder.OpenElement(1, "parent"); + builder.OpenElement(2, "some element"); + if (Handler != null) + { + builder.AddAttribute(3, "some event", Handler); + } + builder.CloseElement(); + builder.CloseElement(); + } builder.CloseElement(); } } + private class ConditionalParentComponent : IComponent where T : IComponent + { + public bool IncludeChild { get; set; } + public IDictionary ChildParameters { get; set; } + + public void BuildRenderTree(RenderTreeBuilder builder) + { + builder.AddText(0, "Parent here"); + + if (IncludeChild) + { + builder.OpenComponent(1); + if (ChildParameters != null) + { + var sequence = 2; + foreach (var kvp in ChildParameters) + { + builder.AddAttribute(sequence++, kvp.Key, kvp.Value); + } + } + builder.CloseComponent(); + } + } + } + void AssertCanBeCollected(Func targetFactory) { // We have to construct the WeakReference in a separate scope diff --git a/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml b/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml index 82a1e0fbfd..f929f23651 100644 --- a/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml +++ b/test/testapps/BasicTestApp/KeyPressEventComponent.cshtml @@ -1,6 +1,6 @@ @using System.Collections.Generic @using Microsoft.AspNetCore.Blazor.RenderTree -Type here: +Type here:
    @foreach (var key in keysPressed) { @@ -11,22 +11,7 @@ Type here: @functions { List keysPressed = new List(); - // TODO: Fix this - // Currently, you can only trigger an event handler whose value changed in the most - // recent render cycle. That's because we reference the event handlers by their index - // into the current diff's ReferenceFrames array. We need some better mechanism of - // locating the delegates that is independent of whether the corresponding attribute - // changed in the last diff, and not assuming the attribute in the original render - // tree is still at the same index. - // Once that's fixed, remove the 'CreateNewDelegateInstance' method entirely and - // the 'irrelevantObject' arg from below, and simplify to onkeypress=@OnKeyPressed - UIEventHandler CreateNewDelegateInstance() - { - var irrelevantObject = new object(); - return args => OnKeyPressed(args, irrelevantObject); - } - - void OnKeyPressed(UIEventArgs eventArgs, object irrelevantObject) + void OnKeyPressed(UIEventArgs eventArgs) { keysPressed.Add(((UIKeyboardEventArgs)eventArgs).Key); }