Skip to content

Commit 4d5ced4

Browse files
author
Ryan Nowak
committed
Add AddMultipleAttributes improve RTB
- Adds AddMultipleAttributes - Fix RTB to de-dupe attributes - Fix RTB behaviour with boxed EventCallback (#8336) - Add lots of tests for new RTB behaviour and EventCallback
1 parent 23f6ccb commit 4d5ced4

File tree

5 files changed

+1292
-100
lines changed

5 files changed

+1292
-100
lines changed

src/Components/Components/src/EventCallback.cs

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ namespace Microsoft.AspNetCore.Components
99
/// <summary>
1010
/// A bound event handler delegate.
1111
/// </summary>
12-
public readonly struct EventCallback
12+
public readonly struct EventCallback : IEventCallback
1313
{
1414
/// <summary>
1515
/// Gets a reference to the <see cref="EventCallbackFactory"/>.
@@ -60,12 +60,17 @@ public Task InvokeAsync(object arg)
6060

6161
return Receiver.HandleEventAsync(new EventCallbackWorkItem(Delegate), arg);
6262
}
63+
64+
object IEventCallback.UnpackForRenderTree()
65+
{
66+
return RequiresExplicitReceiver ? (object)this : Delegate;
67+
}
6368
}
6469

6570
/// <summary>
6671
/// A bound event handler delegate.
6772
/// </summary>
68-
public readonly struct EventCallback<T>
73+
public readonly struct EventCallback<T> : IEventCallback
6974
{
7075
internal readonly MulticastDelegate Delegate;
7176
internal readonly IHandleEvent Receiver;
@@ -86,7 +91,7 @@ public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate)
8691
/// </summary>
8792
public bool HasDelegate => Delegate != null;
8893

89-
// This is a hint to the runtime that Reciever is a different object than what
94+
// This is a hint to the runtime that Receiver is a different object than what
9095
// Delegate.Target points to. This allows us to avoid boxing the command object
9196
// when building the render tree. See logic where this is used.
9297
internal bool RequiresExplicitReceiver => Receiver != null && !object.ReferenceEquals(Receiver, Delegate?.Target);
@@ -111,5 +116,16 @@ internal EventCallback AsUntyped()
111116
{
112117
return new EventCallback(Receiver ?? Delegate?.Target as IHandleEvent, Delegate);
113118
}
119+
120+
object IEventCallback.UnpackForRenderTree()
121+
{
122+
return RequiresExplicitReceiver ? (object)AsUntyped() : Delegate;
123+
}
124+
}
125+
126+
// Used to understand boxed generic EventCallbacks
127+
internal interface IEventCallback
128+
{
129+
object UnpackForRenderTree();
114130
}
115131
}

src/Components/Components/src/RenderTree/ArrayBuilder.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -97,6 +97,17 @@ internal int Append(T[] source, int startIndex, int length)
9797
public void Overwrite(int index, in T value)
9898
=> _items[index] = value;
9999

100+
/// <summary>
101+
/// Removes the item at the specified index.
102+
/// </summary>
103+
/// <param name="index">The index of the item to remove.</param>
104+
public void RemoveAt(int index)
105+
{
106+
Array.Copy(_items, index + 1, _items, index, _itemsInUse - 1 - index);
107+
Array.Clear(_items, _itemsInUse - 1, 1); // Clear last item
108+
_itemsInUse--;
109+
}
110+
100111
/// <summary>
101112
/// Removes the last item.
102113
/// </summary>

src/Components/Components/src/RenderTree/RenderTreeBuilder.cs

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System;
55
using System.Collections.Generic;
6+
using System.Diagnostics;
67
using System.Threading.Tasks;
78
using Microsoft.AspNetCore.Components;
89
using Microsoft.AspNetCore.Components.Rendering;
@@ -157,6 +158,10 @@ public void AddAttribute(int sequence, string name, bool value)
157158
// or absence of an attribute, and false => "False" which isn't falsy in js.
158159
Append(RenderTreeFrame.Attribute(sequence, name, BoxedTrue));
159160
}
161+
else
162+
{
163+
ClearAttributesWithName(name);
164+
}
160165
}
161166

162167
/// <summary>
@@ -178,6 +183,10 @@ public void AddAttribute(int sequence, string name, string value)
178183
{
179184
Append(RenderTreeFrame.Attribute(sequence, name, value));
180185
}
186+
else
187+
{
188+
ClearAttributesWithName(name);
189+
}
181190
}
182191

183192
/// <summary>
@@ -275,6 +284,10 @@ public void AddAttribute(int sequence, string name, MulticastDelegate value)
275284
{
276285
Append(RenderTreeFrame.Attribute(sequence, name, value));
277286
}
287+
else
288+
{
289+
ClearAttributesWithName(name);
290+
}
278291
}
279292

280293
/// <summary>
@@ -372,16 +385,24 @@ public void AddAttribute(int sequence, string name, object value)
372385
{
373386
if (value == null)
374387
{
375-
// Do nothing, treat 'null' attribute values for elements as a conditional attribute.
388+
// Treat 'null' attribute values for elements as a conditional attribute.
389+
ClearAttributesWithName(name);
376390
}
377391
else if (value is bool boolValue)
378392
{
379393
if (boolValue)
380394
{
381395
Append(RenderTreeFrame.Attribute(sequence, name, BoxedTrue));
382396
}
383-
384-
// Don't add anything for false bool value.
397+
else
398+
{
399+
// Don't add anything for false bool value.
400+
ClearAttributesWithName(name);
401+
}
402+
}
403+
else if (value is IEventCallback callbackValue)
404+
{
405+
Append(RenderTreeFrame.Attribute(sequence, name, callbackValue.UnpackForRenderTree()));
385406
}
386407
else if (value is MulticastDelegate)
387408
{
@@ -395,6 +416,7 @@ public void AddAttribute(int sequence, string name, object value)
395416
}
396417
else if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
397418
{
419+
// If this is a component, we always want to preserve the original type.
398420
Append(RenderTreeFrame.Attribute(sequence, name, value));
399421
}
400422
else
@@ -425,6 +447,38 @@ public void AddAttribute(int sequence, in RenderTreeFrame frame)
425447
Append(frame.WithAttributeSequence(sequence));
426448
}
427449

450+
/// <summary>
451+
/// Adds frames representing multiple attributes with the same sequence number.
452+
/// </summary>
453+
/// <typeparam name="T">The attribute value type.</typeparam>
454+
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
455+
/// <param name="attributes">A collection of key-value pairs representing attributes.</param>
456+
public void AddMultipleAttributes<T>(int sequence, IEnumerable<KeyValuePair<string, T>> attributes)
457+
{
458+
// NOTE: The IEnumerable<KeyValuePair<string, T>> is the simplest way to support a variety of
459+
// different types like IReadOnlyDictionary<>, Dictionary<>, and IDictionary<>.
460+
//
461+
// None of those types are contravariant, and since we want to support attributes having a value
462+
// of type object, the simplest thing to do is drop down to IEnumerable<KeyValuePair<>> which
463+
// is contravariant. This also gives us things like List<KeyValuePair<>> and KeyValuePair<>[]
464+
// for free even though we don't expect those types to be common.
465+
466+
// Calling this up-front just to make sure we validate before mutating anything.
467+
AssertCanAddAttribute();
468+
469+
if (attributes != null)
470+
{
471+
foreach (var attribute in attributes)
472+
{
473+
// This will call the AddAttribute(int, string, object) overload.
474+
//
475+
// This is fine because we try to make the object overload behave identically
476+
// to the others.
477+
AddAttribute(sequence, attribute.Key, attribute.Value);
478+
}
479+
}
480+
}
481+
428482
/// <summary>
429483
/// Appends a frame representing a child component.
430484
/// </summary>
@@ -590,13 +644,42 @@ public ArrayRange<RenderTreeFrame> GetFrames() =>
590644

591645
private void Append(in RenderTreeFrame frame)
592646
{
647+
var frameType = frame.FrameType;
648+
if (frameType == RenderTreeFrameType.Attribute)
649+
{
650+
ClearAttributesWithName(frame.AttributeName);
651+
}
652+
593653
_entries.Append(frame);
594654

595-
var frameType = frame.FrameType;
596655
if (frameType != RenderTreeFrameType.Attribute)
597656
{
598657
_lastNonAttributeFrameType = frame.FrameType;
599658
}
600659
}
660+
661+
private void ClearAttributesWithName(string name)
662+
{
663+
// When an AddAttribute or AddMultipleAttributes method is called, we need to clear
664+
// any prior attributes that have the same attribute name for the current element
665+
// or component.
666+
//
667+
// This is how we enforce the *last attribute wins* semantic.
668+
Debug.Assert(_openElementIndices.Count > 0);
669+
670+
// Start at the last open element/component and iterate forward until
671+
// we find a duplicate.
672+
//
673+
// Since we prevent duplicates, we can always stop after finding one.
674+
for (var i = _openElementIndices.Peek(); i < _entries.Count; i++)
675+
{
676+
if (_entries.Buffer[i].FrameType == RenderTreeFrameType.Attribute &&
677+
string.Equals(name, _entries.Buffer[i].AttributeName, StringComparison.OrdinalIgnoreCase))
678+
{
679+
_entries.RemoveAt(i);
680+
break;
681+
}
682+
}
683+
}
601684
}
602685
}

0 commit comments

Comments
 (0)