For @key, support both strict (default) and loose modes
This commit is contained in:
parent
20115f3c84
commit
5141ee930e
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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}.");
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
@using Microsoft.AspNetCore.Components.RenderTree
|
||||
<div class="node">
|
||||
<strong class="label">@Data.Label</strong>
|
||||
[
|
||||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue