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:
parent
ff5e6a78c3
commit
d097190824
|
|
@ -5,4 +5,5 @@ obj/
|
|||
launchSettings.json
|
||||
artifacts/
|
||||
msbuild.binlog
|
||||
.vscode/
|
||||
.vscode/
|
||||
BenchmarkDotNet.Artifacts/
|
||||
13
Blazor.sln
13
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}
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
@ -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>
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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.
|
||||
|
|
@ -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)' != '' " />
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<Import Project="dependencies.props" />
|
||||
|
||||
<PropertyGroup>
|
||||
<EnableBenchmarkValidation>false</EnableBenchmarkValidation>
|
||||
<EnableBenchmarkValidation>true</EnableBenchmarkValidation>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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) { }
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in New Issue