Add support for conditional attributes

Adds conditional attributes for HTML elements.

This means that an attribute with a 'false' .NET bool value or a null
.NET value of another type will not be rendered in the HTML.
This commit is contained in:
Ryan Nowak 2018-03-29 23:31:40 -07:00 committed by Ryan Nowak
parent ff5e6a78c3
commit d097190824
19 changed files with 1181 additions and 50 deletions

3
.gitignore vendored
View File

@ -5,4 +5,5 @@ obj/
launchSettings.json
artifacts/
msbuild.binlog
.vscode/
.vscode/
BenchmarkDotNet.Artifacts/

View File

@ -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}

View File

@ -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]

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<OutputType>Exe</OutputType>
<ServerGarbageCollection>true</ServerGarbageCollection>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor\Microsoft.AspNetCore.Blazor.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="$(BenchmarkDotNetPackageVersion)" />
<PackageReference Include="Microsoft.AspNetCore.BenchmarkRunner.Sources" Version="$(MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion)" />
</ItemGroup>
</Project>

View File

@ -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);
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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 -- <benchmark_name>
```
## Troubleshooting
The runner will create logs in the `<project>\BenchmarkDotNet.Artifacts` directory. That should include a lot more information
than what gets printed to the console.
## Results
Also in the `<project>\BenchmarkDotNet.Artifacts\results` directive you'll find some markdown-formatted tables suitable for posting
in a github comment.

View File

@ -3,7 +3,9 @@
<MSBuildAllProjects>$(MSBuildAllProjects);$(MSBuildThisFileFullPath)</MSBuildAllProjects>
</PropertyGroup>
<PropertyGroup Label="Package Versions">
<BenchmarkDotNetPackageVersion>0.10.13</BenchmarkDotNetPackageVersion>
<InternalAspNetCoreSdkPackageVersion>2.1.0-preview2-15704</InternalAspNetCoreSdkPackageVersion>
<MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>2.1.0-preview3-32064</MicrosoftAspNetCoreBenchmarkRunnerSourcesPackageVersion>
</PropertyGroup>
<Import Project="$(DotNetPackageVersionPropsPath)" Condition=" '$(DotNetPackageVersionPropsPath)' != '' " />

View File

@ -2,7 +2,7 @@
<Import Project="dependencies.props" />
<PropertyGroup>
<EnableBenchmarkValidation>false</EnableBenchmarkValidation>
<EnableBenchmarkValidation>true</EnableBenchmarkValidation>
</PropertyGroup>
<PropertyGroup>

View File

@ -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;

View File

@ -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")]

View File

@ -18,6 +18,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// </summary>
public class RenderTreeBuilder
{
private readonly static object BoxedTrue = true;
private readonly static object BoxedFalse = false;
private readonly Renderer _renderer;
private readonly ArrayBuilder<RenderTreeFrame> _entries = new ArrayBuilder<RenderTreeFrame>(10);
private readonly Stack<int> _openElementIndices = new Stack<int>();
@ -97,9 +100,33 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
public void AddContent(int sequence, object textContent)
=> AddContent(sequence, textContent?.ToString());
/// <summary>
/// Appends a frame representing a bool-valued attribute.
/// The attribute is associated with the most recently added element. If the value is <c>false</c> and the
/// current element is not a component, the frame will be omitted.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(int sequence, string name, bool value)
{
AssertCanAddAttribute();
if (_lastNonAttributeFrameType == RenderTreeFrameType.Component)
{
Append(RenderTreeFrame.Attribute(sequence, name, value ? BoxedTrue : BoxedFalse));
}
else if (value)
{
// Don't add 'false' attributes for elements. We want booleans to map to the presence
// or absence of an attribute, and false => "False" which isn't falsy in js.
Append(RenderTreeFrame.Attribute(sequence, name, BoxedTrue));
}
}
/// <summary>
/// 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 <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
@ -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));
}
}
/// <summary>
/// Appends a frame 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. If the value is <c>null</c> and the
/// current element is not a component, the frame will be omitted.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
@ -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));
}
}
/// <summary>
/// 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 <c>null</c>, or
/// the <see cref="System.Boolean" /> value <c>false</c> and the current element is not a component, the
/// frame will be omitted.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="name">The name of the attribute.</param>
/// <param name="value">The value of the attribute.</param>
public void AddAttribute(int sequence, string name, object value)
{
// This looks a bit daunting because we need to handle the boxed/object version of all of the
// types that AddAttribute special cases.
if (_lastNonAttributeFrameType == RenderTreeFrameType.Element)
{
// 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)
{

View File

@ -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<RenderTreeEdit> Edits;
public readonly ArrayBuilder<RenderTreeFrame> ReferenceFrames;
public readonly Dictionary<string, int> 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;
}
}

View File

@ -27,6 +27,9 @@ namespace Microsoft.AspNetCore.Blazor.Rendering
public Queue<RenderQueueEntry> ComponentRenderQueue { get; } = new Queue<RenderQueueEntry>();
public Queue<int> ComponentDisposalQueue { get; } = new Queue<int>();
// Scratch data structure for understanding attribute diffs.
public Dictionary<string, int> AttributeDiffSet { get; } = new Dictionary<string, int>();
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()

View File

@ -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);
}

View File

@ -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));
}

View File

@ -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);

View File

@ -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<TestComponent>(0);
builder.AddAttribute(1, "attr", value);
builder.CloseComponent();
// Assert
Assert.Collection(
builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(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<TestComponent>(0);
builder.AddAttribute(1, "attr", value);
builder.CloseComponent();
// Assert
Assert.Collection(
builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(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<UIEventHandler> UIEventHandlerValues => new TheoryData<UIEventHandler>
{
null,
(e) => { },
};
[Theory]
[MemberData(nameof(UIEventHandlerValues))]
public void AddAttribute_Component_EventHandlerValue_SetsAttributeValue(UIEventHandler value)
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act
builder.OpenComponent<TestComponent>(0);
builder.AddAttribute(1, "attr", value);
builder.CloseComponent();
// Assert
Assert.Collection(
builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(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<TestComponent>(0);
builder.AddAttribute(1, "attr", (object)value);
builder.CloseComponent();
// Assert
Assert.Collection(
builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(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<TestComponent>(0);
builder.AddAttribute(1, "attr", (object)"hi");
builder.CloseComponent();
// Assert
Assert.Collection(
builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(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<TestComponent>(0);
builder.AddAttribute(1, "attr", (object)value);
builder.CloseComponent();
// Assert
Assert.Collection(
builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(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) { }

View File

@ -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()
{