diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs index 781a6e0b5b..9084f43a4e 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Block.cs @@ -11,6 +11,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { internal class Block : SyntaxTreeNode { + private int? _length; + public Block(BlockBuilder source) : this(source.Type, source.Children, source.ChunkGenerator) { @@ -58,7 +60,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy } } - public override int Length => Children.Sum(child => child.Length); + public override int Length + { + get + { + if (_length == null) + { + var length = 0; + for (var i = 0; i < Children.Count; i++) + { + length += Children[i].Length; + } + + _length = length; + } + + return _length.Value; + } + } + public virtual IEnumerable Flatten() { @@ -214,6 +234,14 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy visitor.VisitBlock(this); } + internal void ChildChanged() + { + // A node in our graph has changed. We'll need to recompute our length the next time we're asked for it. + _length = null; + + Parent?.ChildChanged(); + } + private class EquivalenceComparer : IEqualityComparer { public static readonly EquivalenceComparer Default = new EquivalenceComparer(); diff --git a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs index 2e7a04189c..880e8ee6f6 100644 --- a/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs +++ b/src/Microsoft.AspNetCore.Razor.Language/Legacy/Span.cs @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { private static readonly int TypeHashCode = typeof(Span).GetHashCode(); private string _content; + private int? _length; private SourceLocation _start; public Span(SpanBuilder builder) @@ -32,7 +33,31 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy public override bool IsBlock => false; - public override int Length => Content.Length; + public override int Length + { + get + { + if (_length == null) + { + var length = 0; + if (_content == null) + { + for (var i = 0; i < Symbols.Count; i++) + { + length += Symbols[i].Content.Length; + } + } + else + { + length = _content.Length; + } + + _length = length; + } + + return _length.Value; + } + } public override SourceLocation Start => _start; @@ -79,6 +104,9 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy ChunkGenerator = builder.ChunkGenerator ?? SpanChunkGenerator.Null; _start = builder.Start; _content = null; + _length = null; + + Parent?.ChildChanged(); // Since we took references to the values in SpanBuilder, clear its references out builder.Reset(); diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs index aa826bb323..dc57fb1147 100644 --- a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/BlockTest.cs @@ -8,6 +8,42 @@ namespace Microsoft.AspNetCore.Razor.Language.Legacy { public class BlockTest { + [Fact] + public void ChildChanged_NotifiesParent() + { + // Arrange + var spanBuilder = new SpanBuilder(SourceLocation.Zero); + spanBuilder.Accept(new HtmlSymbol("hello", HtmlSymbolType.Text)); + var span = spanBuilder.Build(); + var blockBuilder = new BlockBuilder() + { + Type = BlockKindInternal.Markup, + }; + blockBuilder.Children.Add(span); + var childBlock = blockBuilder.Build(); + blockBuilder = new BlockBuilder() + { + Type = BlockKindInternal.Markup, + }; + blockBuilder.Children.Add(childBlock); + var parentBlock = blockBuilder.Build(); + var originalBlockLength = parentBlock.Length; + spanBuilder = new SpanBuilder(SourceLocation.Zero); + spanBuilder.Accept(new HtmlSymbol("hi", HtmlSymbolType.Text)); + span.ReplaceWith(spanBuilder); + + // Wire up parents now so we can re-trigger ChildChanged to cause cache refresh. + span.Parent = childBlock; + childBlock.Parent = parentBlock; + + // Act + childBlock.ChildChanged(); + + // Assert + Assert.Equal(5, originalBlockLength); + Assert.Equal(2, parentBlock.Length); + } + [Fact] public void ConstructorWithBlockBuilderSetsParent() { diff --git a/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/SpanTest.cs b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/SpanTest.cs new file mode 100644 index 0000000000..4c2ee94e41 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Language.Test/Legacy/SpanTest.cs @@ -0,0 +1,58 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Razor.Language.Legacy +{ + public class SpanTest + { + [Fact] + public void ReplaceWith_ResetsLength() + { + // Arrange + var builder = new SpanBuilder(SourceLocation.Zero); + builder.Accept(new HtmlSymbol("hello", HtmlSymbolType.Text)); + var span = builder.Build(); + var newBuilder = new SpanBuilder(SourceLocation.Zero); + newBuilder.Accept(new HtmlSymbol("hi", HtmlSymbolType.Text)); + var originalLength = span.Length; + + // Act + span.ReplaceWith(newBuilder); + + // Assert + Assert.Equal(5, originalLength); + Assert.Equal(2, span.Length); + } + + + // Note: This is more of an integration-like test. However, it's valuable to determine + // that the Span's ReplaceWith code is properly propogating change notifications to parents. + [Fact] + public void ReplaceWith_NotifiesParentChildHasChanged() + { + // Arrange + var spanBuilder = new SpanBuilder(SourceLocation.Zero); + spanBuilder.Accept(new HtmlSymbol("hello", HtmlSymbolType.Text)); + var span = spanBuilder.Build(); + var blockBuilder = new BlockBuilder() + { + Type = BlockKindInternal.Markup, + }; + blockBuilder.Children.Add(span); + var block = blockBuilder.Build(); + span.Parent = block; + var originalBlockLength = block.Length; + var newSpanBuilder = new SpanBuilder(SourceLocation.Zero); + newSpanBuilder.Accept(new HtmlSymbol("hi", HtmlSymbolType.Text)); + + // Act + span.ReplaceWith(newSpanBuilder); + + // Assert + Assert.Equal(5, originalBlockLength); + Assert.Equal(2, block.Length); + } + } +}