diff --git a/.gitignore b/.gitignore
index 9b152674da..eabb0b0e98 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,4 +5,5 @@ obj/
launchSettings.json
artifacts/
msbuild.binlog
-.vscode/
\ No newline at end of file
+.vscode/
+BenchmarkDotNet.Artifacts/
\ No newline at end of file
diff --git a/Blazor.sln b/Blazor.sln
index 853eabe2ae..dfffd765ed 100644
--- a/Blazor.sln
+++ b/Blazor.sln
@@ -92,6 +92,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestContentPackage", "test\
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LiveReloadTestApp", "test\testapps\LiveReloadTestApp\LiveReloadTestApp.csproj", "{0246AA77-1A27-4A67-874B-6EF6F99E414E}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Blazor.Performance", "benchmarks\Microsoft.AspNetCore.Blazor.Performance\Microsoft.AspNetCore.Blazor.Performance.csproj", "{50F6820F-D058-4E68-9E15-801F893F514E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -323,6 +327,14 @@ Global
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.Release|Any CPU.Build.0 = Release|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
{0246AA77-1A27-4A67-874B-6EF6F99E414E}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU
+ {50F6820F-D058-4E68-9E15-801F893F514E}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -364,6 +376,7 @@ Global
{9088E4E4-B855-457F-AE9E-D86709A5E1F4} = {F563ABB6-85FB-4CFC-B0D2-1D5130E8246D}
{C57382BC-EE93-49D5-BC40-5C98AF8AA048} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
{0246AA77-1A27-4A67-874B-6EF6F99E414E} = {4AE0D35B-D97A-44D0-8392-C9240377DCCE}
+ {50F6820F-D058-4E68-9E15-801F893F514E} = {36A7DEB7-5F88-4BFB-B57E-79EEC9950E25}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {504DA352-6788-4DC0-8705-82167E72A4D3}
diff --git a/benchmarks/Microsoft.AspNetCore.Blazor.Performance/AssemblyInfo.cs b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/AssemblyInfo.cs
new file mode 100644
index 0000000000..59c4ce5180
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/AssemblyInfo.cs
@@ -0,0 +1,4 @@
+// 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.
+
+[assembly: BenchmarkDotNet.Attributes.AspNetCoreBenchmark]
\ No newline at end of file
diff --git a/benchmarks/Microsoft.AspNetCore.Blazor.Performance/Microsoft.AspNetCore.Blazor.Performance.csproj b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/Microsoft.AspNetCore.Blazor.Performance.csproj
new file mode 100644
index 0000000000..4782af235c
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/Microsoft.AspNetCore.Blazor.Performance.csproj
@@ -0,0 +1,19 @@
+
+
+
+ netcoreapp2.0
+ Exe
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/benchmarks/Microsoft.AspNetCore.Blazor.Performance/Program.cs b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/Program.cs
new file mode 100644
index 0000000000..71532fa840
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/Program.cs
@@ -0,0 +1,47 @@
+// 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.Diagnostics;
+using Microsoft.AspNetCore.Blazor.Performance;
+
+namespace Microsoft.AspNetCore.BenchmarkDotNet.Runner
+{
+ internal partial class Program
+ {
+ static partial void BeforeMain(string[] args)
+ {
+ if (args.Length == 0 || args[0] != "--profile")
+ {
+ return;
+ }
+
+ // Write code here if you want to profile something. Normally Benchmark.NET launches
+ // a separate process, which can be hard to profile.
+ //
+ // See: https://github.com/dotnet/BenchmarkDotNet/issues/387
+
+ // Example:
+ //Console.WriteLine("Starting...");
+ //var stopwatch = Stopwatch.StartNew();
+ //var benchmark = new RenderTreeDiffBuilderBenchmark();
+
+ //for (var i = 0; i < 100000; i++)
+ //{
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ // benchmark.ComputeDiff_SingleFormField();
+ //}
+
+ //Console.WriteLine($"Done after {stopwatch.ElapsedMilliseconds}ms");
+ //Environment.Exit(0);
+ }
+ }
+}
diff --git a/benchmarks/Microsoft.AspNetCore.Blazor.Performance/RenderTreeDiffBuilderBenchmark.cs b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/RenderTreeDiffBuilderBenchmark.cs
new file mode 100644
index 0000000000..9f81a9042d
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/RenderTreeDiffBuilderBenchmark.cs
@@ -0,0 +1,108 @@
+// 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.Linq;
+using BenchmarkDotNet.Attributes;
+using Microsoft.AspNetCore.Blazor.Components;
+using Microsoft.AspNetCore.Blazor.Rendering;
+using Microsoft.AspNetCore.Blazor.RenderTree;
+
+namespace Microsoft.AspNetCore.Blazor.Performance
+{
+ public class RenderTreeDiffBuilderBenchmark
+ {
+ private readonly Renderer renderer;
+ private readonly RenderTreeBuilder original;
+ private readonly RenderTreeBuilder modified;
+ private readonly RenderBatchBuilder builder;
+
+ public RenderTreeDiffBuilderBenchmark()
+ {
+ builder = new RenderBatchBuilder();
+ renderer = new FakeRenderer();
+
+ // A simple component for basic tests -- this is similar to what MVC scaffolding generates
+ // for bootstrap3 form fields, but modified to be more Blazorey.
+ original = new RenderTreeBuilder(renderer);
+ original.OpenElement(0, "div");
+ original.AddAttribute(1, "class", "form-group");
+
+ original.OpenElement(2, "label");
+ original.AddAttribute(3, "class", "control-label");
+ original.AddAttribute(4, "for", "name");
+ original.AddAttribute(5, "data-unvalidated", true);
+ original.AddContent(6, "Car");
+ original.CloseElement();
+
+ original.OpenElement(7, "input");
+ original.AddAttribute(8, "class", "form-control");
+ original.AddAttribute(9, "type", "text");
+ original.AddAttribute(10, "name", "name"); // Notice the gap in sequence numbers
+ original.AddAttribute(12, "value", "");
+ original.CloseElement();
+
+ original.OpenElement(13, "span");
+ original.AddAttribute(14, "class", "text-danger field-validation-valid");
+ original.AddContent(15, "");
+ original.CloseElement();
+
+ original.CloseElement();
+
+ // Now simulate some input
+ modified = new RenderTreeBuilder(renderer);
+ modified.OpenElement(0, "div");
+ modified.AddAttribute(1, "class", "form-group");
+
+ modified.OpenElement(2, "label");
+ modified.AddAttribute(3, "class", "control-label");
+ modified.AddAttribute(4, "for", "name");
+ modified.AddAttribute(5, "data-unvalidated", false);
+ modified.AddContent(6, "Car");
+ modified.CloseElement();
+
+ modified.OpenElement(7, "input");
+ modified.AddAttribute(8, "class", "form-control");
+ modified.AddAttribute(9, "type", "text");
+ modified.AddAttribute(10, "name", "name");
+ modified.AddAttribute(11, "data-validation-state", "invalid");
+ modified.AddAttribute(12, "value", "Lamborghini");
+ modified.CloseElement();
+
+ modified.OpenElement(13, "span");
+ modified.AddAttribute(14, "class", "text-danger field-validation-invalid"); // changed
+ modified.AddContent(15, "No, you can't afford that.");
+ modified.CloseElement();
+
+ modified.CloseElement();
+ }
+
+ [Benchmark(Description = "RenderTreeDiffBuilder: Input and validation on a single form field.", Baseline = true)]
+ public void ComputeDiff_SingleFormField()
+ {
+ builder.Clear();
+ var diff = RenderTreeDiffBuilder.ComputeDiff(renderer, builder, 0, original.GetFrames(), modified.GetFrames());
+ GC.KeepAlive(diff);
+ }
+
+ private class FakeRenderer : Renderer
+ {
+ public FakeRenderer()
+ : base(new TestServiceProvider())
+ {
+ }
+
+ protected override void UpdateDisplay(RenderBatch renderBatch)
+ {
+ }
+ }
+
+ private class TestServiceProvider : IServiceProvider
+ {
+ public object GetService(Type serviceType)
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/benchmarks/Microsoft.AspNetCore.Blazor.Performance/readme.md b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/readme.md
new file mode 100644
index 0000000000..82e7adfab7
--- /dev/null
+++ b/benchmarks/Microsoft.AspNetCore.Blazor.Performance/readme.md
@@ -0,0 +1,35 @@
+# Benchmarks
+
+## Instructions
+
+### Run All Benchmarks
+
+To run all use `*` as parameter
+```
+dotnet run -c Release -- *
+```
+
+### Interactive Mode
+
+To see the list of benchmarks run (and choose interactively):
+```
+dotnet run -c Release
+```
+
+### Run Specific Benchmark
+
+To run a specific benchmark add it as parameter
+```
+dotnet run -c Release --
+```
+
+
+## Troubleshooting
+
+The runner will create logs in the `\BenchmarkDotNet.Artifacts` directory. That should include a lot more information
+than what gets printed to the console.
+
+## Results
+
+Also in the `\BenchmarkDotNet.Artifacts\results` directive you'll find some markdown-formatted tables suitable for posting
+in a github comment.
\ No newline at end of file
diff --git a/build/dependencies.props b/build/dependencies.props
index 0142a8ed99..d36a1f4fda 100644
--- a/build/dependencies.props
+++ b/build/dependencies.props
@@ -3,7 +3,9 @@
$(MSBuildAllProjects);$(MSBuildThisFileFullPath)
+ 0.10.13
2.1.0-preview2-15704
+ 2.1.0-preview3-32064
diff --git a/build/repo.props b/build/repo.props
index 3921cba3bd..35e3dd34f0 100644
--- a/build/repo.props
+++ b/build/repo.props
@@ -2,7 +2,7 @@
- false
+ true
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts
index 495f9f2a9b..c06195c3c4 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts
+++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Rendering/BrowserRenderer.ts
@@ -230,7 +230,7 @@ export class BrowserRenderer {
case 'SELECT':
case 'TEXTAREA':
if (isCheckbox(element)) {
- (element as HTMLInputElement).checked = value === 'True';
+ (element as HTMLInputElement).checked = value === '';
} else {
(element as any).value = value;
diff --git a/src/Microsoft.AspNetCore.Blazor/Properties/AssemblyInfo.cs b/src/Microsoft.AspNetCore.Blazor/Properties/AssemblyInfo.cs
index e25c100011..fcbca4b037 100644
--- a/src/Microsoft.AspNetCore.Blazor/Properties/AssemblyInfo.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Properties/AssemblyInfo.cs
@@ -3,3 +3,4 @@
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Test")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Browser.Test")]
[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Build.Test")]
+[assembly: InternalsVisibleTo("Microsoft.AspNetCore.Blazor.Performance")]
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
index 698f6ce51c..73f27914c1 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeBuilder.cs
@@ -18,6 +18,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
///
public class RenderTreeBuilder
{
+ private readonly static object BoxedTrue = true;
+ private readonly static object BoxedFalse = false;
+
private readonly Renderer _renderer;
private readonly ArrayBuilder _entries = new ArrayBuilder(10);
private readonly Stack _openElementIndices = new Stack();
@@ -97,9 +100,33 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public void AddContent(int sequence, object textContent)
=> AddContent(sequence, textContent?.ToString());
+ ///
+ /// Appends a frame representing a bool-valued attribute.
+ /// The attribute is associated with the most recently added element. If the value is false and the
+ /// current element is not a component, the frame will be omitted.
+ ///
+ /// An integer that represents the position of the instruction in the source code.
+ /// The name of the attribute.
+ /// The value of the attribute.
+ 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));
+ }
+ }
+
///
/// Appends a frame 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. If the value is null and the
+ /// current element is not a component, the frame will be omitted.
///
/// An integer that represents the position of the instruction in the source code.
/// The name of the attribute.
@@ -107,12 +134,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public void AddAttribute(int sequence, string name, string value)
{
AssertCanAddAttribute();
- Append(RenderTreeFrame.Attribute(sequence, name, value));
+ if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
+ {
+ Append(RenderTreeFrame.Attribute(sequence, name, value));
+ }
}
///
/// Appends a frame representing an -valued attribute.
- /// The attribute is associated with the most recently added element.
+ /// The attribute is associated with the most recently added element. If the value is null and the
+ /// current element is not a component, the frame will be omitted.
///
/// An integer that represents the position of the instruction in the source code.
/// The name of the attribute.
@@ -120,22 +151,49 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public void AddAttribute(int sequence, string name, UIEventHandler value)
{
AssertCanAddAttribute();
- Append(RenderTreeFrame.Attribute(sequence, name, value));
+ if (value != null || _lastNonAttributeFrameType == RenderTreeFrameType.Component)
+ {
+ Append(RenderTreeFrame.Attribute(sequence, name, value));
+ }
}
///
/// Appends a frame 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. If the value is null, or
+ /// the value false and the current element is not a component, the
+ /// frame will be omitted.
///
/// An integer that represents the position of the instruction in the source code.
/// The name of the attribute.
/// The value of the attribute.
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)
{
- // Element attribute values can only be strings or UIEventHandler
- Append(RenderTreeFrame.Attribute(sequence, name, value.ToString()));
+ if (value == null)
+ {
+ // Do nothing, treat 'null' attribute values for elements as a conditional attribute.
+ }
+ else if (value is bool boolValue)
+ {
+ if (boolValue)
+ {
+ Append(RenderTreeFrame.Attribute(sequence, name, BoxedTrue));
+ }
+
+ // Don't add anything for false bool value.
+ }
+ else if (value is UIEventHandler eventHandler)
+ {
+ 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)
{
diff --git a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs
index 12952e2945..31828e4da2 100644
--- a/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs
+++ b/src/Microsoft.AspNetCore.Blazor/RenderTree/RenderTreeDiffBuilder.cs
@@ -2,6 +2,7 @@
// 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 Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Rendering;
@@ -138,6 +139,125 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
+ // 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.
+ // Put another way, the attributes list of an element or component is *conceptually*
+ // unordered. This is a case where we can produce a more minimal diff by avoiding
+ // non-meaningful reorderings of attributes.
+ private static void AppendAttributeDiffEntriesForRange(
+ ref DiffContext diffContext,
+ int oldStartIndex, int oldEndIndexExcl,
+ int newStartIndex, int newEndIndexExcl)
+ {
+ // The overhead of the dictionary used by AppendAttributeDiffEntriesForRangeSlow is
+ // significant, so we want to try and do a merge-join if possible, but fall back to
+ // a hash-join if not. We'll do a merge join until we hit a case we can't handle and
+ // then fall back to the slow path.
+ //
+ // Also since duplicate attributes are not legal, we don't need to care about loops or
+ // the more complicated scenarios handled by AppendDiffEntriesForRange.
+ //
+ // We also assume that we won't see an attribute occur with different sequence numbers
+ // in the old and new sequences. It will be handled correct, but will generate a suboptimal
+ // diff.
+ var hasMoreOld = oldEndIndexExcl > oldStartIndex;
+ var hasMoreNew = newEndIndexExcl > newStartIndex;
+ var oldTree = diffContext.OldTree;
+ var newTree = diffContext.NewTree;
+
+ while (hasMoreOld || hasMoreNew)
+ {
+ var oldSeq = hasMoreOld ? oldTree[oldStartIndex].Sequence : int.MaxValue;
+ var newSeq = hasMoreNew ? newTree[newStartIndex].Sequence : int.MaxValue;
+ var oldAttributeName = oldTree[oldStartIndex].AttributeName;
+ var newAttributeName = newTree[newStartIndex].AttributeName;
+
+ if (oldSeq == newSeq &&
+ string.Equals(oldAttributeName, newAttributeName, StringComparison.Ordinal))
+ {
+ // These two attributes have the same sequence and name. Keep merging.
+ AppendDiffEntriesForAttributeFrame(ref diffContext, oldStartIndex, newStartIndex);
+
+ oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
+ newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
+ hasMoreOld = oldEndIndexExcl > oldStartIndex;
+ hasMoreNew = newEndIndexExcl > newStartIndex;
+ }
+ else if (oldSeq < newSeq)
+ {
+ // An attribute was removed compared to the old sequence.
+ RemoveOldFrame(ref diffContext, oldStartIndex);
+
+ oldStartIndex = NextSiblingIndex(oldTree[oldStartIndex], oldStartIndex);
+ hasMoreOld = oldEndIndexExcl > oldStartIndex;
+ }
+ else if (oldSeq > newSeq)
+ {
+ // An attribute was added compared to the new sequence.
+ InsertNewFrame(ref diffContext, newStartIndex);
+
+ newStartIndex = NextSiblingIndex(newTree[newStartIndex], newStartIndex);
+ hasMoreNew = newEndIndexExcl > newStartIndex;
+ }
+ else
+ {
+ // These two attributes have the same sequence and different names. This is
+ // a failure case for merge-join, fall back to the slow path.
+ AppendAttributeDiffEntriesForRangeSlow(
+ ref diffContext,
+ oldStartIndex, oldEndIndexExcl,
+ newStartIndex, newEndIndexExcl);
+ return;
+ }
+ }
+ }
+
+ private static void AppendAttributeDiffEntriesForRangeSlow(
+ ref DiffContext diffContext,
+ int oldStartIndex, int oldEndIndexExcl,
+ int newStartIndex, int newEndIndexExcl)
+ {
+ var oldTree = diffContext.OldTree;
+ var newTree = diffContext.NewTree;
+
+ // Slow version of AppendAttributeDiffEntriesForRange that uses a dictionary.
+ // Algorithm:
+ //
+ // 1. iterate through the 'new' tree and add all attributes to the attributes set
+ // 2. iterate through the 'old' tree, removing matching attributes from set, and diffing
+ // 3. iterate through the remaining attributes in the set and add them
+ for (var i = newStartIndex; i < newEndIndexExcl; i++)
+ {
+ diffContext.AttributeDiffSet[newTree[i].AttributeName] = i;
+ }
+
+ for (var i = oldStartIndex; i < oldEndIndexExcl; i++)
+ {
+ var oldName = oldTree[i].AttributeName;
+ if (diffContext.AttributeDiffSet.TryGetValue(oldName, out var matchIndex))
+ {
+ // Has a match in the new tree, look for a diff
+ AppendDiffEntriesForAttributeFrame(ref diffContext, i, matchIndex);
+ diffContext.AttributeDiffSet.Remove(oldName);
+ }
+ else
+ {
+ // No match in the new tree, remove old attribute
+ RemoveOldFrame(ref diffContext, i);
+ }
+ }
+
+ foreach (var kvp in diffContext.AttributeDiffSet)
+ {
+ // No match in the old tree
+ InsertNewFrame(ref diffContext, kvp.Value);
+ }
+
+ // We should have processed any additions at this point. Reset for the next batch.
+ diffContext.AttributeDiffSet.Clear();
+ }
+
private static void UpdateRetainedChildComponent(
ref DiffContext diffContext,
int oldComponentIndex,
@@ -219,7 +339,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
var newFrameAttributesEndIndexExcl = GetAttributesEndIndexExclusive(newTree, newFrameIndex);
// Diff the attributes
- AppendDiffEntriesForRange(
+ AppendAttributeDiffEntriesForRange(
ref diffContext,
oldFrameIndex + 1, oldFrameAttributesEndIndexExcl,
newFrameIndex + 1, newFrameAttributesEndIndexExcl);
@@ -284,46 +404,46 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break;
}
- case RenderTreeFrameType.Attribute:
- {
- var oldName = oldFrame.AttributeName;
- var newName = newFrame.AttributeName;
- if (string.Equals(oldName, newName, StringComparison.Ordinal))
- {
- // Using Equals to account for string comparisons, nulls, etc.
- var valueChanged = !Equals(oldFrame.AttributeValue, newFrame.AttributeValue);
- if (valueChanged)
- {
- if (oldFrame.AttributeEventHandlerId > 0)
- {
- diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId);
- }
- InitializeNewAttributeFrame(ref diffContext, ref newFrame);
- var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
- diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
- }
- else if (oldFrame.AttributeEventHandlerId > 0)
- {
- // Retain the event handler ID
- newFrame = oldFrame;
- }
- }
- 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
- RemoveOldFrame(ref diffContext, oldFrameIndex);
- InsertNewFrame(ref diffContext, newFrameIndex);
- }
- break;
- }
-
+ // We don't handle attributes here, they have their own diff logic.
+ // See AppendDiffEntriesForAttributeFrame
default:
throw new NotImplementedException($"Encountered unsupported frame type during diffing: {newTree[newFrameIndex].FrameType}");
}
}
+ // This should only be called for attributes that have the same name. This is an
+ // invariant maintained by the callers.
+ private static void AppendDiffEntriesForAttributeFrame(
+ ref DiffContext diffContext,
+ int oldFrameIndex,
+ int newFrameIndex)
+ {
+ var oldTree = diffContext.OldTree;
+ var newTree = diffContext.NewTree;
+ ref var oldFrame = ref oldTree[oldFrameIndex];
+ ref var newFrame = ref newTree[newFrameIndex];
+
+ // Using Equals to account for string comparisons, nulls, etc.
+ var valueChanged = !Equals(oldFrame.AttributeValue, newFrame.AttributeValue);
+ if (valueChanged)
+ {
+ if (oldFrame.AttributeEventHandlerId > 0)
+ {
+ diffContext.BatchBuilder.DisposedEventHandlerIds.Append(oldFrame.AttributeEventHandlerId);
+ }
+ InitializeNewAttributeFrame(ref diffContext, ref newFrame);
+ var referenceFrameIndex = diffContext.ReferenceFrames.Append(newFrame);
+ diffContext.Edits.Append(RenderTreeEdit.SetAttribute(diffContext.SiblingIndex, referenceFrameIndex));
+ }
+ else if (oldFrame.AttributeEventHandlerId > 0)
+ {
+ // Retain the event handler ID by copying the old frame over the new frame.
+ // this will prevent us from needing to dispose the old event handler
+ // since it was unchanged.
+ newFrame = oldFrame;
+ }
+ }
+
private static void InsertNewFrame(ref DiffContext diffContext, int newFrameIndex)
{
var newTree = diffContext.NewTree;
@@ -514,6 +634,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public readonly RenderTreeFrame[] NewTree;
public readonly ArrayBuilder Edits;
public readonly ArrayBuilder ReferenceFrames;
+ public readonly Dictionary AttributeDiffSet;
public int SiblingIndex;
public DiffContext(
@@ -528,6 +649,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
NewTree = newTree;
Edits = batchBuilder.EditsBuffer;
ReferenceFrames = batchBuilder.ReferenceFramesBuffer;
+ AttributeDiffSet = batchBuilder.AttributeDiffSet;
SiblingIndex = 0;
}
}
diff --git a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs
index e380cc753e..9892e6d7d0 100644
--- a/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Rendering/RenderBatchBuilder.cs
@@ -27,6 +27,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
public Queue ComponentRenderQueue { get; } = new Queue();
public Queue ComponentDisposalQueue { get; } = new Queue();
+ // Scratch data structure for understanding attribute diffs.
+ public Dictionary AttributeDiffSet { get; } = new Dictionary();
+
public void Clear()
{
EditsBuffer.Clear();
@@ -35,6 +38,7 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
UpdatedComponentDiffs.Clear();
DisposedComponentIds.Clear();
DisposedEventHandlerIds.Clear();
+ AttributeDiffSet.Clear();
}
public RenderBatch ToBatch()
diff --git a/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs b/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs
index 84c8f03d47..8277f076f5 100644
--- a/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Routing/NavLink.cs
@@ -106,7 +106,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
builder.AddAttribute(0, "class", CombineWithSpace(_cssClass, _isActive ? "active" : null));
// Pass through all other attributes unchanged
- foreach (var kvp in _allAttributes.Where(kvp => kvp.Key != "class"))
+ foreach (var kvp in _allAttributes.Where(kvp => kvp.Key != "class" && kvp.Key != nameof(RenderTreeBuilder.ChildContent)))
{
builder.AddAttribute(0, kvp.Key, kvp.Value);
}
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs
index 94785c1d7e..47e8434ec2 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/BindRazorIntegrationTest.cs
@@ -379,9 +379,8 @@ namespace Test
// Assert
Assert.Collection(
frames,
- frame => AssertFrame.Element(frame, "input", 4, 0),
+ frame => AssertFrame.Element(frame, "input", 3, 0),
frame => AssertFrame.Attribute(frame, "type", "checkbox", 1),
- frame => AssertFrame.Attribute(frame, "value", "False", 2),
frame => AssertFrame.Attribute(frame, "onchange", typeof(UIEventHandler), 3),
frame => AssertFrame.Whitespace(frame, 4));
}
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs
index 45b9dc40b3..d11ee173be 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RenderingRazorIntegrationTest.cs
@@ -480,7 +480,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Element(frame, "input", 3, 0),
- frame => AssertFrame.Attribute(frame, "value", "True", 1),
+ frame => AssertFrame.Attribute(frame, "value", true, 1),
frame =>
{
AssertFrame.Attribute(frame, "onchange", 2);
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs
index aa111caef1..7da569fdc1 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeBuilderTest.cs
@@ -8,6 +8,7 @@ using Microsoft.AspNetCore.Blazor.Test.Helpers;
using System;
using System.Linq;
using Xunit;
+using Xunit.Extensions;
namespace Microsoft.AspNetCore.Blazor.Test
{
@@ -386,6 +387,326 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Empty(builder.GetFrames());
}
+ [Fact]
+ public void AddAttribute_Element_BoolTrue_AddsFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", true);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", true, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_BoolFalse_IgnoresFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", false);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 1, 0));
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void AddAttribute_Component_Bool_SetsAttributeValue(bool value)
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, "attr", value);
+ builder.CloseComponent();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Component(frame, 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", value, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_StringValue_AddsFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", "hi");
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", "hi", 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_StringNull_IgnoresFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", (string)null);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 1, 0));
+ }
+
+ [Theory]
+ [InlineData("hi")]
+ [InlineData(null)]
+ public void AddAttribute_Component_StringValue_SetsAttributeValue(string value)
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, "attr", value);
+ builder.CloseComponent();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Component(frame, 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", value, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_EventHandler_AddsFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ var value = new UIEventHandler((e) => { });
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", value);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", value, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_NullEventHandler_IgnoresFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", (UIEventHandler)null);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 1, 0));
+ }
+
+ public static TheoryData UIEventHandlerValues => new TheoryData
+ {
+ null,
+ (e) => { },
+ };
+
+ [Theory]
+ [MemberData(nameof(UIEventHandlerValues))]
+ public void AddAttribute_Component_EventHandlerValue_SetsAttributeValue(UIEventHandler value)
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, "attr", value);
+ builder.CloseComponent();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Component(frame, 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", value, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_ObjectBoolTrue_AddsFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", (object)true);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", true, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_ObjectBoolFalse_IgnoresFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", (object)false);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 1, 0));
+ }
+
+ [Theory]
+ [InlineData(false)]
+ [InlineData(true)]
+ public void AddAttribute_Component_ObjectBoolValue_SetsAttributeValue(bool value)
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, "attr", (object)value);
+ builder.CloseComponent();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Component(frame, 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", value, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_ObjectStringValue_AddsFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", (object)"hi");
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", "hi", 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Component_ObjectStringValue_SetsAttributeValue()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, "attr", (object)"hi");
+ builder.CloseComponent();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Component(frame, 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", "hi", 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_ObjectEventHandler_AddsFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ var value = new UIEventHandler((e) => { });
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", (object)value);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", value, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Component_ObjectEventHandleValue_SetsAttributeValue()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ var value = new UIEventHandler((e) => { });
+
+ // Act
+ builder.OpenComponent(0);
+ builder.AddAttribute(1, "attr", (object)value);
+ builder.CloseComponent();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Component(frame, 2, 0),
+ frame => AssertFrame.Attribute(frame, "attr", value, 1));
+ }
+
+ [Fact]
+ public void AddAttribute_Element_ObjectNull_IgnoresFrame()
+ {
+ // Arrange
+ var builder = new RenderTreeBuilder(new TestRenderer());
+
+ // Act
+ builder.OpenElement(0, "elem");
+ builder.AddAttribute(1, "attr", (object)null);
+ builder.CloseElement();
+
+ // Assert
+ Assert.Collection(
+ builder.GetFrames(),
+ frame => AssertFrame.Element(frame, "elem", 1, 0));
+ }
+
private class TestComponent : IComponent
{
public void Init(RenderHandle renderHandle) { }
diff --git a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
index 75a7191756..61b3992fdf 100644
--- a/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Test/RenderTreeDiffBuilderTest.cs
@@ -475,6 +475,352 @@ namespace Microsoft.AspNetCore.Blazor.Test
AssertFrame.Attribute(referenceFrames[0], "newname", "same value");
}
+ [Fact]
+ public void AttributeDiff_WithSameSequenceNumber_AttributeAddedAtStart()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(0, "attr2", "value2");
+ oldTree.AddAttribute(0, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(0, "attr1", "value1");
+ newTree.AddAttribute(0, "attr2", "value2");
+ newTree.AddAttribute(0, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
+ Assert.Equal(0, entry.ReferenceFrameIndex);
+ });
+ Assert.Collection(
+ referenceFrames,
+ frame => AssertFrame.Attribute(frame, "attr1", 0));
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSameSequenceNumber_AttributeAddedInMiddle()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(0, "attr1", "value1");
+ oldTree.AddAttribute(0, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(0, "attr1", "value1");
+ newTree.AddAttribute(0, "attr2", "value2");
+ newTree.AddAttribute(0, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
+ Assert.Equal(0, entry.ReferenceFrameIndex);
+ });
+
+ Assert.Collection(
+ referenceFrames,
+ frame => AssertFrame.Attribute(frame, "attr2", 0));
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSameSequenceNumber_AttributeAddedAtEnd()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(0, "attr1", "value1");
+ oldTree.AddAttribute(0, "attr2", "value2");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(0, "attr1", "value1");
+ newTree.AddAttribute(0, "attr2", "value2");
+ newTree.AddAttribute(0, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
+ Assert.Equal(0, entry.ReferenceFrameIndex);
+ });
+
+ Assert.Collection(
+ referenceFrames,
+ frame => AssertFrame.Attribute(frame, "attr3", 0));
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSequentialSequenceNumber_AttributeAddedAtStart()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(2, "attr2", "value2");
+ oldTree.AddAttribute(3, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(1, "attr1", "value1");
+ newTree.AddAttribute(2, "attr2", "value2");
+ newTree.AddAttribute(3, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
+ Assert.Equal(0, entry.ReferenceFrameIndex);
+ });
+ Assert.Collection(
+ referenceFrames,
+ frame => AssertFrame.Attribute(frame, "attr1", 1));
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSequentialSequenceNumber_AttributeAddedInMiddle()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(1, "attr1", "value1");
+ oldTree.AddAttribute(3, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(1, "attr1", "value1");
+ newTree.AddAttribute(2, "attr2", "value2");
+ newTree.AddAttribute(3, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
+ Assert.Equal(0, entry.ReferenceFrameIndex);
+ });
+
+ Assert.Collection(
+ referenceFrames,
+ frame => AssertFrame.Attribute(frame, "attr2", 2));
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSequentialSequenceNumber_AttributeAddedAtEnd()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(1, "attr1", "value1");
+ oldTree.AddAttribute(2, "attr2", "value2");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(1, "attr1", "value1");
+ newTree.AddAttribute(2, "attr2", "value2");
+ newTree.AddAttribute(3, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.SetAttribute, 0);
+ Assert.Equal(0, entry.ReferenceFrameIndex);
+ });
+
+ Assert.Collection(
+ referenceFrames,
+ frame => AssertFrame.Attribute(frame, "attr3", 3));
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSameSequenceNumber_AttributeRemovedAtStart()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(0, "attr1", "value1");
+ oldTree.AddAttribute(0, "attr2", "value2");
+ oldTree.AddAttribute(0, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(0, "attr2", "value2");
+ newTree.AddAttribute(0, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0);
+ Assert.Equal("attr1", entry.RemovedAttributeName);
+ });
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSameSequenceNumber_AttributeRemovedInMiddle()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(0, "attr1", "value1");
+ oldTree.AddAttribute(0, "attr2", "value2");
+ oldTree.AddAttribute(0, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(0, "attr1", "value1");
+ newTree.AddAttribute(0, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0);
+ Assert.Equal("attr2", entry.RemovedAttributeName);
+ });
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSameSequenceNumber_AttributeRemovedAtEnd()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(0, "attr1", "value1");
+ oldTree.AddAttribute(0, "attr2", "value2");
+ oldTree.AddAttribute(0, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(0, "attr1", "value1");
+ newTree.AddAttribute(0, "attr2", "value2");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0);
+ Assert.Equal("attr3", entry.RemovedAttributeName);
+ });
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSequentialSequenceNumber_AttributeRemovedAtStart()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(1, "attr1", "value1");
+ oldTree.AddAttribute(2, "attr2", "value2");
+ oldTree.AddAttribute(3, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(2, "attr2", "value2");
+ newTree.AddAttribute(3, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0);
+ Assert.Equal("attr1", entry.RemovedAttributeName);
+ });
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSequentialSequenceNumber_AttributeRemovedInMiddle()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(1, "attr1", "value1");
+ oldTree.AddAttribute(2, "attr2", "value2");
+ oldTree.AddAttribute(3, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(1, "attr1", "value1");
+ newTree.AddAttribute(3, "attr3", "value3");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0);
+ Assert.Equal("attr2", entry.RemovedAttributeName);
+ });
+ }
+
+ [Fact]
+ public void AttributeDiff_WithSequentialSequenceNumber_AttributeRemovedAtEnd()
+ {
+ // Arrange
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(1, "attr1", "value1");
+ oldTree.AddAttribute(2, "attr2", "value2");
+ oldTree.AddAttribute(3, "attr3", "value3");
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(1, "attr1", "value1");
+ newTree.AddAttribute(2, "attr2", "value2");
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent();
+
+ // Assert
+ Assert.Collection(
+ result.Edits,
+ entry =>
+ {
+ AssertEdit(entry, RenderTreeEditType.RemoveAttribute, 0);
+ Assert.Equal("attr3", entry.RemovedAttributeName);
+ });
+ }
+
[Fact]
public void DiffsElementsHierarchically()
{
@@ -804,6 +1150,57 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Same(originalFakeComponent2Instance, newFrame2.Component);
}
+ [Fact]
+ public void PreservesEventHandlerIdsForRetainedEventHandlers()
+ {
+ // Arrange
+ UIEventHandler retainedHandler = _ => { };
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(1, "will remain", retainedHandler);
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(1, "will remain", retainedHandler);
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent(initializeFromFrames: true);
+ var oldAttributeFrame = oldTree.GetFrames().Array[1];
+ var newAttributeFrame = newTree.GetFrames().Array[1];
+
+ // Assert
+ Assert.Empty(result.Edits);
+ AssertFrame.Attribute(oldAttributeFrame, "will remain", retainedHandler);
+ AssertFrame.Attribute(newAttributeFrame, "will remain", retainedHandler);
+ Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId);
+ Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId);
+ }
+
+ [Fact]
+ public void PreservesEventHandlerIdsForRetainedEventHandlers_SlowPath()
+ {
+ // Arrange
+ UIEventHandler retainedHandler = _ => { };
+ oldTree.OpenElement(0, "My element");
+ oldTree.AddAttribute(0, "will remain", retainedHandler);
+ oldTree.CloseElement();
+ newTree.OpenElement(0, "My element");
+ newTree.AddAttribute(0, "another-attribute", "go down the slow path please");
+ newTree.AddAttribute(0, "will remain", retainedHandler);
+ newTree.CloseElement();
+
+ // Act
+ var (result, referenceFrames) = GetSingleUpdatedComponent(initializeFromFrames: true);
+ var oldAttributeFrame = oldTree.GetFrames().Array[1];
+ var newAttributeFrame = newTree.GetFrames().Array[2];
+
+ // Assert
+ Assert.Single(result.Edits);
+ AssertFrame.Attribute(oldAttributeFrame, "will remain", retainedHandler);
+ AssertFrame.Attribute(newAttributeFrame, "will remain", retainedHandler);
+ Assert.NotEqual(0, oldAttributeFrame.AttributeEventHandlerId);
+ Assert.Equal(oldAttributeFrame.AttributeEventHandlerId, newAttributeFrame.AttributeEventHandlerId);
+ }
+
[Fact]
public void SetsUpdatedParametersOnChildComponents()
{