/
RenderTreeBuilder.cs
765 lines (697 loc) · 34.3 KB
/
RenderTreeBuilder.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components.RenderTree;
namespace Microsoft.AspNetCore.Components.Rendering
{
// IMPORTANT
//
// Many of these names are used in code generation. Keep these in sync with the code generation code
// See: aspnet/AspNetCore-Tooling
/// <summary>
/// Provides methods for building a collection of <see cref="RenderTreeFrame"/> entries.
/// </summary>
public sealed class RenderTreeBuilder : IDisposable
{
private readonly static object BoxedTrue = true;
private readonly static object BoxedFalse = false;
private readonly static string ComponentReferenceCaptureInvalidParentMessage = $"Component reference captures may only be added as children of frames of type {RenderTreeFrameType.Component}";
private readonly ArrayBuilder<RenderTreeFrame> _entries = new ArrayBuilder<RenderTreeFrame>();
private readonly Stack<int> _openElementIndices = new Stack<int>();
private RenderTreeFrameType? _lastNonAttributeFrameType;
private bool _hasSeenAddMultipleAttributes;
private Dictionary<string, int> _seenAttributeNames;
/// <summary>
/// The reserved parameter name used for supplying child content.
/// </summary>
private const string ChildContent = nameof(ChildContent);
/// <summary>
/// Appends a frame representing an element, i.e., a container for other frames.
/// In order for the <see cref="RenderTreeBuilder"/> state to be valid, you must
/// also call <see cref="CloseElement"/> immediately after appending the
/// new element's child frames.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="elementName">A value representing the type of the element.</param>
public void OpenElement(int sequence, string elementName)
{
// We are entering a new scope, since we track the "duplicate attributes" per
// element/component we might need to clean them up now.
if (_hasSeenAddMultipleAttributes)
{
var indexOfLastElementOrComponent = _openElementIndices.Peek();
ProcessDuplicateAttributes(first: indexOfLastElementOrComponent + 1);
}
_openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.Element(sequence, elementName));
}
/// <summary>
/// Marks a previously appended element frame as closed. Calls to this method
/// must be balanced with calls to <see cref="OpenElement(int, string)"/>.
/// </summary>
public void CloseElement()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
// We might be closing an element with only attributes, run the duplicate cleanup pass
// if necessary.
if (_hasSeenAddMultipleAttributes)
{
ProcessDuplicateAttributes(first: indexOfEntryBeingClosed + 1);
}
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
entry = entry.WithElementSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
/// <summary>
/// Appends a frame representing markup content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="markupContent">Content for the new markup frame.</param>
public void AddMarkupContent(int sequence, string markupContent)
=> Append(RenderTreeFrame.Markup(sequence, markupContent ?? string.Empty));
/// <summary>
/// Appends a frame representing text content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="textContent">Content for the new text frame.</param>
public void AddContent(int sequence, string textContent)
=> Append(RenderTreeFrame.Text(sequence, textContent ?? string.Empty));
/// <summary>
/// Appends frames representing an arbitrary fragment of content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="fragment">Content to append.</param>
public void AddContent(int sequence, RenderFragment fragment)
{
if (fragment != null)
{
// We surround the fragment with a region delimiter to indicate that the
// sequence numbers inside the fragment are unrelated to the sequence numbers
// outside it. If we didn't do this, the diffing logic might produce inefficient
// diffs depending on how the sequence numbers compared.
OpenRegion(sequence);
fragment(this);
CloseRegion();
}
}
/// <summary>
/// Appends frames representing an arbitrary fragment of content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="fragment">Content to append.</param>
/// <param name="value">The value used by <paramref name="fragment"/>.</param>
public void AddContent<TValue>(int sequence, RenderFragment<TValue> fragment, TValue value)
{
if (fragment != null)
{
AddContent(sequence, fragment(value));
}
}
/// <summary>
/// Appends a frame representing markup content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="markupContent">Content for the new markup frame.</param>
public void AddContent(int sequence, MarkupString markupContent)
=> AddMarkupContent(sequence, markupContent.Value);
/// <summary>
/// Appends a frame representing text content.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="textContent">Content for the new text frame.</param>
public void AddContent(int sequence, object textContent)
=> AddContent(sequence, textContent?.ToString());
/// <summary>
/// <para>
/// Appends a frame representing a bool-valued attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element. If the value is <c>false</c> and the
/// current element is not a component, the frame will be omitted.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(int sequence, string name, bool value)
{
AssertCanAddAttribute();
if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
Append(RenderTreeFrame.Attribute(sequence, name, value ? BoxedTrue : BoxedFalse));
}
else if (value)
{
// Don't add 'false' attributes for elements. We want booleans to map to the presence
// or absence of an attribute, and false => "False" which isn't falsy in js.
Append(RenderTreeFrame.Attribute(sequence, name, BoxedTrue));
}
else
{
TrackAttributeName(name);
}
}
/// <summary>
/// <para>
/// Appends a frame representing a string-valued attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element. If the value is <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(int sequence, string name, string value)
{
AssertCanAddAttribute();
if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
Append(RenderTreeFrame.Attribute(sequence, name, value));
}
else
{
TrackAttributeName(name);
}
}
/// <summary>
/// <para>
/// Appends a frame representing a delegate-valued attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element. If the value is <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(int sequence, string name, MulticastDelegate value)
{
AssertCanAddAttribute();
if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
Append(RenderTreeFrame.Attribute(sequence, name, value));
}
else
{
TrackAttributeName(name);
}
}
/// <summary>
/// <para>
/// Appends a frame representing an <see cref="EventCallback"/> attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element. If the value is <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
/// <remarks>
/// This method is provided for infrastructure purposes, and is used to support generated code
/// that uses <see cref="EventCallbackFactory"/>.
/// </remarks>
public void AddAttribute(int sequence, string name, EventCallback value)
{
AssertCanAddAttribute();
if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
// Since this is a component, we need to preserve the type of the EventCallabck, so we have
// to box.
Append(RenderTreeFrame.Attribute(sequence, name, (object)value));
}
else if (value.RequiresExplicitReceiver)
{
// If we need to preserve the receiver, we just box the EventCallback
// so we can get it out on the other side.
Append(RenderTreeFrame.Attribute(sequence, name, (object)value));
}
else if (value.HasDelegate)
{
// In the common case the receiver is also the delegate's target, so we
// just need to retain the delegate. This allows us to avoid an allocation.
Append(RenderTreeFrame.Attribute(sequence, name, value.Delegate));
}
else
{
// Track the attribute name if needed since we elided the frame.
TrackAttributeName(name);
}
}
/// <summary>
/// <para>
/// Appends a frame representing an <see cref="EventCallback"/> attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element. If the value is <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
/// <remarks>
/// This method is provided for infrastructure purposes, and is used to support generated code
/// that uses <see cref="EventCallbackFactory"/>.
/// </remarks>
public void AddAttribute<TArgument>(int sequence, string name, EventCallback<TArgument> value)
{
AssertCanAddAttribute();
if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
// Since this is a component, we need to preserve the type of the EventCallback, so we have
// to box.
Append(RenderTreeFrame.Attribute(sequence, name, (object)value));
}
else if (value.RequiresExplicitReceiver)
{
// If we need to preserve the receiver - we convert this to an untyped EventCallback. We don't
// need to preserve the type of an EventCallback<T> when it's invoked from the DOM.
Append(RenderTreeFrame.Attribute(sequence, name, (object)value.AsUntyped()));
}
else if (value.HasDelegate)
{
// In the common case the receiver is also the delegate's target, so we
// just need to retain the delegate. This allows us to avoid an allocation.
Append(RenderTreeFrame.Attribute(sequence, name, value.Delegate));
}
else
{
// Track the attribute name if needed since we elided the frame.
TrackAttributeName(name);
}
}
/// <summary>
/// Appends a frame representing a string-valued attribute.
/// The attribute is associated with the most recently added element. If the value is <c>null</c>, or
/// the <see cref="System.Boolean" /> value <c>false</c> and the current element is not a component, the
/// frame will be omitted.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(int sequence, string name, object value)
{
// This looks a bit daunting because we need to handle the boxed/object version of all of the
// types that AddAttribute special cases.
if (_lastNonAttributeFrameType == RenderTreeFrameType.Element)
{
if (value == null)
{
// Treat 'null' attribute values for elements as a conditional attribute.
TrackAttributeName(name);
}
else if (value is bool boolValue)
{
if (boolValue)
{
Append(RenderTreeFrame.Attribute(sequence, name, BoxedTrue));
}
else
{
// Don't add anything for false bool value.
TrackAttributeName(name);
}
}
else if (value is IEventCallback callbackValue)
{
if (callbackValue.HasDelegate)
{
Append(RenderTreeFrame.Attribute(sequence, name, callbackValue.UnpackForRenderTree()));
}
else
{
TrackAttributeName(name);
}
}
else if (value is MulticastDelegate)
{
Append(RenderTreeFrame.Attribute(sequence, name, value));
}
else
{
// The value is either a string, or should be treated as a string.
Append(RenderTreeFrame.Attribute(sequence, name, value.ToString()));
}
}
else if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
// If this is a component, we always want to preserve the original type.
Append(RenderTreeFrame.Attribute(sequence, name, value));
}
else
{
// This is going to throw. Calling it just to get a consistent exception message.
AssertCanAddAttribute();
}
}
/// <summary>
/// <para>
/// Appends a frame representing an attribute.
/// </para>
/// <para>
/// The attribute is associated with the most recently added element.
/// </para>
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="frame">A <see cref="RenderTreeFrame"/> holding the name and value of the attribute.</param>
public void AddAttribute(int sequence, in RenderTreeFrame frame)
{
if (frame.FrameType != RenderTreeFrameType.Attribute)
{
throw new ArgumentException($"The {nameof(frame.FrameType)} must be {RenderTreeFrameType.Attribute}.");
}
AssertCanAddAttribute();
Append(frame.WithAttributeSequence(sequence));
}
/// <summary>
/// Adds frames representing multiple attributes with the same sequence number.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="attributes">A collection of key-value pairs representing attributes.</param>
public void AddMultipleAttributes(int sequence, IEnumerable<KeyValuePair<string, object>> attributes)
{
// Calling this up-front just to make sure we validate before mutating anything.
AssertCanAddAttribute();
if (attributes != null)
{
_hasSeenAddMultipleAttributes = true;
foreach (var attribute in attributes)
{
// This will call the AddAttribute(int, string, object) overload.
//
// This is fine because we try to make the object overload behave identically
// to the others.
AddAttribute(sequence, attribute.Key, attribute.Value);
}
}
}
/// <summary>
/// <para>
/// Indicates that the preceding attribute represents an event handler
/// whose execution updates the attribute with name <paramref name="updatesAttributeName"/>.
/// </para>
/// <para>
/// This information is used by the rendering system to determine whether
/// to accept a value update for the other attribute when receiving a
/// call to the event handler.
/// </para>
/// </summary>
/// <param name="updatesAttributeName">The name of another attribute whose value can be updated when the event handler is executed.</param>
public void SetUpdatesAttributeName(string updatesAttributeName)
{
if (_entries.Count == 0)
{
throw new InvalidOperationException("No preceding attribute frame exists.");
}
ref var prevFrame = ref _entries.Buffer[_entries.Count - 1];
if (prevFrame.FrameType != RenderTreeFrameType.Attribute)
{
throw new InvalidOperationException($"Incorrect frame type: '{prevFrame.FrameType}'");
}
prevFrame = prevFrame.WithAttributeEventUpdatesAttributeName(updatesAttributeName);
}
/// <summary>
/// Appends a frame representing a child component.
/// </summary>
/// <typeparam name="TComponent">The type of the child component.</typeparam>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
public void OpenComponent<TComponent>(int sequence) where TComponent : IComponent
=> OpenComponentUnchecked(sequence, typeof(TComponent));
/// <summary>
/// Appends a frame representing a child component.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="componentType">The type of the child component.</param>
public void OpenComponent(int sequence, Type componentType)
{
if (!typeof(IComponent).IsAssignableFrom(componentType))
{
throw new ArgumentException($"The component type must implement {typeof(IComponent).FullName}.");
}
OpenComponentUnchecked(sequence, componentType);
}
/// <summary>
/// Assigns the specified key value to the current element or component.
/// </summary>
/// <param name="value">The value for the key.</param>
public void SetKey(object value)
{
if (value == null)
{
// Null is equivalent to not having set a key, which is valuable because Razor syntax doesn't have an
// easy way to have conditional directive attributes
return;
}
var parentFrameIndex = GetCurrentParentFrameIndex();
if (!parentFrameIndex.HasValue)
{
throw new InvalidOperationException("Cannot set a key outside the scope of a component or element.");
}
var parentFrameIndexValue = parentFrameIndex.Value;
ref var parentFrame = ref _entries.Buffer[parentFrameIndexValue];
switch (parentFrame.FrameType)
{
case RenderTreeFrameType.Element:
parentFrame = parentFrame.WithElementKey(value); // It's a ref var, so this writes to the array
break;
case RenderTreeFrameType.Component:
parentFrame = parentFrame.WithComponentKey(value); // It's a ref var, so this writes to the array
break;
default:
throw new InvalidOperationException($"Cannot set a key on a frame of type {parentFrame.FrameType}.");
}
}
private void OpenComponentUnchecked(int sequence, Type componentType)
{
// We are entering a new scope, since we track the "duplicate attributes" per
// element/component we might need to clean them up now.
if (_hasSeenAddMultipleAttributes)
{
var indexOfLastElementOrComponent = _openElementIndices.Peek();
ProcessDuplicateAttributes(first: indexOfLastElementOrComponent + 1);
}
_openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.ChildComponent(sequence, componentType));
}
/// <summary>
/// Marks a previously appended component frame as closed. Calls to this method
/// must be balanced with calls to <see cref="OpenComponent{TComponent}"/>.
/// </summary>
public void CloseComponent()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
// We might be closing a component with only attributes. Run the attribute cleanup pass
// if necessary.
if (_hasSeenAddMultipleAttributes)
{
ProcessDuplicateAttributes(first: indexOfEntryBeingClosed + 1);
}
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
entry = entry.WithComponentSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
/// <summary>
/// Appends a frame representing an instruction to capture a reference to the parent element.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="elementReferenceCaptureAction">An action to be invoked whenever the reference value changes.</param>
public void AddElementReferenceCapture(int sequence, Action<ElementReference> elementReferenceCaptureAction)
{
if (GetCurrentParentFrameType() != RenderTreeFrameType.Element)
{
throw new InvalidOperationException($"Element reference captures may only be added as children of frames of type {RenderTreeFrameType.Element}");
}
Append(RenderTreeFrame.ElementReferenceCapture(sequence, elementReferenceCaptureAction));
}
/// <summary>
/// Appends a frame representing an instruction to capture a reference to the parent component.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="componentReferenceCaptureAction">An action to be invoked whenever the reference value changes.</param>
public void AddComponentReferenceCapture(int sequence, Action<object> componentReferenceCaptureAction)
{
var parentFrameIndex = GetCurrentParentFrameIndex();
if (!parentFrameIndex.HasValue)
{
throw new InvalidOperationException(ComponentReferenceCaptureInvalidParentMessage);
}
var parentFrameIndexValue = parentFrameIndex.Value;
if (_entries.Buffer[parentFrameIndexValue].FrameType != RenderTreeFrameType.Component)
{
throw new InvalidOperationException(ComponentReferenceCaptureInvalidParentMessage);
}
Append(RenderTreeFrame.ComponentReferenceCapture(sequence, componentReferenceCaptureAction, parentFrameIndexValue));
}
/// <summary>
/// Appends a frame representing a region of frames.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
public void OpenRegion(int sequence)
{
_openElementIndices.Push(_entries.Count);
Append(RenderTreeFrame.Region(sequence));
}
/// <summary>
/// Marks a previously appended region frame as closed. Calls to this method
/// must be balanced with calls to <see cref="OpenRegion(int)"/>.
/// </summary>
public void CloseRegion()
{
var indexOfEntryBeingClosed = _openElementIndices.Pop();
ref var entry = ref _entries.Buffer[indexOfEntryBeingClosed];
entry = entry.WithRegionSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
private void AssertCanAddAttribute()
{
if (_lastNonAttributeFrameType != RenderTreeFrameType.Element
&& _lastNonAttributeFrameType != RenderTreeFrameType.Component)
{
throw new InvalidOperationException($"Attributes may only be added immediately after frames of type {RenderTreeFrameType.Element} or {RenderTreeFrameType.Component}");
}
}
private int? GetCurrentParentFrameIndex()
=> _openElementIndices.Count == 0 ? (int?)null : _openElementIndices.Peek();
private RenderTreeFrameType? GetCurrentParentFrameType()
{
var parentIndex = GetCurrentParentFrameIndex();
return parentIndex.HasValue
? _entries.Buffer[parentIndex.Value].FrameType
: (RenderTreeFrameType?)null;
}
/// <summary>
/// Clears the builder.
/// </summary>
public void Clear()
{
_entries.Clear();
_openElementIndices.Clear();
_lastNonAttributeFrameType = null;
_hasSeenAddMultipleAttributes = false;
_seenAttributeNames?.Clear();
}
// internal because this should only be used during the post-event tree patching logic
// It's expensive because it involves copying all the subsequent memory in the array
internal void InsertAttributeExpensive(int insertAtIndex, int sequence, string attributeName, object attributeValue)
{
// Replicate the same attribute omission logic as used elsewhere
if ((attributeValue == null) || (attributeValue is bool boolValue && !boolValue))
{
return;
}
_entries.InsertExpensive(insertAtIndex, RenderTreeFrame.Attribute(sequence, attributeName, attributeValue));
}
/// <summary>
/// Returns the <see cref="RenderTreeFrame"/> values that have been appended.
/// </summary>
/// <returns>An array range of <see cref="RenderTreeFrame"/> values.</returns>
public ArrayRange<RenderTreeFrame> GetFrames() =>
_entries.ToRange();
private void Append(in RenderTreeFrame frame)
{
var frameType = frame.FrameType;
_entries.Append(frame);
if (frameType != RenderTreeFrameType.Attribute)
{
_lastNonAttributeFrameType = frame.FrameType;
}
}
// Internal for testing
internal void ProcessDuplicateAttributes(int first)
{
Debug.Assert(_hasSeenAddMultipleAttributes);
// When AddMultipleAttributes method has been called, we need to postprocess attributes while closing
// the element/component. However, we also don't know the end index we should look at because it
// will contain nested content.
var buffer = _entries.Buffer;
var last = _entries.Count - 1;
for (var i = first; i <= last; i++)
{
if (buffer[i].FrameType != RenderTreeFrameType.Attribute)
{
last = i - 1;
break;
}
}
// Now that we've found the last attribute, we can iterate backwards and process duplicates.
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
for (var i = last; i >= first; i--)
{
ref var frame = ref buffer[i];
Debug.Assert(frame.FrameType == RenderTreeFrameType.Attribute, $"Frame type is {frame.FrameType} at {i}");
if (!seenAttributeNames.TryGetValue(frame.AttributeName, out var index))
{
// This is the first time seeing this attribute name. Add to the dictionary and move on.
seenAttributeNames.Add(frame.AttributeName, i);
}
else if (index < i)
{
// This attribute is overriding a "silent frame" where we didn't create a frame for an AddAttribute call.
// This is the case for a null event handler, or bool false value.
//
// We need to update our tracking, in case the attribute appeared 3 or more times.
seenAttributeNames[frame.AttributeName] = i;
}
else if (index > i)
{
// This attribute has been overridden. For now, blank out its name to *mark* it. We'll do a pass
// later to wipe it out.
frame = default;
}
else
{
// OK so index == i. How is that possible? Well it's possible for a "silent frame" immediately
// followed by setting the same attribute. Think of it this way, when we create a "silent frame"
// we have to track that attribute name with *some* index.
//
// The only index value we can safely use is _entries.Count (next available). This is fine because
// we never use these indexes to look stuff up, only for comparison.
//
// That gets you here, and there's no action to take.
}
}
// This is the pass where we cleanup attributes that have been wiped out.
//
// We copy the entries we're keeping into the earlier parts of the list (preserving order).
//
// Note that we iterate to the end of the list here, there might be additional frames after the attributes
// (ref) or content) that need to move to the left.
var offset = first;
for (var i = first; i < _entries.Count; i++)
{
ref var frame = ref buffer[i];
if (frame.FrameType != RenderTreeFrameType.None)
{
buffer[offset++] = frame;
}
}
// Clean up now unused space at the end of the list.
var residue = _entries.Count - offset;
for (var i = 0; i < residue; i++)
{
_entries.RemoveLast();
}
seenAttributeNames.Clear();
_hasSeenAddMultipleAttributes = false;
}
// Internal for testing
internal void TrackAttributeName(string name)
{
if (!_hasSeenAddMultipleAttributes)
{
return;
}
var seenAttributeNames = (_seenAttributeNames ??= new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase));
seenAttributeNames[name] = _entries.Count; // See comment in ProcessAttributes for why this is OK.
}
void IDisposable.Dispose()
{
_entries.Dispose();
}
}
}