Support diffing attributes. This required tracking 'sequence' values on attributes too.

This commit is contained in:
Steve Sanderson 2018-01-21 20:57:11 +00:00
parent 7a1abbaca3
commit 0a5e27fdcf
10 changed files with 299 additions and 48 deletions

View File

@ -30,10 +30,22 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
private string _unconsumedHtml; private string _unconsumedHtml;
private IList<object> _currentAttributeValues; private IList<object> _currentAttributeValues;
private IDictionary<string, object> _currentElementAttributes = new Dictionary<string, object>(); private IDictionary<string, PendingAttribute> _currentElementAttributes = new Dictionary<string, PendingAttribute>();
private IList<IntermediateToken> _currentElementAttributeTokens = new List<IntermediateToken>(); private IList<PendingAttributeToken> _currentElementAttributeTokens = new List<PendingAttributeToken>();
private int _sourceSequence = 0; private int _sourceSequence = 0;
private struct PendingAttribute
{
public int SourceSequence;
public object AttributeValue;
}
private struct PendingAttributeToken
{
public int SourceSequence;
public IntermediateToken AttributeValue;
}
public override void BeginWriterScope(CodeRenderingContext context, string writer) public override void BeginWriterScope(CodeRenderingContext context, string writer)
{ {
throw new System.NotImplementedException(nameof(BeginWriterScope)); throw new System.NotImplementedException(nameof(BeginWriterScope));
@ -100,7 +112,11 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
if (_unconsumedHtml != null) if (_unconsumedHtml != null)
{ {
var token = (IntermediateToken)node.Children.Single(); var token = (IntermediateToken)node.Children.Single();
_currentElementAttributeTokens.Add(token); _currentElementAttributeTokens.Add(new PendingAttributeToken
{
SourceSequence = _sourceSequence++,
AttributeValue = token
});
return; return;
} }
@ -149,9 +165,14 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node) public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
{ {
var attributeSourceSequence = _sourceSequence++;
_currentAttributeValues = new List<object>(); _currentAttributeValues = new List<object>();
context.RenderChildren(node); context.RenderChildren(node);
_currentElementAttributes[node.AttributeName] = _currentAttributeValues; _currentElementAttributes[node.AttributeName] = new PendingAttribute
{
SourceSequence = attributeSourceSequence,
AttributeValue = _currentAttributeValues
};
_currentAttributeValues = null; _currentAttributeValues = null;
} }
@ -213,14 +234,14 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
foreach (var attribute in nextTag.Attributes) foreach (var attribute in nextTag.Attributes)
{ {
WriteAttribute(codeWriter, attribute.Key, attribute.Value); WriteAttribute(codeWriter, _sourceSequence++, attribute.Key, attribute.Value);
} }
if (_currentElementAttributes.Count > 0) if (_currentElementAttributes.Count > 0)
{ {
foreach (var pair in _currentElementAttributes) foreach (var pair in _currentElementAttributes)
{ {
WriteAttribute(codeWriter, pair.Key, pair.Value); WriteAttribute(codeWriter, pair.Value.SourceSequence, pair.Key, pair.Value.AttributeValue);
} }
_currentElementAttributes.Clear(); _currentElementAttributes.Clear();
} }
@ -231,7 +252,9 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
{ {
codeWriter codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}") .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
.Write(token.Content) .Write(token.SourceSequence.ToString())
.WriteParameterSeparator()
.Write(token.AttributeValue.Content)
.WriteEndMethodInvocation(); .WriteEndMethodInvocation();
} }
_currentElementAttributeTokens.Clear(); _currentElementAttributeTokens.Clear();
@ -263,10 +286,12 @@ namespace Microsoft.Blazor.Build.Core.RazorCompilation.Engine
} }
} }
private static void WriteAttribute(CodeWriter codeWriter, string key, object value) private static void WriteAttribute(CodeWriter codeWriter, int sourceSequence, string key, object value)
{ {
codeWriter codeWriter
.WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}") .WriteStartMethodInvocation($"{builderVarName}.{nameof(RenderTreeBuilder.AddAttribute)}")
.Write(sourceSequence.ToString())
.WriteParameterSeparator()
.WriteStringLiteral(key) .WriteStringLiteral(key)
.WriteParameterSeparator(); .WriteParameterSeparator();
WriteAttributeValue(codeWriter, value); WriteAttributeValue(codeWriter, value);

View File

@ -41,6 +41,7 @@ namespace Microsoft.Blazor.Components
/// <param name="handler">The handler to be invoked when the event occurs.</param> /// <param name="handler">The handler to be invoked when the event occurs.</param>
/// <returns>A <see cref="RenderTreeNode"/> that represents the event handler.</returns> /// <returns>A <see cref="RenderTreeNode"/> that represents the event handler.</returns>
protected RenderTreeNode onclick(Action handler) protected RenderTreeNode onclick(Action handler)
=> RenderTreeNode.Attribute("onclick", _ => handler()); // Note that the 'sequence' value is updated later when inserted into the tree
=> RenderTreeNode.Attribute(0, "onclick", _ => handler());
} }
} }

View File

@ -73,45 +73,49 @@ namespace Microsoft.Blazor.RenderTree
/// Appends a node representing a string-valued attribute. /// Appends a node representing a string-valued attribute.
/// The attribute is associated with the most recently added element. /// The attribute is associated with the most recently added element.
/// </summary> /// </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="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param> /// <param name="value">The value of the attribute.</param>
public void AddAttribute(string name, string value) public void AddAttribute(int sequence, string name, string value)
{ {
AssertCanAddAttribute(); AssertCanAddAttribute();
Append(RenderTreeNode.Attribute(name, value)); Append(RenderTreeNode.Attribute(sequence, name, value));
} }
/// <summary> /// <summary>
/// Appends a node representing an <see cref="UIEventArgs"/>-valued attribute. /// Appends a node representing an <see cref="UIEventArgs"/>-valued attribute.
/// The attribute is associated with the most recently added element. /// The attribute is associated with the most recently added element.
/// </summary> /// </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="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param> /// <param name="value">The value of the attribute.</param>
public void AddAttribute(string name, UIEventHandler value) public void AddAttribute(int sequence, string name, UIEventHandler value)
{ {
AssertCanAddAttribute(); AssertCanAddAttribute();
Append(RenderTreeNode.Attribute(name, value)); Append(RenderTreeNode.Attribute(sequence, name, value));
} }
/// <summary> /// <summary>
/// Appends a node representing a string-valued attribute. /// Appends a node representing a string-valued attribute.
/// The attribute is associated with the most recently added element. /// The attribute is associated with the most recently added element.
/// </summary> /// </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="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param> /// <param name="value">The value of the attribute.</param>
public void AddAttribute(string name, object value) public void AddAttribute(int sequence, string name, object value)
{ {
AssertCanAddAttribute(); AssertCanAddAttribute();
Append(RenderTreeNode.Attribute(name, value.ToString())); Append(RenderTreeNode.Attribute(sequence, name, value.ToString()));
} }
/// <summary> /// <summary>
/// Appends a node representing an attribute. /// Appends a node representing an attribute.
/// The attribute is associated with the most recently added element. /// The attribute is associated with the most recently added element.
/// </summary> /// </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="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param> /// <param name="value">The value of the attribute.</param>
public void AddAttribute(RenderTreeNode node) public void AddAttribute(int sequence, RenderTreeNode node)
{ {
if (node.NodeType != RenderTreeNodeType.Attribute) if (node.NodeType != RenderTreeNodeType.Attribute)
{ {
@ -119,6 +123,7 @@ namespace Microsoft.Blazor.RenderTree
} }
AssertCanAddAttribute(); AssertCanAddAttribute();
node.SetSequence(sequence);
Append(node); Append(node);
} }

View File

@ -33,8 +33,8 @@ namespace Microsoft.Blazor.RenderTree
RenderTreeNode[] oldTree, int oldStartIndex, int oldEndIndexExcl, RenderTreeNode[] oldTree, int oldStartIndex, int oldEndIndexExcl,
RenderTreeNode[] newTree, int newStartIndex, int newEndIndexExcl) RenderTreeNode[] newTree, int newStartIndex, int newEndIndexExcl)
{ {
var hasMoreOld = oldEndIndexExcl > 0; var hasMoreOld = oldEndIndexExcl > oldStartIndex;
var hasMoreNew = newEndIndexExcl > 0; var hasMoreNew = newEndIndexExcl > newStartIndex;
var prevOldSeq = -1; var prevOldSeq = -1;
var prevNewSeq = -1; var prevNewSeq = -1;
while (hasMoreOld || hasMoreNew) while (hasMoreOld || hasMoreNew)
@ -119,15 +119,32 @@ namespace Microsoft.Blazor.RenderTree
if (treatAsInsert) if (treatAsInsert)
{ {
Append(RenderTreeDiffEntry.PrependNode(newStartIndex)); if (newTree[newStartIndex].NodeType == RenderTreeNodeType.Attribute)
newStartIndex = NextSiblingIndex(newTree, newStartIndex); {
Append(RenderTreeDiffEntry.SetAttribute(newStartIndex));
newStartIndex++;
}
else
{
Append(RenderTreeDiffEntry.PrependNode(newStartIndex));
newStartIndex = NextSiblingIndex(newTree, newStartIndex);
}
hasMoreNew = newEndIndexExcl > newStartIndex; hasMoreNew = newEndIndexExcl > newStartIndex;
prevNewSeq = newSeq; prevNewSeq = newSeq;
} }
else else
{ {
Append(RenderTreeDiffEntry.RemoveNode()); if (oldTree[oldStartIndex].NodeType == RenderTreeNodeType.Attribute)
oldStartIndex = NextSiblingIndex(oldTree, oldStartIndex); {
Append(RenderTreeDiffEntry.RemoveAttribute(oldTree[oldStartIndex].AttributeName));
oldStartIndex++;
}
else
{
Append(RenderTreeDiffEntry.RemoveNode());
oldStartIndex = NextSiblingIndex(oldTree, oldStartIndex);
}
hasMoreOld = oldEndIndexExcl > oldStartIndex; hasMoreOld = oldEndIndexExcl > oldStartIndex;
prevOldSeq = oldSeq; prevOldSeq = oldSeq;
} }
@ -170,8 +187,12 @@ namespace Microsoft.Blazor.RenderTree
var newElementName = newTree[newNodeIndex].ElementName; var newElementName = newTree[newNodeIndex].ElementName;
if (string.Equals(oldElementName, newElementName, StringComparison.Ordinal)) if (string.Equals(oldElementName, newElementName, StringComparison.Ordinal))
{ {
// TODO: Compare attributes // Recurse into the element. This covers diffing the attributes as well as
// TODO: Then, recurse into children // the descendants.
AppendDiffEntriesForRange(
oldTree, oldNodeIndex + 1, oldTree[oldNodeIndex].ElementDescendantsEndIndex + 1,
newTree, newNodeIndex + 1, newTree[newNodeIndex].ElementDescendantsEndIndex + 1);
Append(RenderTreeDiffEntry.Continue()); Append(RenderTreeDiffEntry.Continue());
} }
else else
@ -205,8 +226,33 @@ namespace Microsoft.Blazor.RenderTree
break; break;
} }
case RenderTreeNodeType.Attribute:
{
var oldName = oldTree[oldNodeIndex].AttributeName;
var newName = newTree[newNodeIndex].AttributeName;
if (string.Equals(oldName, newName, StringComparison.Ordinal))
{
var changed =
!string.Equals(oldTree[oldNodeIndex].AttributeValue, newTree[newNodeIndex].AttributeValue, StringComparison.Ordinal)
|| oldTree[oldNodeIndex].AttributeEventHandlerValue != newTree[newNodeIndex].AttributeEventHandlerValue;
if (changed)
{
Append(RenderTreeDiffEntry.SetAttribute(newNodeIndex));
}
}
else
{
// 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
Append(RenderTreeDiffEntry.SetAttribute(newNodeIndex));
Append(RenderTreeDiffEntry.RemoveAttribute(oldName));
}
break;
}
default: default:
throw new NotImplementedException($"Not yet implemented: diffing for nodes of type {newTree[newNodeIndex].NodeType}"); throw new NotImplementedException($"Encountered unsupported node type during diffing: {newTree[newNodeIndex].NodeType}");
} }
} }

View File

@ -1,12 +1,16 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
namespace Microsoft.Blazor.RenderTree namespace Microsoft.Blazor.RenderTree
{ {
internal struct RenderTreeDiffEntry internal struct RenderTreeDiffEntry
{ {
public RenderTreeDiffEntryType Type { get; private set; } public RenderTreeDiffEntryType Type { get; private set; }
public int NewTreeIndex { get; private set; } public int NewTreeIndex { get; private set; }
public string RemovedAttributeName { get; private set; }
public static RenderTreeDiffEntry Continue() => new RenderTreeDiffEntry public static RenderTreeDiffEntry Continue() => new RenderTreeDiffEntry
{ {
@ -29,5 +33,17 @@ namespace Microsoft.Blazor.RenderTree
Type = RenderTreeDiffEntryType.UpdateText, Type = RenderTreeDiffEntryType.UpdateText,
NewTreeIndex = newTreeIndex NewTreeIndex = newTreeIndex
}; };
internal static RenderTreeDiffEntry SetAttribute(int newNodeIndex) => new RenderTreeDiffEntry
{
Type = RenderTreeDiffEntryType.SetAttribute,
NewTreeIndex = newNodeIndex
};
internal static RenderTreeDiffEntry RemoveAttribute(string name) => new RenderTreeDiffEntry
{
Type = RenderTreeDiffEntryType.RemoveAttribute,
RemovedAttributeName = name
};
} }
} }

View File

@ -8,7 +8,8 @@ namespace Microsoft.Blazor.RenderTree
Continue = 1, Continue = 1,
PrependNode = 2, PrependNode = 2,
RemoveNode = 3, RemoveNode = 3,
UpdateAttribute = 4, SetAttribute = 4,
UpdateText = 5, RemoveAttribute = 5,
UpdateText = 6,
} }
} }

View File

@ -16,9 +16,7 @@ namespace Microsoft.Blazor.RenderTree
public struct RenderTreeNode public struct RenderTreeNode
{ {
/// <summary> /// <summary>
/// If the <see cref="NodeType"/> property equals <see cref="RenderTreeNodeType.Element"/>, /// Gets the sequence number of the node. Sequence numbers indicate the relative source
/// <see cref="RenderTreeNodeType.Text"/>, or <see cref="RenderTreeNodeType.Component"/>,
/// gets the sequence number of the node. Sequence numbers indicate the relative source
/// positions of the instructions that inserted the nodes. Sequence numbers are only /// positions of the instructions that inserted the nodes. Sequence numbers are only
/// comparable within the same sequence (typically, the same source method). /// comparable within the same sequence (typically, the same source method).
/// </summary> /// </summary>
@ -98,15 +96,17 @@ namespace Microsoft.Blazor.RenderTree
TextContent = textContent ?? string.Empty, TextContent = textContent ?? string.Empty,
}; };
internal static RenderTreeNode Attribute(string name, string value) => new RenderTreeNode internal static RenderTreeNode Attribute(int sequence, string name, string value) => new RenderTreeNode
{ {
Sequence = sequence,
NodeType = RenderTreeNodeType.Attribute, NodeType = RenderTreeNodeType.Attribute,
AttributeName = name, AttributeName = name,
AttributeValue = value AttributeValue = value
}; };
internal static RenderTreeNode Attribute(string name, UIEventHandler value) => new RenderTreeNode internal static RenderTreeNode Attribute(int sequence, string name, UIEventHandler value) => new RenderTreeNode
{ {
Sequence = sequence,
NodeType = RenderTreeNodeType.Attribute, NodeType = RenderTreeNodeType.Attribute,
AttributeName = name, AttributeName = name,
AttributeEventHandlerValue = value AttributeEventHandlerValue = value
@ -129,5 +129,13 @@ namespace Microsoft.Blazor.RenderTree
ComponentId = componentId; ComponentId = componentId;
Component = component; Component = component;
} }
internal void SetSequence(int sequence)
{
// This is only used when appending attribute nodes, because helpers such as @onclick
// need to construct the attribute node in a context where they don't know the sequence
// number, so we assign it later
Sequence = sequence;
}
} }
} }

View File

@ -174,10 +174,10 @@ namespace Microsoft.Blazor.Test
// Act // Act
builder.OpenElement(0, "myelement"); // 0: <myelement builder.OpenElement(0, "myelement"); // 0: <myelement
builder.AddAttribute("attribute1", "value 1"); // 1: attribute1="value 1" builder.AddAttribute(0, "attribute1", "value 1"); // 1: attribute1="value 1"
builder.AddAttribute("attribute2", 123); // 2: attribute2=intExpression123> builder.AddAttribute(0, "attribute2", 123); // 2: attribute2=intExpression123>
builder.OpenElement(0, "child"); // 3: <child builder.OpenElement(0, "child"); // 3: <child
builder.AddAttribute("childevent", eventHandler); // 4: childevent=eventHandler> builder.AddAttribute(0, "childevent", eventHandler); // 4: childevent=eventHandler>
builder.AddText(0, "some text"); // 5: some text builder.AddText(0, "some text"); // 5: some text
builder.CloseElement(); // </child> builder.CloseElement(); // </child>
builder.CloseElement(); // </myelement> builder.CloseElement(); // </myelement>
@ -201,7 +201,7 @@ namespace Microsoft.Blazor.Test
// Act/Assert // Act/Assert
Assert.Throws<InvalidOperationException>(() => Assert.Throws<InvalidOperationException>(() =>
{ {
builder.AddAttribute("name", "value"); builder.AddAttribute(0, "name", "value");
}); });
} }
@ -214,7 +214,7 @@ namespace Microsoft.Blazor.Test
// Act/Assert // Act/Assert
Assert.Throws<InvalidOperationException>(() => Assert.Throws<InvalidOperationException>(() =>
{ {
builder.AddAttribute("name", eventInfo => { }); builder.AddAttribute(0, "name", eventInfo => { });
}); });
} }
@ -229,7 +229,7 @@ namespace Microsoft.Blazor.Test
{ {
builder.OpenElement(0, "some element"); builder.OpenElement(0, "some element");
builder.AddText(1, "hello"); builder.AddText(1, "hello");
builder.AddAttribute("name", "value"); builder.AddAttribute(2, "name", "value");
}); });
} }
@ -244,7 +244,7 @@ namespace Microsoft.Blazor.Test
{ {
builder.OpenElement(0, "some element"); builder.OpenElement(0, "some element");
builder.AddText(1, "hello"); builder.AddText(1, "hello");
builder.AddAttribute("name", eventInfo => { }); builder.AddAttribute(2, "name", eventInfo => { });
}); });
} }
@ -255,13 +255,13 @@ namespace Microsoft.Blazor.Test
var builder = new RenderTreeBuilder(new TestRenderer()); var builder = new RenderTreeBuilder(new TestRenderer());
// Act // Act
builder.OpenElement(0, "parent"); // 0: <parent> builder.OpenElement(10, "parent"); // 0: <parent>
builder.AddComponent<TestComponent>(1); // 1: <testcomponent builder.AddComponent<TestComponent>(11); // 1: <testcomponent
builder.AddAttribute("child1attribute1", "A"); // 2: child1attribute1="A" builder.AddAttribute(12, "child1attribute1", "A"); // 2: child1attribute1="A"
builder.AddAttribute("child1attribute2", "B"); // 3: child1attribute2="B" /> builder.AddAttribute(13, "child1attribute2", "B"); // 3: child1attribute2="B" />
builder.AddComponent<TestComponent>(2); // 4: <testcomponent builder.AddComponent<TestComponent>(14); // 4: <testcomponent
builder.AddAttribute("child2attribute", "C"); // 5: child2attribute="C" /> builder.AddAttribute(15, "child2attribute", "C"); // 5: child2attribute="C" />
builder.CloseElement(); // </parent> builder.CloseElement(); // </parent>
// Assert // Assert
Assert.Collection(builder.GetNodes(), Assert.Collection(builder.GetNodes(),

View File

@ -40,7 +40,7 @@ namespace Microsoft.Blazor.Test
builder => builder =>
{ {
builder.OpenElement(0, "Some Element"); builder.OpenElement(0, "Some Element");
builder.AddAttribute("My attribute", "My value"); builder.AddAttribute(1, "My attribute", "My value");
builder.CloseElement(); builder.CloseElement();
}, },
builder => builder.AddComponent<FakeComponent>(0) builder => builder.AddComponent<FakeComponent>(0)
@ -363,6 +363,155 @@ namespace Microsoft.Blazor.Test
entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type)); entry => Assert.Equal(RenderTreeDiffEntryType.RemoveNode, entry.Type));
} }
[Fact]
public void RecognizesAttributesAdded()
{
// Arrange
var oldTree = new RenderTreeBuilder(new FakeRenderer());
var newTree = new RenderTreeBuilder(new FakeRenderer());
var diff = new RenderTreeDiff();
oldTree.OpenElement(0, "My element");
oldTree.AddAttribute(1, "existing", "existing value");
oldTree.CloseElement();
newTree.OpenElement(0, "My element");
newTree.AddAttribute(1, "existing", "existing value");
newTree.AddAttribute(2, "added", "added value");
newTree.CloseElement();
// Act
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
// Assert
Assert.Collection(result,
entry =>
{
Assert.Equal(RenderTreeDiffEntryType.SetAttribute, entry.Type);
Assert.Equal(2, entry.NewTreeIndex);
},
entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type));
}
[Fact]
public void RecognizesAttributesRemoved()
{
// Arrange
var oldTree = new RenderTreeBuilder(new FakeRenderer());
var newTree = new RenderTreeBuilder(new FakeRenderer());
var diff = new RenderTreeDiff();
oldTree.OpenElement(0, "My element");
oldTree.AddAttribute(1, "will be removed", "will be removed value");
oldTree.AddAttribute(2, "will survive", "surviving value");
oldTree.CloseElement();
newTree.OpenElement(0, "My element");
newTree.AddAttribute(2, "will survive", "surviving value");
newTree.CloseElement();
// Act
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
// Assert
Assert.Collection(result,
entry =>
{
Assert.Equal(RenderTreeDiffEntryType.RemoveAttribute, entry.Type);
Assert.Equal("will be removed", entry.RemovedAttributeName);
},
entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type));
}
[Fact]
public void RecognizesAttributeStringValuesChanged()
{
// Arrange
var oldTree = new RenderTreeBuilder(new FakeRenderer());
var newTree = new RenderTreeBuilder(new FakeRenderer());
var diff = new RenderTreeDiff();
oldTree.OpenElement(0, "My element");
oldTree.AddAttribute(1, "will remain", "will remain value");
oldTree.AddAttribute(2, "will change", "will change value");
oldTree.CloseElement();
newTree.OpenElement(0, "My element");
newTree.AddAttribute(1, "will remain", "will remain value");
newTree.AddAttribute(2, "will change", "did change value");
newTree.CloseElement();
// Act
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
// Assert
Assert.Collection(result,
entry =>
{
Assert.Equal(RenderTreeDiffEntryType.SetAttribute, entry.Type);
Assert.Equal(2, entry.NewTreeIndex);
},
entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type));
}
[Fact]
public void RecognizesAttributeEventHandlerValuesChanged()
{
// Arrange
var oldTree = new RenderTreeBuilder(new FakeRenderer());
var newTree = new RenderTreeBuilder(new FakeRenderer());
var diff = new RenderTreeDiff();
UIEventHandler retainedHandler = _ => { };
UIEventHandler removedHandler = _ => { };
UIEventHandler addedHandler = _ => { };
oldTree.OpenElement(0, "My element");
oldTree.AddAttribute(1, "will remain", retainedHandler);
oldTree.AddAttribute(2, "will change", removedHandler);
oldTree.CloseElement();
newTree.OpenElement(0, "My element");
newTree.AddAttribute(1, "will remain", retainedHandler);
newTree.AddAttribute(2, "will change", addedHandler);
newTree.CloseElement();
// Act
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
// Assert
Assert.Collection(result,
entry =>
{
Assert.Equal(RenderTreeDiffEntryType.SetAttribute, entry.Type);
Assert.Equal(2, entry.NewTreeIndex);
},
entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type));
}
[Fact]
public void RecognizesAttributeNamesChangedAtSameSourceSequence()
{
// Arrange
var oldTree = new RenderTreeBuilder(new FakeRenderer());
var newTree = new RenderTreeBuilder(new FakeRenderer());
var diff = new RenderTreeDiff();
oldTree.OpenElement(0, "My element");
oldTree.AddAttribute(1, "oldname", "same value");
oldTree.CloseElement();
newTree.OpenElement(0, "My element");
newTree.AddAttribute(1, "newname", "same value");
newTree.CloseElement();
// Act
var result = diff.ComputeDifference(oldTree.GetNodes(), newTree.GetNodes());
// Assert
Assert.Collection(result,
entry =>
{
Assert.Equal(RenderTreeDiffEntryType.SetAttribute, entry.Type);
Assert.Equal(1, entry.NewTreeIndex);
},
entry =>
{
Assert.Equal(RenderTreeDiffEntryType.RemoveAttribute, entry.Type);
Assert.Equal("oldname", entry.RemovedAttributeName);
},
entry => Assert.Equal(RenderTreeDiffEntryType.Continue, entry.Type));
}
private class FakeRenderer : Renderer private class FakeRenderer : Renderer
{ {
internal protected override void UpdateDisplay(int componentId, ArraySegment<RenderTreeNode> renderTree) internal protected override void UpdateDisplay(int componentId, ArraySegment<RenderTreeNode> renderTree)

View File

@ -361,7 +361,7 @@ namespace Microsoft.Blazor.Test
public void BuildRenderTree(RenderTreeBuilder builder) public void BuildRenderTree(RenderTreeBuilder builder)
{ {
builder.OpenElement(0, "some element"); builder.OpenElement(0, "some element");
builder.AddAttribute("some event", Handler); builder.AddAttribute(1, "some event", Handler);
builder.CloseElement(); builder.CloseElement();
} }
} }