For @key, support both strict (default) and loose modes

This commit is contained in:
Steve Sanderson 2019-07-15 12:38:00 +01:00
parent 20115f3c84
commit 5141ee930e
11 changed files with 308 additions and 49 deletions

View File

@ -0,0 +1,17 @@
// 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;
namespace Microsoft.AspNetCore.Components.RenderTree
{
/// <summary>
/// Describes flags associated with a <see cref="RenderTreeFrame"/> whose <see cref="RenderTreeFrame.FrameType"/>
/// equals <see cref="RenderTreeFrameType.Component"/>.
/// </summary>
[Flags]
public enum ComponentFlags: short
{
LooseKey = 1,
}
}

View File

@ -0,0 +1,17 @@
// 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;
namespace Microsoft.AspNetCore.Components.RenderTree
{
/// <summary>
/// Describes flags associated with a <see cref="RenderTreeFrame"/> whose <see cref="RenderTreeFrame.FrameType"/>
/// equals <see cref="RenderTreeFrameType.Element"/>.
/// </summary>
[Flags]
public enum ElementFlags: short
{
LooseKey = 1,
}
}

View File

@ -520,10 +520,25 @@ namespace Microsoft.AspNetCore.Components.RenderTree
/// </summary>
/// <param name="value">The value for the key.</param>
public void SetKey(object value)
=> SetKey(value, looseKey: false);
/// <summary>
/// Assigns the specified key value to the current element or component.
/// </summary>
/// <param name="value">The value for the key.</param>
/// <param name="looseKey">If true, null values and duplicates will be ignored. If false, null values and duplicates will be treated as an error.</param>
public void SetKey(object value, bool looseKey)
{
if (value == null)
{
throw new ArgumentNullException(nameof(value));
if (looseKey)
{
return;
}
else
{
throw new ArgumentNullException(nameof(value));
}
}
var parentFrameIndex = GetCurrentParentFrameIndex();
@ -537,10 +552,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree
switch (parentFrame.FrameType)
{
case RenderTreeFrameType.Element:
parentFrame = parentFrame.WithElementKey(value); // It's a ref var, so this writes to the array
parentFrame = parentFrame.WithElementKey(value, looseKey); // 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
parentFrame = parentFrame.WithComponentKey(value, looseKey); // 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}.");

View File

@ -50,6 +50,8 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// if you plan to refactor this, be sure to benchmark the old and new versions
// on Mono WebAssembly.
var origOldStartIndex = oldStartIndex;
var origNewStartIndex = newStartIndex;
var hasMoreOld = oldEndIndexExcl > oldStartIndex;
var hasMoreNew = newEndIndexExcl > newStartIndex;
var prevOldSeq = -1;
@ -108,7 +110,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// Keys don't match
if (keyedItemInfos == null)
{
keyedItemInfos = BuildKeyToInfoLookup(diffContext, oldStartIndex, oldEndIndexExcl, newStartIndex, newEndIndexExcl);
keyedItemInfos = BuildKeyToInfoLookup(diffContext, origOldStartIndex, oldEndIndexExcl, origNewStartIndex, newEndIndexExcl);
}
var oldKeyItemInfo = oldKey != null ? keyedItemInfos[oldKey] : new KeyedItemInfo(-1, -1, false);
@ -304,7 +306,15 @@ namespace Microsoft.AspNetCore.Components.RenderTree
var key = KeyValue(ref frame);
if (key != null)
{
result[key] = new KeyedItemInfo(oldStartIndex, -1, isUnique: !result.ContainsKey(key));
var isUnique = !result.ContainsKey(key);
if (isUnique || KeyIsLoose(ref frame))
{
result[key] = new KeyedItemInfo(oldStartIndex, -1, isUnique);
}
else
{
ThrowExceptionForDuplicateKey(key);
}
}
oldStartIndex = NextSiblingIndex(frame, oldStartIndex);
@ -316,9 +326,22 @@ namespace Microsoft.AspNetCore.Components.RenderTree
var key = KeyValue(ref frame);
if (key != null)
{
result[key] = result.TryGetValue(key, out var existingEntry)
? new KeyedItemInfo(existingEntry.OldIndex, newStartIndex, isUnique: existingEntry.NewIndex < 0)
: new KeyedItemInfo(-1, newStartIndex, isUnique: true);
if (!result.TryGetValue(key, out var existingEntry))
{
result[key] = new KeyedItemInfo(-1, newStartIndex, isUnique: true);
}
else
{
var isUnique = existingEntry.NewIndex < 0;
if (isUnique || KeyIsLoose(ref frame))
{
result[key] = new KeyedItemInfo(existingEntry.OldIndex, newStartIndex, isUnique);
}
else
{
ThrowExceptionForDuplicateKey(key);
}
}
}
newStartIndex = NextSiblingIndex(frame, newStartIndex);
@ -327,6 +350,11 @@ namespace Microsoft.AspNetCore.Components.RenderTree
return result;
}
private static void ThrowExceptionForDuplicateKey(object key)
{
throw new InvalidOperationException($"More than one sibling has the same key value, '{key}'. Key values must be unique, or 'loose' key mode must be used.");
}
private static object KeyValue(ref RenderTreeFrame frame)
{
switch (frame.FrameType)
@ -340,6 +368,19 @@ namespace Microsoft.AspNetCore.Components.RenderTree
}
}
private static bool KeyIsLoose(ref RenderTreeFrame frame)
{
switch (frame.FrameType)
{
case RenderTreeFrameType.Element:
return frame.ElementFlags.HasFlag(ElementFlags.LooseKey);
case RenderTreeFrameType.Component:
return frame.ComponentFlags.HasFlag(ComponentFlags.LooseKey);
default:
return false;
}
}
// Handles the diff for attribute nodes only - this invariant is enforced by the caller.
//
// The diff for attributes is different because we allow attributes to appear in any order.

View File

@ -53,6 +53,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// RenderTreeFrameType.Element
// --------------------------------------------------------------------------------
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>
/// gets the flags associated with the frame.
/// </summary>
[FieldOffset(6)] public readonly ElementFlags ElementFlags;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Element"/>
/// gets the number of frames in the subtree for which this frame is the root.
@ -116,6 +122,12 @@ namespace Microsoft.AspNetCore.Components.RenderTree
// RenderTreeFrameType.Component
// --------------------------------------------------------------------------------
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>
/// gets the flags associated with the frame.
/// </summary>
[FieldOffset(6)] public readonly ComponentFlags ComponentFlags;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>
/// gets the number of frames in the subtree for which this frame is the root.
@ -213,22 +225,24 @@ namespace Microsoft.AspNetCore.Components.RenderTree
[FieldOffset(16)] public readonly string MarkupContent;
// Element constructor
private RenderTreeFrame(int sequence, int elementSubtreeLength, string elementName, object elementKey)
private RenderTreeFrame(int sequence, ElementFlags elementFlags, int elementSubtreeLength, string elementName, object elementKey)
: this()
{
Sequence = sequence;
FrameType = RenderTreeFrameType.Element;
ElementFlags = elementFlags;
ElementSubtreeLength = elementSubtreeLength;
ElementName = elementName;
ElementKey = elementKey;
}
// Component constructor
private RenderTreeFrame(int sequence, int componentSubtreeLength, Type componentType, ComponentState componentState, object componentKey)
private RenderTreeFrame(int sequence, ComponentFlags componentFlags, int componentSubtreeLength, Type componentType, ComponentState componentState, object componentKey)
: this()
{
Sequence = sequence;
FrameType = RenderTreeFrameType.Component;
ComponentFlags = componentFlags;
ComponentSubtreeLength = componentSubtreeLength;
ComponentType = componentType;
ComponentKey = componentKey;
@ -299,7 +313,7 @@ namespace Microsoft.AspNetCore.Components.RenderTree
}
internal static RenderTreeFrame Element(int sequence, string elementName)
=> new RenderTreeFrame(sequence, elementSubtreeLength: 0, elementName, null);
=> new RenderTreeFrame(sequence, elementFlags: default, elementSubtreeLength: 0, elementName, null);
internal static RenderTreeFrame Text(int sequence, string textContent)
=> new RenderTreeFrame(sequence, isMarkup: false, textOrMarkup: textContent);
@ -311,10 +325,10 @@ namespace Microsoft.AspNetCore.Components.RenderTree
=> new RenderTreeFrame(sequence, attributeName: name, attributeValue: value, attributeEventHandlerId: 0, attributeEventUpdatesAttributeName: null);
internal static RenderTreeFrame ChildComponent(int sequence, Type componentType)
=> new RenderTreeFrame(sequence, componentSubtreeLength: 0, componentType, null, null);
=> new RenderTreeFrame(sequence, componentFlags: default, componentSubtreeLength: 0, componentType, null, null);
internal static RenderTreeFrame PlaceholderChildComponentWithSubtreeLength(int subtreeLength)
=> new RenderTreeFrame(0, componentSubtreeLength: subtreeLength, typeof(IComponent), null, null);
=> new RenderTreeFrame(0, componentFlags: default, componentSubtreeLength: subtreeLength, typeof(IComponent), null, null);
internal static RenderTreeFrame Region(int sequence)
=> new RenderTreeFrame(sequence, regionSubtreeLength: 0);
@ -326,16 +340,16 @@ namespace Microsoft.AspNetCore.Components.RenderTree
=> new RenderTreeFrame(sequence, componentReferenceCaptureAction: componentReferenceCaptureAction, parentFrameIndex: parentFrameIndex);
internal RenderTreeFrame WithElementSubtreeLength(int elementSubtreeLength)
=> new RenderTreeFrame(Sequence, elementSubtreeLength: elementSubtreeLength, ElementName, ElementKey);
=> new RenderTreeFrame(Sequence, elementFlags: ElementFlags, elementSubtreeLength: elementSubtreeLength, ElementName, ElementKey);
internal RenderTreeFrame WithComponentSubtreeLength(int componentSubtreeLength)
=> new RenderTreeFrame(Sequence, componentSubtreeLength: componentSubtreeLength, ComponentType, ComponentState, ComponentKey);
=> new RenderTreeFrame(Sequence, componentFlags: ComponentFlags, componentSubtreeLength: componentSubtreeLength, ComponentType, ComponentState, ComponentKey);
internal RenderTreeFrame WithAttributeSequence(int sequence)
=> new RenderTreeFrame(sequence, attributeName: AttributeName, AttributeValue, AttributeEventHandlerId, AttributeEventUpdatesAttributeName);
internal RenderTreeFrame WithComponent(ComponentState componentState)
=> new RenderTreeFrame(Sequence, componentSubtreeLength: ComponentSubtreeLength, ComponentType, componentState, ComponentKey);
=> new RenderTreeFrame(Sequence, componentFlags: ComponentFlags, componentSubtreeLength: ComponentSubtreeLength, ComponentType, componentState, ComponentKey);
internal RenderTreeFrame WithAttributeEventHandlerId(int eventHandlerId)
=> new RenderTreeFrame(Sequence, attributeName: AttributeName, AttributeValue, eventHandlerId, AttributeEventUpdatesAttributeName);
@ -352,11 +366,17 @@ namespace Microsoft.AspNetCore.Components.RenderTree
internal RenderTreeFrame WithElementReferenceCaptureId(string elementReferenceCaptureId)
=> new RenderTreeFrame(Sequence, elementReferenceCaptureAction: ElementReferenceCaptureAction, elementReferenceCaptureId);
internal RenderTreeFrame WithElementKey(object elementKey)
=> new RenderTreeFrame(Sequence, elementSubtreeLength: ElementSubtreeLength, ElementName, elementKey);
internal RenderTreeFrame WithElementKey(object elementKey, bool looseKey)
{
var flags = looseKey ? (ElementFlags | ElementFlags.LooseKey) : ElementFlags;
return new RenderTreeFrame(Sequence, elementFlags: flags, elementSubtreeLength: ElementSubtreeLength, ElementName, elementKey);
}
internal RenderTreeFrame WithComponentKey(object componentKey)
=> new RenderTreeFrame(Sequence, componentSubtreeLength: ComponentSubtreeLength, ComponentType, ComponentState, componentKey);
internal RenderTreeFrame WithComponentKey(object componentKey, bool looseKey)
{
var flags = looseKey ? (ComponentFlags | ComponentFlags.LooseKey) : ComponentFlags;
return new RenderTreeFrame(Sequence, componentFlags: flags, componentSubtreeLength: ComponentSubtreeLength, ComponentType, ComponentState, componentKey);
}
/// <inheritdoc />
// Just to be nice for debugging and unit tests.

View File

@ -1475,8 +1475,10 @@ namespace Microsoft.AspNetCore.Components.Test
frame => AssertFrame.Element(frame, "elem", 1, 0));
}
[Fact]
public void CanAddKeyToElement()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanAddKeyToElement(bool looseKey)
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
@ -1485,7 +1487,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
builder.OpenElement(0, "elem");
builder.AddAttribute(1, "attribute before", "before value");
builder.SetKey(keyValue);
builder.SetKey(keyValue, looseKey);
builder.AddAttribute(2, "attribute after", "after value");
builder.CloseElement();
@ -1496,13 +1498,16 @@ namespace Microsoft.AspNetCore.Components.Test
{
AssertFrame.Element(frame, "elem", 3, 0);
Assert.Same(keyValue, frame.ElementKey);
Assert.Equal(looseKey, frame.ElementFlags.HasFlag(ElementFlags.LooseKey));
},
frame => AssertFrame.Attribute(frame, "attribute before", "before value", 1),
frame => AssertFrame.Attribute(frame, "attribute after", "after value", 2));
}
[Fact]
public void CanAddKeyToComponent()
[Theory]
[InlineData(true)]
[InlineData(false)]
public void CanAddKeyToComponent(bool looseKey)
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
@ -1511,7 +1516,7 @@ namespace Microsoft.AspNetCore.Components.Test
// Act
builder.OpenComponent<TestComponent>(0);
builder.AddAttribute(1, "param before", 123);
builder.SetKey(keyValue);
builder.SetKey(keyValue, looseKey);
builder.AddAttribute(2, "param after", 456);
builder.CloseComponent();
@ -1522,6 +1527,7 @@ namespace Microsoft.AspNetCore.Components.Test
{
AssertFrame.Component<TestComponent>(frame, 3, 0);
Assert.Same(keyValue, frame.ComponentKey);
Assert.Equal(looseKey, frame.ComponentFlags.HasFlag(ComponentFlags.LooseKey));
},
frame => AssertFrame.Attribute(frame, "param before", 123, 1),
frame => AssertFrame.Attribute(frame, "param after", 456, 2));
@ -1576,6 +1582,48 @@ namespace Microsoft.AspNetCore.Components.Test
Assert.Equal("value", ex.ParamName);
}
[Fact]
public void IgnoresNullElementKeyIfLoose()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.OpenElement(0, "elem");
builder.SetKey(null, looseKey: true);
builder.CloseElement();
// Assert
Assert.Collection(
builder.GetFrames().AsEnumerable(),
frame =>
{
AssertFrame.Element(frame, "elem", 1, 0);
Assert.Null(frame.ElementKey);
});
}
[Fact]
public void IgnoresNullComponentKeyIfLoose()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.OpenComponent<TestComponent>(0);
builder.SetKey(null, looseKey: true);
builder.CloseComponent();
// Assert
Assert.Collection(
builder.GetFrames().AsEnumerable(),
frame =>
{
AssertFrame.Component<TestComponent>(frame, 1, 0);
Assert.Null(frame.ComponentKey);
});
}
[Fact]
public void ProcessDuplicateAttributes_DoesNotRemoveDuplicatesWithoutAddMultipleAttributes()
{

View File

@ -315,7 +315,41 @@ namespace Microsoft.AspNetCore.Components.Test
}
[Fact]
public void HandlesClashingKeys_FirstUsage()
public void HandlesClashingKeysInOldTree_Strict()
{
// Arrange
AddWithKey(oldTree, "key1", "attrib1a");
AddWithKey(oldTree, "key2", "attrib2");
AddWithKey(oldTree, "key1", "attrib3");
AddWithKey(newTree, "key1", "attrib1b");
AddWithKey(newTree, "key2", "attrib2");
AddWithKey(newTree, "key3", "attrib3");
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => GetSingleUpdatedComponent());
Assert.Equal("More than one sibling has the same key value, 'key1'. Key values must be unique, or 'loose' key mode must be used.", ex.Message);
}
[Fact]
public void HandlesClashingKeysInNewTree_Strict()
{
// Arrange
AddWithKey(oldTree, "key1", "attrib1a");
AddWithKey(oldTree, "key2", "attrib2");
AddWithKey(oldTree, "key3", "attrib3");
AddWithKey(newTree, "key1", "attrib1b");
AddWithKey(newTree, "key2", "attrib2");
AddWithKey(newTree, "key1", "attrib3");
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => GetSingleUpdatedComponent());
Assert.Equal("More than one sibling has the same key value, 'key1'. Key values must be unique, or 'loose' key mode must be used.", ex.Message);
}
[Fact]
public void HandlesClashingKeys_Loose_FirstUsage()
{
// This scenario is problematic for the algorithm if it uses a "first key
// usage wins" policy for duplicate keys. It would not end up with attrib1b
@ -326,15 +360,20 @@ namespace Microsoft.AspNetCore.Components.Test
// dictionary match" policy, we don't preserve any of the key1 items, and
// the diff is valid.
// Since we only pass the "loose" flag for the key that would otherwise
// trigger an exception due to a clash, this shows that "loose" only matters
// in that case. It would be fine to pass loose:true for all the others too,
// as would normally be done in .razor, but it makes no difference.
// Arrange
AddWithKey(oldTree, "key3", "attrib3");
AddWithKey(oldTree, "key1", "attrib1a");
AddWithKey(oldTree, "key1", "attrib1a");
AddWithKey(oldTree, "key1", "attrib1a", looseKey: true);
AddWithKey(oldTree, "key2", "attrib2");
AddWithKey(newTree, "key1", "attrib1a");
AddWithKey(newTree, "key2", "attrib2");
AddWithKey(newTree, "key1", "attrib1b");
AddWithKey(newTree, "key1", "attrib1b", looseKey: true);
// Act
var (result, referenceFrames) = GetSingleUpdatedComponent();
@ -362,7 +401,7 @@ namespace Microsoft.AspNetCore.Components.Test
}
[Fact]
public void HandlesClashingKeys_LastUsage()
public void HandlesClashingKeys_Loose_LastUsage()
{
// This scenario is problematic for the algorithm if it uses a "last key
// usage wins" policy for duplicate keys. It would not end up with attrib1b
@ -373,14 +412,19 @@ namespace Microsoft.AspNetCore.Components.Test
// dictionary match" policy, we don't preserve any of the key1 items, and
// the diff is valid.
// Since we only pass the "loose" flag for the key that would otherwise
// trigger an exception due to a clash, this shows that "loose" only matters
// in that case. It would be fine to pass loose:true for all the others too,
// as would normally be done in .razor, but it makes no difference.
// Arrange
AddWithKey(oldTree, "key1", "attrib1a");
AddWithKey(oldTree, "key2", "attrib2");
AddWithKey(oldTree, "key1", "attrib1b");
AddWithKey(oldTree, "key1", "attrib1b", looseKey: true);
AddWithKey(newTree, "key2", "attrib2");
AddWithKey(newTree, "key1", "attrib1b");
AddWithKey(newTree, "key1", "attrib1a");
AddWithKey(newTree, "key1", "attrib1a", looseKey: true);
// Act
var (result, referenceFrames) = GetSingleUpdatedComponent();
@ -2193,10 +2237,19 @@ namespace Microsoft.AspNetCore.Components.Test
.Select(x => (T)x.Component)
.ToList();
private static void AddWithKey(RenderTreeBuilder builder, object key, string attributeValue = null)
private static void AddWithKey(RenderTreeBuilder builder, object key, string attributeValue = null, bool looseKey = false)
{
builder.OpenElement(0, "el");
builder.SetKey(key);
if (looseKey)
{
builder.SetKey(key, looseKey: true);
}
else
{
// Using this overload here (not explicitly passing any flag) to show that the default is strict
builder.SetKey(key);
}
if (attributeValue != null)
{

File diff suppressed because one or more lines are too long

View File

@ -206,6 +206,25 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
});
}
[Fact]
public void CanDuplicateKeysInLooseMode()
{
PerformTest(
useLooseKeys: true,
before: new[]
{
new Node("orig1", "A"),
new Node("orig2", "B"),
},
after: new[]
{
new Node("orig1", "A edited"),
new Node("orig1", "A inserted") { IsNew = true },
new Node("orig2", "B edited"),
new Node("orig2", "B inserted") { IsNew = true },
});
}
[Fact]
public async Task CanRetainFocusWhileMovingTextBox()
{
@ -272,7 +291,7 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Browser.Equal(Array.Empty<bool>(), completeItemStates);
}
private void PerformTest(Node[] before, Node[] after)
private void PerformTest(Node[] before, Node[] after, bool useLooseKeys = false)
{
var rootBefore = new Node(null, "root", before);
var rootAfter = new Node(null, "root", after);
@ -283,6 +302,11 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
var textbox = appElem.FindElement(By.TagName("textarea"));
var updateButton = appElem.FindElement(By.TagName("button"));
if (useLooseKeys)
{
appElem.FindElement(By.Id("key-loose")).Click();
}
SetTextAreaValueFast(textbox, jsonBefore);
updateButton.Click();
ValidateRenderedOutput(appElem, rootBefore, validatePreservation: false);

View File

@ -4,6 +4,10 @@
<p>Model</p>
<textarea @bind="modelJson" id="key-model"></textarea>
<button @onclick="Update">Update</button>
<label>
<input type="checkbox" @bind="@renderContext.UseLooseKeys" id="key-loose" />
Use loose keys
</label>
</div>
<div class="render-output">
<p>Output</p>
@ -69,5 +73,7 @@
// This is so the descendants can detect and display whether they are
// newly-instantiated on any given render
public int UpdateCount { get; set; }
public bool UseLooseKeys { get; set; }
}
}

View File

@ -1,3 +1,4 @@
@using Microsoft.AspNetCore.Components.RenderTree
<div class="node">
<strong class="label">@Data.Label</strong>
&nbsp;&nbsp;[
@ -16,19 +17,9 @@
@if (Data.Children?.Any() ?? false)
{
<div class="children">@{
foreach (var child in Data.Children)
{
if (child.Key != null)
{
<KeyCasesTreeNode @key="@child.Key" Data="@child" />
}
else
{
<KeyCasesTreeNode Data="@child" />
}
}
}</div>
<div class="children">
@((RenderFragment)RenderKeyCasesTreeNodes)
</div>
}
</div>
@ -50,4 +41,31 @@
{
firstCreatedOnUpdateCount = RenderContext.UpdateCount;
}
void RenderKeyCasesTreeNodes(RenderTreeBuilder builder)
{
// This is equivalent to:
// @foreach (var child in Data.Children)
// {
// if (key != null)
// {
// <KeyCasesTreeNode @key="@child.Key" @key:loose="@looseKey" Data="@child" />
// }
// else
// {
// <KeyCasesTreeNode Data="@child" />
// }
// }
// TODO: Once the compiler supports @key:loose, eliminate this and just use regular Razor syntax
foreach (var child in Data.Children)
{
builder.OpenComponent<KeyCasesTreeNode>(0);
if (child.Key != null)
{
builder.SetKey(child.Key, looseKey: RenderContext.UseLooseKeys);
}
builder.AddAttribute(1, nameof(Data), child);
builder.CloseComponent();
}
}
}