Support 'ref' syntax for capturing references to elements and components (#685)

This commit is contained in:
Steve Sanderson 2018-04-27 17:41:21 +01:00 committed by GitHub
parent a700fa945e
commit 4033560734
61 changed files with 2181 additions and 18 deletions

View File

@ -1,11 +1,14 @@
import { platform } from '../Environment';
import { System_String } from '../Platform/Platform';
import { getRegisteredFunction } from './RegisteredFunction';
import { getElementByCaptureId } from '../Rendering/ElementReferenceCapture';
const elementRefKey = '_blazorElementRef'; // Keep in sync with ElementRef.cs
export function invokeWithJsonMarshalling(identifier: System_String, ...argsJson: System_String[]) {
const identifierJsString = platform.toJavaScriptString(identifier);
const funcInstance = getRegisteredFunction(identifierJsString);
const args = argsJson.map(json => JSON.parse(platform.toJavaScriptString(json)));
const args = argsJson.map(json => JSON.parse(platform.toJavaScriptString(json), jsonReviver));
const result = funcInstance.apply(null, args);
if (result !== null && result !== undefined) {
const resultJson = JSON.stringify(result);
@ -14,3 +17,11 @@ export function invokeWithJsonMarshalling(identifier: System_String, ...argsJson
return null;
}
}
function jsonReviver(key: string, value: any): any {
if (value && typeof value === 'object' && value.hasOwnProperty(elementRefKey) && typeof value[elementRefKey] === 'number') {
return getElementByCaptureId(value[elementRefKey]);
}
return value;
}

View File

@ -5,6 +5,7 @@ import { platform } from '../Environment';
import { EventDelegator } from './EventDelegator';
import { EventForDotNet, UIEventArgs } from './EventForDotNet';
import { LogicalElement, toLogicalElement, insertLogicalChild, removeLogicalChild, getLogicalParent, getLogicalChild, createAndInsertLogicalContainer, isSvgElement } from './LogicalElements';
import { applyCaptureIdToElement } from './ElementReferenceCapture';
const selectValuePropname = '_blazorSelectValue';
let raiseEventMethod: MethodHandle;
let renderComponentMethod: MethodHandle;
@ -138,6 +139,13 @@ export class BrowserRenderer {
return 1;
case FrameType.region:
return this.insertFrameRange(componentId, parent, childIndex, frames, frameIndex + 1, frameIndex + renderTreeFrame.subtreeLength(frame));
case FrameType.elementReferenceCapture:
if (parent instanceof Element) {
applyCaptureIdToElement(parent, renderTreeFrame.elementReferenceCaptureId(frame));
return 0; // A "capture" is a child in the diff, but has no node in the DOM
} else {
throw new Error('Reference capture frames can only be children of element frames.');
}
default:
const unknownType: never = frameType; // Compile-time verification that the switch was exhaustive
throw new Error(`Unknown frame type: ${unknownType}`);
@ -252,16 +260,27 @@ export class BrowserRenderer {
childIndex += numChildrenInserted;
// Skip over any descendants, since they are already dealt with recursively
const subtreeLength = renderTreeFrame.subtreeLength(frame);
if (subtreeLength > 1) {
index += subtreeLength - 1;
}
index += countDescendantFrames(frame);
}
return (childIndex - origChildIndex); // Total number of children inserted
}
}
function countDescendantFrames(frame: RenderTreeFramePointer): number {
switch (renderTreeFrame.frameType(frame)) {
// The following frame types have a subtree length. Other frames may use that memory slot
// to mean something else, so we must not read it. We should consider having nominal subtypes
// of RenderTreeFramePointer that prevent access to non-applicable fields.
case FrameType.component:
case FrameType.element:
case FrameType.region:
return renderTreeFrame.subtreeLength(frame) - 1;
default:
return 0;
}
}
function isCheckbox(element: Element) {
return element.tagName === 'INPUT' && element.getAttribute('type') === 'checkbox';
}

View File

@ -0,0 +1,12 @@
export function applyCaptureIdToElement(element: Element, referenceCaptureId: number) {
element.setAttribute(getCaptureIdAttributeName(referenceCaptureId), '');
}
export function getElementByCaptureId(referenceCaptureId: number) {
const selector = `[${getCaptureIdAttributeName(referenceCaptureId)}]`;
return document.querySelector(selector);
}
function getCaptureIdAttributeName(referenceCaptureId: number) {
return `_bl_${referenceCaptureId}`;
}

View File

@ -14,6 +14,7 @@ export const renderTreeFrame = {
// The properties and memory layout must be kept in sync with the .NET equivalent in RenderTreeFrame.cs
frameType: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 4) as FrameType,
subtreeLength: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 8) as FrameType,
elementReferenceCaptureId: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 8),
componentId: (frame: RenderTreeFramePointer) => platform.readInt32Field(frame, 12),
elementName: (frame: RenderTreeFramePointer) => platform.readStringField(frame, 16),
textContent: (frame: RenderTreeFramePointer) => platform.readStringField(frame, 16),
@ -29,6 +30,7 @@ export enum FrameType {
attribute = 3,
component = 4,
region = 5,
elementReferenceCapture = 6,
}
// Nominal type to ensure only valid pointers are passed to the renderTreeFrame functions.

View File

@ -54,6 +54,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string AddAttribute = nameof(AddAttribute);
public static readonly string AddElementReferenceCapture = nameof(AddElementReferenceCapture);
public static readonly string AddComponentReferenceCapture = nameof(AddComponentReferenceCapture);
public static readonly string Clear = nameof(Clear);
public static readonly string GetFrames = nameof(GetFrames);
@ -91,5 +95,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.EventHandlerAttribute";
}
public static class ElementRef
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.ElementRef";
}
}
}

View File

@ -482,6 +482,39 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
}
public override void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode refNode)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (refNode == null)
{
throw new ArgumentNullException(nameof(refNode));
}
// The runtime node writer moves the call elsewhere. At design time we
// just want sufficiently similar code that any unknown-identifier or type
// errors will be equivalent
var captureTypeName = refNode.IsComponentCapture
? refNode.ComponentCaptureTypeName
: BlazorApi.ElementRef.FullTypeName;
WriteCSharpCode(context, new CSharpCodeIntermediateNode
{
Source = refNode.Source,
Children =
{
refNode.IdentifierToken,
new IntermediateToken
{
Kind = TokenKind.CSharp,
Content = $" = default({captureTypeName});"
}
}
});
}
private void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token)
{
if (string.IsNullOrWhiteSpace(token.Content))

View File

@ -76,11 +76,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.Features.Add(new BindLoweringPass());
builder.Features.Add(new EventHandlerLoweringPass());
builder.Features.Add(new ComponentLoweringPass());
builder.Features.Add(new RefLoweringPass());
builder.Features.Add(new OrphanTagHelperLoweringPass());
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
builder.Features.Add(new BindTagHelperDescriptorProvider());
builder.Features.Add(new EventHandlerTagHelperDescriptorProvider());
builder.Features.Add(new RefTagHelperDescriptorProvider());
if (isDeclarationOnlyCompile)
{

View File

@ -43,5 +43,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public readonly static string TagHelperKind = "Blazor.EventHandler";
}
public static class Ref
{
public readonly static string TagHelperKind = "Blazor.Ref";
public static readonly string RuntimeName = "Blazor.None";
}
}
}

View File

@ -27,5 +27,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public abstract void WriteComponentBody(CodeRenderingContext context, ComponentBodyExtensionNode node);
public abstract void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node);
public abstract void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node);
}
}

View File

@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
private string _unconsumedHtml;
private List<IntermediateToken> _currentAttributeValues;
private IDictionary<string, PendingAttribute> _currentElementAttributes = new Dictionary<string, PendingAttribute>();
private List<RefExtensionNode> _currentElementRefCaptures = new List<RefExtensionNode>();
private int _sourceSequence = 0;
private struct PendingAttribute
@ -269,6 +270,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor
_currentElementAttributes.Clear();
}
if (_currentElementRefCaptures.Count > 0)
{
foreach (var refNode in _currentElementRefCaptures)
{
WriteAddReferenceCaptureCall(context, refNode);
}
_currentElementRefCaptures.Clear();
}
_scopeStack.OpenScope( tagName: nextTag.Data, isComponent: false);
}
@ -306,6 +316,39 @@ namespace Microsoft.AspNetCore.Blazor.Razor
}
}
private void WriteAddReferenceCaptureCall(CodeRenderingContext context, RefExtensionNode refNode)
{
var codeWriter = context.CodeWriter;
var methodName = refNode.IsComponentCapture
? nameof(BlazorApi.RenderTreeBuilder.AddComponentReferenceCapture)
: nameof(BlazorApi.RenderTreeBuilder.AddElementReferenceCapture);
codeWriter
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{methodName}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator();
const string refCaptureParamName = "__value";
using (var lambdaScope = codeWriter.BuildLambda(refCaptureParamName))
{
var typecastIfNeeded = refNode.IsComponentCapture ? $"({refNode.ComponentCaptureTypeName})" : string.Empty;
WriteCSharpCode(context, new CSharpCodeIntermediateNode
{
Source = refNode.Source,
Children =
{
refNode.IdentifierToken,
new IntermediateToken {
Kind = TokenKind.CSharp,
Content = $" = {typecastIfNeeded}{refCaptureParamName};"
}
}
});
}
codeWriter.WriteEndMethodInvocation();
}
private void RejectDisallowedHtmlTags(IntermediateNode node, HtmlTagToken tagToken)
{
// Disallow <script> in components as per #552
@ -484,6 +527,26 @@ namespace Microsoft.AspNetCore.Blazor.Razor
context.CodeWriter.WriteLine();
}
public override void WriteReferenceCapture(CodeRenderingContext context, RefExtensionNode node)
{
if (node.IsComponentCapture)
{
// AddComponentReferenceCapture calls can be inserted directly because they are
// already in the desired place in the IR
WriteAddReferenceCaptureCall(context, node);
}
else
{
// AddElementReferenceCapture calls must appear in the RenderTree as children of the element
// being captured, so store the RefCapture until we're ready to write its children.
// We could instead modify RefLoweringPass to insert the RefExtensionNode at the right
// place, but this is harder because it would involve understanding which HtmlContent
// node represents the end of the tag. It's simpler to do it here because here we are
// already doing HTML parsing to understand the structure.
_currentElementRefCaptures.Add(node);
}
}
private SourceSpan? CalculateSourcePosition(
SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition)

View File

@ -0,0 +1,64 @@
// 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 Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class RefExtensionNode : ExtensionIntermediateNode
{
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
public IntermediateToken IdentifierToken { get; }
public bool IsComponentCapture { get; }
public string ComponentCaptureTypeName { get; }
public RefExtensionNode(IntermediateToken identifierToken)
{
IdentifierToken = identifierToken ?? throw new ArgumentNullException(nameof(identifierToken));
Source = IdentifierToken.Source;
}
public RefExtensionNode(IntermediateToken identifierToken, string componentCaptureTypeName)
: this(identifierToken)
{
if (string.IsNullOrEmpty(componentCaptureTypeName))
{
throw new ArgumentException("Cannot be null or empty", nameof(componentCaptureTypeName));
}
IsComponentCapture = true;
ComponentCaptureTypeName = componentCaptureTypeName;
}
public override void Accept(IntermediateNodeVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
AcceptExtensionNode<RefExtensionNode>(this, visitor);
}
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
{
if (target == null)
{
throw new ArgumentNullException(nameof(target));
}
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var writer = (BlazorNodeWriter)context.NodeWriter;
writer.WriteReferenceCapture(context, this);
}
}
}

View File

@ -0,0 +1,117 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class RefLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
{
// Run after our other passes
public override int Order => 1000;
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
{
var @namespace = documentNode.FindPrimaryNamespace();
var @class = documentNode.FindPrimaryClass();
if (@namespace == null || @class == null)
{
// Nothing to do, bail. We can't function without the standard structure.
return;
}
var nodes = documentNode.FindDescendantNodes<TagHelperIntermediateNode>();
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
for (var j = node.Children.Count - 1; j >= 0; j--)
{
var attributeNode = node.Children[j] as ComponentAttributeExtensionNode;
if (attributeNode != null &&
attributeNode.TagHelper != null &&
attributeNode.TagHelper.IsRefTagHelper())
{
RewriteUsage(@class, node, j, attributeNode);
}
}
}
}
private void RewriteUsage(ClassDeclarationIntermediateNode classNode, TagHelperIntermediateNode node, int index, ComponentAttributeExtensionNode attributeNode)
{
// If we can't get a nonempty attribute name, do nothing because there will
// already be a diagnostic for empty values
var identifierToken = DetermineIdentifierToken(attributeNode);
if (identifierToken != null)
{
node.Children.Remove(attributeNode);
// Determine whether this is an element capture or a component capture, and
// if applicable the type name that will appear in the resulting capture code
var componentTagHelper = node.TagHelpers.FirstOrDefault(x => x.IsComponentTagHelper());
if (componentTagHelper != null)
{
// For components, the RefExtensionNode must go after all ComponentAttributeExtensionNode
// and ComponentBodyExtensionNode siblings because they translate to AddAttribute calls.
// We can therefore put it immediately before the ComponentCloseExtensionNode.
var componentCloseNodePosition = LastIndexOf(node.Children, n => n is ComponentCloseExtensionNode);
if (componentCloseNodePosition < 0)
{
// Should never happen - would imply we're running the lowering passes in the wrong order
throw new InvalidOperationException($"Cannot find {nameof(ComponentCloseExtensionNode)} among ref node siblings.");
}
var refExtensionNode = new RefExtensionNode(identifierToken, componentTagHelper.GetTypeName());
node.Children.Insert(componentCloseNodePosition, refExtensionNode);
}
else
{
// For elements, it doesn't matter how the RefExtensionNode is positioned
// among the children, as the node writer takes care of emitting the
// code at the right point after the AddAttribute calls
node.Children.Add(new RefExtensionNode(identifierToken));
}
}
}
private IntermediateToken DetermineIdentifierToken(ComponentAttributeExtensionNode attributeNode)
{
IntermediateToken foundToken = null;
if (attributeNode.Children.Count == 1)
{
if (attributeNode.Children[0] is IntermediateToken token)
{
foundToken = token;
}
else if (attributeNode.Children[0] is CSharpExpressionIntermediateNode csharpNode)
{
if (csharpNode.Children.Count == 1)
{
foundToken = csharpNode.Children[0] as IntermediateToken;
}
}
}
return !string.IsNullOrWhiteSpace(foundToken?.Content) ? foundToken : null;
}
private static int LastIndexOf<T>(IList<T> items, Predicate<T> predicate)
{
for (var index = items.Count - 1; index >= 0; index--)
{
if (predicate(items[index]))
{
return index;
}
}
return -1;
}
}
}

View File

@ -0,0 +1,61 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using System;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class RefTagHelperDescriptorProvider : ITagHelperDescriptorProvider
{
// Run after the component tag helper provider, because later we may want component-type-specific variants of this
public int Order { get; set; } = 1000;
public RazorEngine Engine { get; set; }
public void Execute(TagHelperDescriptorProviderContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
context.Results.Add(CreateRefTagHelper());
}
private TagHelperDescriptor CreateRefTagHelper()
{
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.Ref.TagHelperKind, "Ref", BlazorApi.AssemblyName);
builder.Documentation = Resources.RefTagHelper_Documentation;
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.Ref.TagHelperKind);
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.Ref.RuntimeName;
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
builder.SetTypeName("Microsoft.AspNetCore.Blazor.Components.Ref");
builder.TagMatchingRule(rule =>
{
rule.TagName = "*";
rule.Attribute(attribute =>
{
attribute.Name = "ref";
});
});
builder.BindAttribute(attribute =>
{
attribute.Documentation = Resources.RefTagHelper_Documentation;
attribute.Name = "ref";
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
// a C# property will crash trying to create the toolips.
attribute.SetPropertyName("Ref");
attribute.TypeName = typeof(object).FullName;
});
return builder.Build();
}
}
}

View File

@ -203,5 +203,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor {
return ResourceManager.GetString("PageDirectiveCannotBeImported", resourceCulture);
}
}
/// <summary>
/// Looks up a localized string similar to Populates the specified field or property with a reference to the element or component..
/// </summary>
internal static string RefTagHelper_Documentation {
get {
return ResourceManager.GetString("RefTagHelper_Documentation", resourceCulture);
}
}
}
}

View File

@ -165,4 +165,7 @@
<data name="PageDirective_RouteToken_Name" xml:space="preserve">
<value>route template</value>
</data>
<data name="RefTagHelper_Documentation" xml:space="preserve">
<value>Populates the specified field or property with a reference to the element or component.</value>
</data>
</root>

View File

@ -103,6 +103,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
string.Equals(BlazorMetadata.EventHandler.TagHelperKind, kind);
}
public static bool IsRefTagHelper(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)
{
throw new ArgumentNullException(nameof(tagHelper));
}
return
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
string.Equals(BlazorMetadata.Ref.TagHelperKind, kind);
}
public static string GetEventArgsType(this TagHelperDescriptor tagHelper)
{
if (tagHelper == null)

View File

@ -0,0 +1,43 @@
// 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 Microsoft.AspNetCore.Blazor.Json;
using System.Collections.Generic;
using System.Threading;
namespace Microsoft.AspNetCore.Blazor
{
/// <summary>
/// Represents a reference to a rendered element.
/// </summary>
public readonly struct ElementRef : ICustomJsonSerializer
{
// Static to ensure uniqueness even if there are multiple Renderer instances
// This would not be necessary if the JS-side code maintained a lookup from capureId to Element instances,
// but we're not doing that presently as it causes more work during disposal to remove those entries
// WARNING: Once we support server-side rendering, we should check if running on the server and avoid
// populating element reference capture IDs at all, because doing so could (a) eventually
// overflow the static int, and (b) disclose information to clients about how many other
// requests the server is handling, etc. In general, as part of implementing SSR, we need to
// audit the code it calls for any use of statics.
private static int _nextId = 0;
internal int Id { get; }
private ElementRef(int id)
{
Id = id;
}
internal static ElementRef CreateWithUniqueId()
=> new ElementRef(Interlocked.Increment(ref _nextId));
object ICustomJsonSerializer.ToJsonPrimitive()
{
return new Dictionary<string, object>
{
{ "_blazorElementRef", Id }
};
}
}
}

View File

@ -0,0 +1,21 @@
// 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.
namespace Microsoft.AspNetCore.Blazor.Json
{
// This is internal because we're trying to avoid expanding JsonUtil into a sophisticated
// API. Developers who want that would be better served by using a different JSON package
// instead. Also the perf implications of the ICustomJsonSerializer approach aren't ideal
// (it forces structs to be boxed, and returning a dictionary means lots more allocations
// and boxing of any value-typed properties).
internal interface ICustomJsonSerializer
{
/// <summary>
/// Supplies a representation suitable for JSON serialization. For example, the
/// return value may be a string->object dictionary containing properties to
/// serialize, or simply a string.
/// </summary>
/// <returns></returns>
object ToJsonPrimitive();
}
}

View File

@ -66,6 +66,7 @@ using System.Globalization;
using System.Reflection;
using System.Runtime.Serialization;
using System.Text;
using Microsoft.AspNetCore.Blazor.Json;
using SimpleJson.Reflection;
// ReSharper disable LoopCanBeConvertedToQuery
@ -1497,6 +1498,8 @@ namespace SimpleJson
output = input.ToString();
else if (input is TimeSpan)
output = ((TimeSpan)input).ToString("c");
else if (input is ICustomJsonSerializer customJsonSerializer)
output = customJsonSerializer.ToJsonPrimitive();
else
{
Enum inputEnum = input as Enum;

View File

@ -21,6 +21,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
{
private readonly static object BoxedTrue = true;
private readonly static object BoxedFalse = false;
private readonly static string ComponentReferenceCaptureInvalidParentMessage = $"Component reference captures may only be added as children of frames of type {RenderTreeFrameType.Component}";
private readonly Renderer _renderer;
private readonly ArrayBuilder<RenderTreeFrame> _entries = new ArrayBuilder<RenderTreeFrame>(10);
@ -356,6 +357,43 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
entry = entry.WithComponentSubtreeLength(_entries.Count - indexOfEntryBeingClosed);
}
/// <summary>
/// Appends a frame representing an instruction to capture a reference to the parent element.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="elementReferenceCaptureAction">An action to be invoked whenever the reference value changes.</param>
public void AddElementReferenceCapture(int sequence, Action<ElementRef> elementReferenceCaptureAction)
{
if (GetCurrentParentFrameType() != RenderTreeFrameType.Element)
{
throw new InvalidOperationException($"Element reference captures may only be added as children of frames of type {RenderTreeFrameType.Element}");
}
Append(RenderTreeFrame.ElementReferenceCapture(sequence, elementReferenceCaptureAction));
}
/// <summary>
/// Appends a frame representing an instruction to capture a reference to the parent component.
/// </summary>
/// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
/// <param name="componentReferenceCaptureAction">An action to be invoked whenever the reference value changes.</param>
public void AddComponentReferenceCapture(int sequence, Action<object> componentReferenceCaptureAction)
{
var parentFrameIndex = GetCurrentParentFrameIndex();
if (!parentFrameIndex.HasValue)
{
throw new InvalidOperationException(ComponentReferenceCaptureInvalidParentMessage);
}
var parentFrameIndexValue = parentFrameIndex.Value;
if (_entries.Buffer[parentFrameIndexValue].FrameType != RenderTreeFrameType.Component)
{
throw new InvalidOperationException(ComponentReferenceCaptureInvalidParentMessage);
}
Append(RenderTreeFrame.ComponentReferenceCapture(sequence, componentReferenceCaptureAction, parentFrameIndexValue));
}
// Internal for tests
// Not public because there's no current use case for user code defining regions arbitrarily.
// Currently the sole use case for regions is when appending a RenderFragment.
@ -382,6 +420,17 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
private int? GetCurrentParentFrameIndex()
=> _openElementIndices.Count == 0 ? (int?)null : _openElementIndices.Peek();
private RenderTreeFrameType? GetCurrentParentFrameType()
{
var parentIndex = GetCurrentParentFrameIndex();
return parentIndex.HasValue
? _entries.Buffer[parentIndex.Value].FrameType
: (RenderTreeFrameType?)null;
}
/// <summary>
/// Clears the builder.
/// </summary>

View File

@ -293,11 +293,17 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
private static int NextSiblingIndex(RenderTreeFrame frame, int frameIndex)
{
var subtreeLength = frame.ElementSubtreeLength;
var distanceToNextSibling = subtreeLength == 0
? 1 // For frames that don't have a subtree length set, such as text frames
: subtreeLength; // For element or component frames
return frameIndex + distanceToNextSibling;
switch (frame.FrameType)
{
case RenderTreeFrameType.Component:
return frameIndex + frame.ComponentSubtreeLength;
case RenderTreeFrameType.Element:
return frameIndex + frame.ElementSubtreeLength;
case RenderTreeFrameType.Region:
return frameIndex + frame.RegionSubtreeLength;
default:
return frameIndex + 1;
}
}
private static void AppendDiffEntriesForFramesWithSameSequence(
@ -404,6 +410,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
break;
}
case RenderTreeFrameType.ElementReferenceCapture:
{
// We could preserve the ElementReferenceCaptureId from the old frame to the new frame,
// and even call newFrame.ElementReferenceCaptureAction(id) each time in case it wants
// to do something different with the ID. However there's no known use case for
// that, so presently the rule is that for any given element, the reference
// capture action is only invoked once.
break;
}
// We don't handle attributes here, they have their own diff logic.
// See AppendDiffEntriesForAttributeFrame
default:
@ -484,6 +500,16 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
diffContext.SiblingIndex++;
break;
}
case RenderTreeFrameType.ElementReferenceCapture:
{
InitializeNewElementReferenceCaptureFrame(ref diffContext, ref newFrame);
break;
}
case RenderTreeFrameType.ComponentReferenceCapture:
{
InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref newFrame);
break;
}
}
}
@ -573,6 +599,12 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
case RenderTreeFrameType.Attribute:
InitializeNewAttributeFrame(ref diffContext, ref frame);
break;
case RenderTreeFrameType.ElementReferenceCapture:
InitializeNewElementReferenceCaptureFrame(ref diffContext, ref frame);
break;
case RenderTreeFrameType.ComponentReferenceCapture:
InitializeNewComponentReferenceCaptureFrame(ref diffContext, ref frame);
break;
}
}
}
@ -609,6 +641,32 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
}
}
private static void InitializeNewElementReferenceCaptureFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
{
var newElementRef = ElementRef.CreateWithUniqueId();
newFrame = newFrame.WithElementReferenceCaptureId(newElementRef.Id);
newFrame.ElementReferenceCaptureAction(newElementRef);
}
private static void InitializeNewComponentReferenceCaptureFrame(ref DiffContext diffContext, ref RenderTreeFrame newFrame)
{
ref var parentFrame = ref diffContext.NewTree[newFrame.ComponentReferenceCaptureParentFrameIndex];
if (parentFrame.FrameType != RenderTreeFrameType.Component)
{
// Should never happen, but will help with diagnosis if it does
throw new InvalidOperationException($"{nameof(RenderTreeFrameType.ComponentReferenceCapture)} frame references invalid parent index.");
}
var componentInstance = parentFrame.Component;
if (componentInstance == null)
{
// Should never happen, but will help with diagnosis if it does
throw new InvalidOperationException($"Trying to initialize {nameof(RenderTreeFrameType.ComponentReferenceCapture)} frame before parent component was assigned.");
}
newFrame.ComponentReferenceCaptureAction(componentInstance);
}
private static void DisposeFramesInRange(RenderBatchBuilder batchBuilder, RenderTreeFrame[] frames, int startIndex, int endIndexExcl)
{
for (var i = startIndex; i < endIndexExcl; i++)

View File

@ -53,7 +53,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Element"/>,
/// gets a name representing the type of the element. Otherwise, the value is <see langword="null"/>.
/// gets a name representing the type of the element. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(16)] public readonly string ElementName;
@ -63,7 +63,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Text"/>,
/// gets the content of the text frame. Otherwise, the value is <see langword="null"/>.
/// gets the content of the text frame. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(16)] public readonly string TextContent;
@ -79,13 +79,13 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Attribute"/>,
/// gets the attribute name. Otherwise, the value is <see langword="null"/>.
/// gets the attribute name. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(16)] public readonly string AttributeName;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Attribute"/>,
/// gets the attribute value. Otherwise, the value is <see langword="null"/>.
/// gets the attribute value. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(24)] public readonly object AttributeValue;
@ -114,7 +114,7 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.Component"/>,
/// gets the child component instance. Otherwise, the value is <see langword="null"/>.
/// gets the child component instance. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(24)] public readonly IComponent Component;
@ -129,6 +129,44 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// </summary>
[FieldOffset(8)] public readonly int RegionSubtreeLength;
// --------------------------------------------------------------------------------
// RenderTreeFrameType.ElementReferenceCapture
// --------------------------------------------------------------------------------
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ElementReferenceCapture"/>,
/// gets the ID of the reference capture. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(8)] public readonly int ElementReferenceCaptureId;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ElementReferenceCapture"/>,
/// gets the action that writes the reference to its target. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(16)] public readonly Action<ElementRef> ElementReferenceCaptureAction;
// --------------------------------------------------------------------------------
// RenderTreeFrameType.ComponentReferenceCapture
// --------------------------------------------------------------------------------
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ComponentReferenceCapture"/>,
/// gets the index of the parent frame representing the component being captured. Otherwise, the value is undefined.
/// WARNING: This index can only be used in the context of the frame's original render tree. If the frame is
/// copied elsewhere, such as to the ReferenceFrames buffer of a RenderTreeDiff, then the index will
/// not relate to entries in that other buffer.
/// Currently there's no scenario where this matters, but if there was, we could change all of the subtree
/// initialization logic in RenderTreeDiffBuilder to walk the frames hierarchically, then it would know
/// the parent index at the point where it wants to initialize the ComponentReferenceCapture frame.
/// </summary>
[FieldOffset(8)] public readonly int ComponentReferenceCaptureParentFrameIndex;
/// <summary>
/// If the <see cref="FrameType"/> property equals <see cref="RenderTreeFrameType.ComponentReferenceCapture"/>,
/// gets the action that writes the reference to its target. Otherwise, the value is undefined.
/// </summary>
[FieldOffset(16)] public readonly Action<object> ComponentReferenceCaptureAction;
private RenderTreeFrame(int sequence, string elementName, int elementSubtreeLength)
: this()
{
@ -189,6 +227,24 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
RegionSubtreeLength = regionSubtreeLength;
}
private RenderTreeFrame(int sequence, Action<ElementRef> elementReferenceCaptureAction, int elementReferenceCaptureId)
: this()
{
FrameType = RenderTreeFrameType.ElementReferenceCapture;
Sequence = sequence;
ElementReferenceCaptureAction = elementReferenceCaptureAction;
ElementReferenceCaptureId = elementReferenceCaptureId;
}
private RenderTreeFrame(int sequence, Action<object> componentReferenceCaptureAction, int parentFrameIndex)
: this()
{
FrameType = RenderTreeFrameType.ComponentReferenceCapture;
Sequence = sequence;
ComponentReferenceCaptureAction = componentReferenceCaptureAction;
ComponentReferenceCaptureParentFrameIndex = parentFrameIndex;
}
internal static RenderTreeFrame Element(int sequence, string elementName)
=> new RenderTreeFrame(sequence, elementName: elementName, elementSubtreeLength: 0);
@ -207,6 +263,12 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal static RenderTreeFrame Region(int sequence)
=> new RenderTreeFrame(sequence, regionSubtreeLength: 0);
internal static RenderTreeFrame ElementReferenceCapture(int sequence, Action<ElementRef> elementReferenceCaptureAction)
=> new RenderTreeFrame(sequence, elementReferenceCaptureAction: elementReferenceCaptureAction, elementReferenceCaptureId: 0);
internal static RenderTreeFrame ComponentReferenceCapture(int sequence, Action<object> componentReferenceCaptureAction, int parentFrameIndex)
=> new RenderTreeFrame(sequence, componentReferenceCaptureAction: componentReferenceCaptureAction, parentFrameIndex: parentFrameIndex);
internal RenderTreeFrame WithElementSubtreeLength(int elementSubtreeLength)
=> new RenderTreeFrame(Sequence, elementName: ElementName, elementSubtreeLength: elementSubtreeLength);
@ -225,6 +287,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal RenderTreeFrame WithRegionSubtreeLength(int regionSubtreeLength)
=> new RenderTreeFrame(Sequence, regionSubtreeLength: regionSubtreeLength);
internal RenderTreeFrame WithElementReferenceCaptureId(int elementReferenceCaptureId)
=> new RenderTreeFrame(Sequence, ElementReferenceCaptureAction, elementReferenceCaptureId);
/// <inheritdoc />
// Just to be nice for debugging and unit tests.
public override string ToString()
@ -245,6 +310,9 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
case RenderTreeFrameType.Text:
return $"Text: (seq={Sequence}, len=n/a) {EscapeNewlines(TextContent)}";
case RenderTreeFrameType.ElementReferenceCapture:
return $"ElementReferenceCapture: (seq={Sequence}, len=n/a) {ElementReferenceCaptureAction}";
}
return base.ToString();

View File

@ -35,5 +35,15 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
/// fragment are not comparable to sequence numbers outside it.
/// </summary>
Region = 5,
/// <summary>
/// Represents an instruction to capture or update a reference to the parent element.
/// </summary>
ElementReferenceCapture = 6,
/// <summary>
/// Represents an instruction to capture or update a reference to the parent component.
/// </summary>
ComponentReferenceCapture = 7,
}
}

View File

@ -689,5 +689,58 @@ namespace Test
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void Element_WithRef()
{
// Arrange/Act
var generated = CompileToCSharp(@"
<elem attributebefore=""before"" ref=""myElem"" attributeafter=""after"">Hello</elem>
@functions {
Microsoft.AspNetCore.Blazor.ElementRef myElem;
void DoSomething() { myElem.GetHashCode(); } // Avoid 'assigned but not used' warning
}
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void Component_WithRef()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent ParamBefore=""before"" ref=""myInstance"" ParamAfter=""after"" />
@functions {
Test.MyComponent myInstance;
void DoSomething() { myInstance.GetHashCode(); } // Avoid 'assigned but not used' warning
}
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
}
}

View File

@ -18,7 +18,8 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
IExtensionIntermediateNodeVisitor<ComponentBodyExtensionNode>,
IExtensionIntermediateNodeVisitor<ComponentCloseExtensionNode>,
IExtensionIntermediateNodeVisitor<ComponentOpenExtensionNode>,
IExtensionIntermediateNodeVisitor<RouteAttributeExtensionNode>
IExtensionIntermediateNodeVisitor<RouteAttributeExtensionNode>,
IExtensionIntermediateNodeVisitor<RefExtensionNode>
{
private readonly TextWriter _writer;
@ -288,5 +289,10 @@ namespace Microsoft.AspNetCore.Razor.Language.IntegrationTests
{
WriteContentNode(node, node.Template);
}
void IExtensionIntermediateNodeVisitor<RefExtensionNode>.VisitExtension(RefExtensionNode node)
{
WriteContentNode(node, node.IdentifierToken.Content, node.IsComponentCapture ? node.ComponentCaptureTypeName : "Element");
}
}
}

View File

@ -942,5 +942,87 @@ namespace Test
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void Element_WithRef()
{
// Arrange/Act
var generated = CompileToCSharp(@"
<elem attributebefore=""before"" ref=""myElem"" attributeafter=""after"">Hello</elem>
@functions {
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
}
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void Component_WithRef()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Arrange/Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent ParamBefore=""before"" ref=""myInstance"" ParamAfter=""after"" />
@functions {
private Test.MyComponent myInstance;
}
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
[Fact]
public void Component_WithRef_WithChildContent()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Arrange/Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent ref=""myInstance"" SomeProp=""val"">
Some <el>further</el> content
</MyComponent>
@functions {
private Test.MyComponent myInstance;
}
");
// Assert
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
CompileToAssembly(generated);
}
}
}

View File

@ -0,0 +1,47 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
((System.Action)(() => {
global::System.Object __typeHelper = "*, TestAssembly";
}
))();
}
#pragma warning restore 219
#pragma warning disable 0414
private static System.Object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.AddAttribute(-1, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
}
));
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
myInstance = default(Test.MyComponent);
#line default
#line hidden
}
#pragma warning restore 1998
#line 4 "x:\dir\subdir\Test\TestComponent.cshtml"
Test.MyComponent myInstance;
void DoSomething() { myInstance.GetHashCode(); } // Avoid 'assigned but not used' warning
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,39 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [12] ) - System
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
UsingDirective - (53:3,1 [17] ) - System.Linq
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
DesignTimeDirective -
DirectiveToken - (14:0,14 [32] ) - "*, Microsoft.AspNetCore.Blazor"
DirectiveToken - (14:0,14 [9] ) - "*, Test"
DirectiveToken - (14:0,14 [15] x:\dir\subdir\Test\TestComponent.cshtml) - *, TestAssembly
CSharpCode -
IntermediateToken - - CSharp - #pragma warning disable 0414
CSharpCode -
IntermediateToken - - CSharp - private static System.Object __o = null;
CSharpCode -
IntermediateToken - - CSharp - #pragma warning restore 0414
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlContent - (29:0,29 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (29:0,29 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
TagHelper - (31:1,0 [72] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - TagMode.SelfClosing
ComponentOpenExtensionNode - - Test.MyComponent
ComponentAttributeExtensionNode - - ParamBefore -
HtmlContent - (57:1,26 [6] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (57:1,26 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - before
ComponentAttributeExtensionNode - - ParamAfter -
HtmlContent - (94:1,63 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (94:1,63 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - after
ComponentBodyExtensionNode -
RefExtensionNode - (70:1,39 [10] x:\dir\subdir\Test\TestComponent.cshtml) - myInstance - Test.MyComponent
ComponentCloseExtensionNode -
HtmlContent - (103:1,72 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (103:1,72 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
HtmlContent - (253:7,1 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (253:7,1 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
CSharpCode - (119:3,12 [133] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (119:3,12 [133] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n Test.MyComponent myInstance;\n\n void DoSomething() { myInstance.GetHashCode(); } // Avoid 'assigned but not used' warning\n

View File

@ -0,0 +1,23 @@
Source Location: (14:0,14 [15] x:\dir\subdir\Test\TestComponent.cshtml)
|*, TestAssembly|
Generated Location: (468:14,38 [15] )
|*, TestAssembly|
Source Location: (70:1,39 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|myInstance|
Generated Location: (1132:30,39 [10] )
|myInstance|
Source Location: (119:3,12 [133] x:\dir\subdir\Test\TestComponent.cshtml)
|
Test.MyComponent myInstance;
void DoSomething() { myInstance.GetHashCode(); } // Avoid 'assigned but not used' warning
|
Generated Location: (1316:37,12 [133] )
|
Test.MyComponent myInstance;
void DoSomething() { myInstance.GetHashCode(); } // Avoid 'assigned but not used' warning
|

View File

@ -0,0 +1,40 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 219
private void __RazorDirectiveTokenHelpers__() {
}
#pragma warning restore 219
#pragma warning disable 0414
private static System.Object __o = null;
#pragma warning restore 0414
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
myElem = default(Microsoft.AspNetCore.Blazor.ElementRef);
#line default
#line hidden
}
#pragma warning restore 1998
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
Microsoft.AspNetCore.Blazor.ElementRef myElem;
void DoSomething() { myElem.GetHashCode(); } // Avoid 'assigned but not used' warning
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,42 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [12] ) - System
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
UsingDirective - (53:3,1 [17] ) - System.Linq
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
DesignTimeDirective -
DirectiveToken - (14:0,14 [32] ) - "*, Microsoft.AspNetCore.Blazor"
DirectiveToken - (14:0,14 [9] ) - "*, Test"
CSharpCode -
IntermediateToken - - CSharp - #pragma warning disable 0414
CSharpCode -
IntermediateToken - - CSharp - private static System.Object __o = null;
CSharpCode -
IntermediateToken - - CSharp - #pragma warning restore 0414
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlContent -
IntermediateToken - - Html - <elem
HtmlContent -
IntermediateToken - - Html - attributebefore="
IntermediateToken - (23:0,23 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - before
IntermediateToken - - Html - "
HtmlContent -
IntermediateToken - - Html - attributeafter="
IntermediateToken - (60:0,60 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - after
IntermediateToken - - Html - "
RefExtensionNode - (36:0,36 [6] x:\dir\subdir\Test\TestComponent.cshtml) - myElem - Element
HtmlContent -
IntermediateToken - - Html - >
HtmlContent - (67:0,67 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (67:0,67 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlContent -
IntermediateToken - - Html - </elem>
HtmlContent - (79:0,79 [4] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (79:0,79 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n\n
HtmlContent - (243:6,1 [2] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (243:6,1 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
CSharpCode - (95:2,12 [147] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (95:2,12 [147] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n Microsoft.AspNetCore.Blazor.ElementRef myElem;\n\n void DoSomething() { myElem.GetHashCode(); } // Avoid 'assigned but not used' warning\n

View File

@ -0,0 +1,18 @@
Source Location: (36:0,36 [6] x:\dir\subdir\Test\TestComponent.cshtml)
|myElem|
Generated Location: (865:23,36 [6] )
|myElem|
Source Location: (95:2,12 [147] x:\dir\subdir\Test\TestComponent.cshtml)
|
Microsoft.AspNetCore.Blazor.ElementRef myElem;
void DoSomething() { myElem.GetHashCode(); } // Avoid 'assigned but not used' warning
|
Generated Location: (1067:30,12 [147] )
|
Microsoft.AspNetCore.Blazor.ElementRef myElem;
void DoSomething() { myElem.GetHashCode(); } // Avoid 'assigned but not used' warning
|

View File

@ -0,0 +1,38 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenComponent<Test.MyComponent>(0);
builder.AddAttribute(1, "ParamBefore", "before");
builder.AddAttribute(2, "ParamAfter", "after");
builder.AddComponentReferenceCapture(3, (__value) => {
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
myInstance = (Test.MyComponent)__value;
#line default
#line hidden
}
);
builder.CloseComponent();
}
#pragma warning restore 1998
#line 4 "x:\dir\subdir\Test\TestComponent.cshtml"
private Test.MyComponent myInstance;
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,23 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
TagHelper - (31:1,0 [72] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - TagMode.SelfClosing
ComponentOpenExtensionNode - - Test.MyComponent
ComponentAttributeExtensionNode - - ParamBefore -
HtmlContent - (57:1,26 [6] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (57:1,26 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - before
ComponentAttributeExtensionNode - - ParamAfter -
HtmlContent - (94:1,63 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (94:1,63 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - after
ComponentBodyExtensionNode -
RefExtensionNode - (70:1,39 [10] x:\dir\subdir\Test\TestComponent.cshtml) - myInstance - Test.MyComponent
ComponentCloseExtensionNode -
CSharpCode - (119:3,12 [44] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (119:3,12 [44] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private Test.MyComponent myInstance;\n

View File

@ -0,0 +1,14 @@
Source Location: (70:1,39 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|myInstance|
Generated Location: (849:20,39 [10] )
|myInstance|
Source Location: (119:3,12 [44] x:\dir\subdir\Test\TestComponent.cshtml)
|
private Test.MyComponent myInstance;
|
Generated Location: (1103:30,12 [44] )
|
private Test.MyComponent myInstance;
|

View File

@ -0,0 +1,45 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenComponent<Test.MyComponent>(0);
builder.AddAttribute(1, "SomeProp", "val");
builder.AddAttribute(2, "ChildContent", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
builder2.AddContent(3, "\n Some ");
builder2.OpenElement(4, "el");
builder2.AddContent(5, "further");
builder2.CloseElement();
builder2.AddContent(6, " content\n");
}
));
builder.AddComponentReferenceCapture(7, (__value) => {
#line 2 "x:\dir\subdir\Test\TestComponent.cshtml"
myInstance = (Test.MyComponent)__value;
#line default
#line hidden
}
);
builder.CloseComponent();
}
#pragma warning restore 1998
#line 6 "x:\dir\subdir\Test\TestComponent.cshtml"
private Test.MyComponent myInstance;
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,26 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
TagHelper - (31:1,0 [96] x:\dir\subdir\Test\TestComponent.cshtml) - MyComponent - TagMode.StartTagAndEndTag
ComponentOpenExtensionNode - - Test.MyComponent
ComponentAttributeExtensionNode - - SomeProp -
HtmlContent - (71:1,40 [3] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (71:1,40 [3] x:\dir\subdir\Test\TestComponent.cshtml) - Html - val
ComponentBodyExtensionNode -
HtmlContent - (76:1,45 [37] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (76:1,45 [11] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n Some
IntermediateToken - (87:2,9 [4] x:\dir\subdir\Test\TestComponent.cshtml) - Html - <el>
IntermediateToken - (91:2,13 [7] x:\dir\subdir\Test\TestComponent.cshtml) - Html - further
IntermediateToken - (98:2,20 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - </el>
IntermediateToken - (103:2,25 [10] x:\dir\subdir\Test\TestComponent.cshtml) - Html - content\n
RefExtensionNode - (49:1,18 [10] x:\dir\subdir\Test\TestComponent.cshtml) - myInstance - Test.MyComponent
ComponentCloseExtensionNode -
CSharpCode - (143:5,12 [44] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (143:5,12 [44] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private Test.MyComponent myInstance;\n

View File

@ -0,0 +1,14 @@
Source Location: (49:1,18 [10] x:\dir\subdir\Test\TestComponent.cshtml)
|myInstance|
Generated Location: (1160:28,18 [10] )
|myInstance|
Source Location: (143:5,12 [44] x:\dir\subdir\Test\TestComponent.cshtml)
|
private Test.MyComponent myInstance;
|
Generated Location: (1414:38,12 [44] )
|
private Test.MyComponent myInstance;
|

View File

@ -0,0 +1,39 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "elem");
builder.AddAttribute(1, "attributebefore", "before");
builder.AddAttribute(2, "attributeafter", "after");
builder.AddElementReferenceCapture(3, (__value) => {
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
myElem = __value;
#line default
#line hidden
}
);
builder.AddContent(4, "Hello");
builder.CloseElement();
}
#pragma warning restore 1998
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,29 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlContent -
IntermediateToken - - Html - <elem
HtmlContent -
IntermediateToken - - Html - attributebefore="
IntermediateToken - (23:0,23 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - before
IntermediateToken - - Html - "
HtmlContent -
IntermediateToken - - Html - attributeafter="
IntermediateToken - (60:0,60 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - after
IntermediateToken - - Html - "
RefExtensionNode - (36:0,36 [6] x:\dir\subdir\Test\TestComponent.cshtml) - myElem - Element
HtmlContent -
IntermediateToken - - Html - >
HtmlContent - (67:0,67 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (67:0,67 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlContent -
IntermediateToken - - Html - </elem>
CSharpCode - (95:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (95:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private Microsoft.AspNetCore.Blazor.ElementRef myElem;\n

View File

@ -0,0 +1,14 @@
Source Location: (36:0,36 [6] x:\dir\subdir\Test\TestComponent.cshtml)
|myElem|
Generated Location: (840:20,36 [6] )
|myElem|
Source Location: (95:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
|
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
|
Generated Location: (1115:31,12 [62] )
|
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
|

View File

@ -0,0 +1,36 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "elem");
builder.AddElementReferenceCapture(1, (__value) => {
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
@class = __value;
#line default
#line hidden
}
);
builder.CloseElement();
}
#pragma warning restore 1998
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
private Microsoft.AspNetCore.Blazor.ElementRef @class;
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,17 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlContent -
IntermediateToken - - Html - <elem
RefExtensionNode - (13:0,13 [6] x:\dir\subdir\Test\TestComponent.cshtml) - @class - Element
HtmlContent -
IntermediateToken - - Html - />
CSharpCode - (40:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (40:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private Microsoft.AspNetCore.Blazor.ElementRef @class;\n

View File

@ -0,0 +1,14 @@
Source Location: (13:0,13 [6] x:\dir\subdir\Test\TestComponent.cshtml)
|@class|
Generated Location: (685:18,13 [6] )
|@class|
Source Location: (40:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
|
private Microsoft.AspNetCore.Blazor.ElementRef @class;
|
Generated Location: (915:28,12 [62] )
|
private Microsoft.AspNetCore.Blazor.ElementRef @class;
|

View File

@ -0,0 +1,39 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "elem");
builder.AddAttribute(1, "attributebefore", "before");
builder.AddAttribute(2, "attributeafter", "after");
builder.AddElementReferenceCapture(3, (__value) => {
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
myElem = __value;
#line default
#line hidden
}
);
builder.AddContent(4, "Hello");
builder.CloseElement();
}
#pragma warning restore 1998
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,29 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlContent -
IntermediateToken - - Html - <elem
HtmlContent -
IntermediateToken - - Html - attributebefore="
IntermediateToken - (23:0,23 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - before
IntermediateToken - - Html - "
HtmlContent -
IntermediateToken - - Html - attributeafter="
IntermediateToken - (61:0,61 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - after
IntermediateToken - - Html - "
RefExtensionNode - (37:0,37 [6] x:\dir\subdir\Test\TestComponent.cshtml) - myElem - Element
HtmlContent -
IntermediateToken - - Html - >
HtmlContent - (68:0,68 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (68:0,68 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlContent -
IntermediateToken - - Html - </elem>
CSharpCode - (96:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (96:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private Microsoft.AspNetCore.Blazor.ElementRef myElem;\n

View File

@ -0,0 +1,14 @@
Source Location: (37:0,37 [6] x:\dir\subdir\Test\TestComponent.cshtml)
|myElem|
Generated Location: (841:20,37 [6] )
|myElem|
Source Location: (96:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
|
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
|
Generated Location: (1116:31,12 [62] )
|
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
|

View File

@ -0,0 +1,39 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
{
#pragma warning disable 1998
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
{
base.BuildRenderTree(builder);
builder.OpenElement(0, "elem");
builder.AddAttribute(1, "attributebefore", "before");
builder.AddAttribute(2, "attributeafter", "after");
builder.AddElementReferenceCapture(3, (__value) => {
#line 1 "x:\dir\subdir\Test\TestComponent.cshtml"
myElem = __value;
#line default
#line hidden
}
);
builder.AddContent(4, "Hello");
builder.CloseElement();
}
#pragma warning restore 1998
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
#line default
#line hidden
}
}
#pragma warning restore 1591

View File

@ -0,0 +1,29 @@
Document -
NamespaceDeclaration - - Test
UsingDirective - (3:1,1 [14] ) - System
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
UsingDirective - (53:3,1 [19] ) - System.Linq
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
MethodDeclaration - - protected override - void - BuildRenderTree
CSharpCode -
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
HtmlContent -
IntermediateToken - - Html - <elem
HtmlContent -
IntermediateToken - - Html - attributebefore="
IntermediateToken - (23:0,23 [6] x:\dir\subdir\Test\TestComponent.cshtml) - Html - before
IntermediateToken - - Html - "
HtmlContent -
IntermediateToken - - Html - attributeafter="
IntermediateToken - (60:0,60 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - after
IntermediateToken - - Html - "
RefExtensionNode - (36:0,36 [6] x:\dir\subdir\Test\TestComponent.cshtml) - myElem - Element
HtmlContent -
IntermediateToken - - Html - >
HtmlContent - (67:0,67 [5] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (67:0,67 [5] x:\dir\subdir\Test\TestComponent.cshtml) - Html - Hello
HtmlContent -
IntermediateToken - - Html - </elem>
CSharpCode - (95:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
IntermediateToken - (95:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n private Microsoft.AspNetCore.Blazor.ElementRef myElem;\n

View File

@ -0,0 +1,14 @@
Source Location: (36:0,36 [6] x:\dir\subdir\Test\TestComponent.cshtml)
|myElem|
Generated Location: (840:20,36 [6] )
|myElem|
Source Location: (95:2,12 [62] x:\dir\subdir\Test\TestComponent.cshtml)
|
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
|
Generated Location: (1115:31,12 [62] )
|
private Microsoft.AspNetCore.Blazor.ElementRef myElem;
|

View File

@ -299,5 +299,76 @@ namespace Microsoft.AspNetCore.Blazor.E2ETest.Tests
var appElement = MountTestComponent<LogicalElementInsertionCases>();
Assert.Equal("First Second Third", appElement.Text);
}
[Fact]
public void CanUseJsInteropToReferenceElements()
{
var appElement = MountTestComponent<ElementRefComponent>();
var inputElement = appElement.FindElement(By.Id("capturedElement"));
var buttonElement = appElement.FindElement(By.TagName("button"));
Assert.Equal(string.Empty, inputElement.GetAttribute("value"));
buttonElement.Click();
Assert.Equal("Clicks: 1", inputElement.GetAttribute("value"));
buttonElement.Click();
Assert.Equal("Clicks: 2", inputElement.GetAttribute("value"));
}
[Fact]
public void CanCaptureReferencesToDynamicallyAddedElements()
{
var appElement = MountTestComponent<ElementRefComponent>();
var buttonElement = appElement.FindElement(By.TagName("button"));
var checkbox = appElement.FindElement(By.CssSelector("input[type=checkbox]"));
// We're going to remove the input. But first, put in some contents
// so we can observe it's not the same instance later
appElement.FindElement(By.Id("capturedElement")).SendKeys("some text");
// Remove the captured element
checkbox.Click();
Assert.Empty(appElement.FindElements(By.Id("capturedElement")));
// Re-add it; observe it starts empty again
checkbox.Click();
var inputElement = appElement.FindElement(By.Id("capturedElement"));
Assert.NotNull(inputElement);
Assert.Equal(string.Empty, inputElement.GetAttribute("value"));
// See that the capture variable was automatically updated to reference the new instance
buttonElement.Click();
Assert.Equal("Clicks: 1", inputElement.GetAttribute("value"));
}
[Fact]
public void CanCaptureReferencesToDynamicallyAddedComponents()
{
var appElement = MountTestComponent<ComponentRefComponent>();
var incrementButtonSelector = By.CssSelector("#child-component button");
var currentCountTextSelector = By.CssSelector("#child-component p:first-of-type");
var resetButton = appElement.FindElement(By.Id("reset-child"));
var toggleChildCheckbox = appElement.FindElement(By.Id("toggle-child"));
// Verify the reference was captured initially
appElement.FindElement(incrementButtonSelector).Click();
Assert.Equal("Current count: 1", appElement.FindElement(currentCountTextSelector).Text);
resetButton.Click();
Assert.Equal("Current count: 0", appElement.FindElement(currentCountTextSelector).Text);
appElement.FindElement(incrementButtonSelector).Click();
Assert.Equal("Current count: 1", appElement.FindElement(currentCountTextSelector).Text);
// Remove and re-add a new instance of the child, checking the text was reset
toggleChildCheckbox.Click();
Assert.Empty(appElement.FindElements(incrementButtonSelector));
toggleChildCheckbox.Click();
Assert.Equal("Current count: 0", appElement.FindElement(currentCountTextSelector).Text);
// Verify we have a new working reference
appElement.FindElement(incrementButtonSelector).Click();
Assert.Equal("Current count: 1", appElement.FindElement(currentCountTextSelector).Text);
resetButton.Click();
Assert.Equal("Current count: 0", appElement.FindElement(currentCountTextSelector).Text);
}
}
}

View File

@ -0,0 +1,84 @@
// 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 Microsoft.AspNetCore.Razor.Language;
using System.Linq;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Razor.Extensions.Test
{
public class RefTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
{
[Fact]
public void Execute_CreatesDescriptor()
{
// Arrange
var context = TagHelperDescriptorProviderContext.Create();
var provider = new RefTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = context.Results.Where(result => result.IsRefTagHelper());
var item = Assert.Single(matches);
Assert.Empty(item.AllowedChildTags);
Assert.Null(item.TagOutputHint);
Assert.Empty(item.Diagnostics);
Assert.False(item.HasErrors);
Assert.Equal(BlazorMetadata.Ref.TagHelperKind, item.Kind);
Assert.Equal(BlazorMetadata.Ref.RuntimeName, item.Metadata[TagHelperMetadata.Runtime.Name]);
Assert.False(item.IsDefaultKind());
Assert.False(item.KindUsesDefaultTagHelperRuntime());
Assert.Equal(
"Populates the specified field or property with a reference to the element or component.",
item.Documentation);
Assert.Equal("Microsoft.AspNetCore.Blazor", item.AssemblyName);
Assert.Equal("Ref", item.Name);
Assert.Equal("Microsoft.AspNetCore.Blazor.Components.Ref", item.DisplayName);
Assert.Equal("Microsoft.AspNetCore.Blazor.Components.Ref", item.GetTypeName());
// The tag matching rule for a ref is just the attribute name "ref"
var rule = Assert.Single(item.TagMatchingRules);
Assert.Empty(rule.Diagnostics);
Assert.False(rule.HasErrors);
Assert.Null(rule.ParentTag);
Assert.Equal("*", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Empty(requiredAttribute.Diagnostics);
Assert.Equal("ref", requiredAttribute.DisplayName);
Assert.Equal("ref", requiredAttribute.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
Assert.Null(requiredAttribute.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
var attribute = Assert.Single(item.BoundAttributes);
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Ref.TagHelperKind, attribute.Kind);
Assert.False(attribute.IsDefaultKind());
Assert.False(attribute.HasIndexer);
Assert.Null(attribute.IndexerNamePrefix);
Assert.Null(attribute.IndexerTypeName);
Assert.False(attribute.IsIndexerBooleanProperty);
Assert.False(attribute.IsIndexerStringProperty);
Assert.Equal(
"Populates the specified field or property with a reference to the element or component.",
attribute.Documentation);
Assert.Equal("ref", attribute.Name);
Assert.Equal("Ref", attribute.GetPropertyName());
Assert.Equal("object Microsoft.AspNetCore.Blazor.Components.Ref.Ref", attribute.DisplayName);
Assert.Equal("System.Object", attribute.TypeName);
Assert.False(attribute.IsStringProperty);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
}
}
}

View File

@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Blazor.Json;
using System;
using System.Collections.Generic;
using Xunit;
@ -132,6 +133,16 @@ namespace Microsoft.AspNetCore.Blazor.Test
exception.Message);
}
[Fact]
public void SupportsInternalCustomSerializer()
{
// Arrange/Act
var json = JsonUtil.Serialize(new WithCustomSerializer());
// Asssert
Assert.Equal("{\"key1\":\"value1\",\"key2\":123}", json);
}
class NonEmptyConstructorPoco
{
public NonEmptyConstructorPoco(int parameter) {}
@ -158,5 +169,17 @@ namespace Microsoft.AspNetCore.Blazor.Test
}
enum Hobbies { Reading = 1, Swordfighting = 2 }
class WithCustomSerializer : ICustomJsonSerializer
{
public object ToJsonPrimitive()
{
return new Dictionary<string, object>
{
{ "key1", "value1" },
{ "key2", 123 },
};
}
}
}
}

View File

@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Blazor.Test.Helpers;
using System;
using System.Linq;
using Xunit;
using Xunit.Extensions;
namespace Microsoft.AspNetCore.Blazor.Test
{
@ -260,6 +259,36 @@ namespace Microsoft.AspNetCore.Blazor.Test
});
}
[Fact]
public void CannotAddAttributeToElementReferenceCapture()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.OpenElement(0, "some element");
builder.AddElementReferenceCapture(1, _ => { });
builder.AddAttribute(2, "name", "value");
});
}
[Fact]
public void CannotAddAttributeToComponentReferenceCapture()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.OpenComponent<TestComponent>(0);
builder.AddComponentReferenceCapture(1, _ => { });
builder.AddAttribute(2, "name", "value");
});
}
[Fact]
public void CanAddChildComponentsUsingGenericParam()
{
@ -370,6 +399,184 @@ namespace Microsoft.AspNetCore.Blazor.Test
frame => AssertFrame.Text(frame, "Some text", 2));
}
[Fact]
public void CanAddElementReferenceCaptureInsideElement()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
Action<ElementRef> referenceCaptureAction = elementRef => { };
// Act
builder.OpenElement(0, "myelement"); // 0: <myelement
builder.AddAttribute(1, "attribute2", 123); // 1: attribute2=intExpression123>
builder.AddElementReferenceCapture(2, referenceCaptureAction); // 2: # capture: referenceCaptureAction
builder.AddContent(3, "some text"); // 3: some text
builder.CloseElement(); // </myelement>
// Assert
Assert.Collection(builder.GetFrames(),
frame => AssertFrame.Element(frame, "myelement", 4, 0),
frame => AssertFrame.Attribute(frame, "attribute2", "123", 1),
frame => AssertFrame.ElementReferenceCapture(frame, referenceCaptureAction, 2),
frame => AssertFrame.Text(frame, "some text", 3));
}
[Fact]
public void CannotAddElementReferenceCaptureWithNoParent()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.AddElementReferenceCapture(0, _ => { });
});
}
[Fact]
public void CannotAddElementReferenceCaptureInsideComponent()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.OpenComponent<TestComponent>(0);
builder.AddElementReferenceCapture(1, _ => { });
});
}
[Fact]
public void CannotAddElementReferenceCaptureInsideRegion()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.OpenRegion(0);
builder.AddElementReferenceCapture(1, _ => { });
});
}
[Fact]
public void CanAddMultipleReferenceCapturesToSameElement()
{
// There won't be any way of doing this from Razor because there's no known use
// case for it. However it's harder to *not* support it than to support it, and
// there's no known reason to prevent it, so here's test coverage to show it
// just works.
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
Action<ElementRef> referenceCaptureAction1 = elementRef => { };
Action<ElementRef> referenceCaptureAction2 = elementRef => { };
// Act
builder.OpenElement(0, "myelement");
builder.AddElementReferenceCapture(0, referenceCaptureAction1);
builder.AddElementReferenceCapture(0, referenceCaptureAction2);
builder.CloseElement();
// Assert
Assert.Collection(builder.GetFrames(),
frame => AssertFrame.Element(frame, "myelement", 3),
frame => AssertFrame.ElementReferenceCapture(frame, referenceCaptureAction1),
frame => AssertFrame.ElementReferenceCapture(frame, referenceCaptureAction2));
}
[Fact]
public void CanAddComponentReferenceCaptureInsideComponent()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
Action<object> myAction = elementRef => { };
// Act
builder.OpenComponent<TestComponent>(0); // 0: <TestComponent
builder.AddAttribute(1, "attribute2", 123); // 1: attribute2=intExpression123>
builder.AddComponentReferenceCapture(2, myAction); // 2: # capture: myAction
builder.AddContent(3, "some text"); // 3: some text
builder.CloseComponent(); // </TestComponent>
// Assert
Assert.Collection(builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(frame, 4, 0),
frame => AssertFrame.Attribute(frame, "attribute2", 123, 1),
frame => AssertFrame.ComponentReferenceCapture(frame, myAction, 2),
frame => AssertFrame.Text(frame, "some text", 3));
}
[Fact]
public void CannotAddComponentReferenceCaptureWithNoParent()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.AddComponentReferenceCapture(0, _ => { });
});
}
[Fact]
public void CannotAddComponentReferenceCaptureInsideElement()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.OpenElement(0, "myelement");
builder.AddComponentReferenceCapture(1, _ => { });
});
}
[Fact]
public void CannotAddComponentReferenceCaptureInsideRegion()
{
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
// Act/Assert
Assert.Throws<InvalidOperationException>(() =>
{
builder.OpenRegion(0);
builder.AddComponentReferenceCapture(1, _ => { });
});
}
[Fact]
public void CanAddMultipleReferenceCapturesToSameComponent()
{
// There won't be any way of doing this from Razor because there's no known use
// case for it. However it's harder to *not* support it than to support it, and
// there's no known reason to prevent it, so here's test coverage to show it
// just works.
// Arrange
var builder = new RenderTreeBuilder(new TestRenderer());
Action<object> referenceCaptureAction1 = elementRef => { };
Action<object> referenceCaptureAction2 = elementRef => { };
// Act
builder.OpenComponent<TestComponent>(0);
builder.AddComponentReferenceCapture(0, referenceCaptureAction1);
builder.AddComponentReferenceCapture(0, referenceCaptureAction2);
builder.CloseComponent();
// Assert
Assert.Collection(builder.GetFrames(),
frame => AssertFrame.Component<TestComponent>(frame, 3),
frame => AssertFrame.ComponentReferenceCapture(frame, referenceCaptureAction1),
frame => AssertFrame.ComponentReferenceCapture(frame, referenceCaptureAction2));
}
[Fact]
public void CanClear()
{

View File

@ -44,7 +44,11 @@ namespace Microsoft.AspNetCore.Blazor.Test
=> new RenderFragment[]
{
builder => builder.AddContent(0, "Hello"),
builder => builder.OpenElement(0, "Some Element"),
builder =>
{
builder.OpenElement(0, "Some Element");
builder.CloseElement();
},
builder =>
{
builder.OpenElement(0, "Some Element");
@ -1323,6 +1327,137 @@ namespace Microsoft.AspNetCore.Blazor.Test
Assert.Equal(new[] { 0, 1 }, batchBuilder.ComponentDisposalQueue);
}
[Fact]
public void AssignsDistinctIdToNewElementReferenceCaptures()
{
// Arrange
ElementRef ref1 = default, ref2 = default;
Action<ElementRef> capture1 = val => { ref1 = val; };
Action<ElementRef> capture2 = val => { ref2 = val; };
newTree.OpenElement(0, "My element");
newTree.AddElementReferenceCapture(1, capture1);
newTree.AddElementReferenceCapture(2, capture2);
newTree.CloseElement();
// Act
var (diff, referenceFrames) = GetSingleUpdatedComponent();
// Assert: Distinct nonzero IDs
Assert.NotEqual(0, ref1.Id);
Assert.NotEqual(0, ref2.Id);
Assert.NotEqual(ref1.Id, ref2.Id);
// Assert: Also specified in diff
Assert.Collection(diff.Edits, edit =>
{
AssertEdit(edit, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(referenceFrames,
frame => AssertFrame.Element(frame, "My element", 3),
frame =>
{
AssertFrame.ElementReferenceCapture(frame, capture1);
Assert.Equal(ref1.Id, frame.ElementReferenceCaptureId);
},
frame =>
{
AssertFrame.ElementReferenceCapture(frame, capture2);
Assert.Equal(ref2.Id, frame.ElementReferenceCaptureId);
});
}
[Fact]
public void PreservesIdsOnRetainedElementReferenceCaptures()
{
// Arrange
var refWriteCount = 0;
ElementRef ref1 = default;
Action<ElementRef> capture1 = val => { ref1 = val; refWriteCount++; };
oldTree.OpenElement(0, "My element");
oldTree.AddElementReferenceCapture(1, capture1);
oldTree.CloseElement();
newTree.OpenElement(0, "My element");
newTree.AddElementReferenceCapture(1, capture1);
newTree.CloseElement();
// Act
var (diff, referenceFrames) = GetSingleUpdatedComponent(initializeFromFrames: true);
// Assert: Did not invoke the capture action a second time
// Note: We're not preserving the ReferenceCaptureId on the actual RenderTreeFrames in the same
// way we do for event handler IDs, simply because there's no need to do so. We only do
// anything with ReferenceCaptureId when frames are first inserted into the document.
Assert.NotEqual(0, ref1.Id);
Assert.Equal(1, refWriteCount);
Assert.Empty(diff.Edits);
Assert.Empty(referenceFrames);
}
[Fact]
public void InvokesAssignerForComponentReferenceCapturesOnInsertion()
{
// Arrange
FakeComponent capturedInstance1 = null, capturedInstance2 = null;
Action<object> assigner1 = val => { capturedInstance1 = (FakeComponent)val; };
Action<object> assigner2 = val => { capturedInstance2 = (FakeComponent)val; };
newTree.OpenComponent<FakeComponent>(0);
newTree.AddComponentReferenceCapture(1, assigner1);
newTree.AddComponentReferenceCapture(2, assigner2);
newTree.CloseComponent();
// Act
var (diff, referenceFrames) = GetSingleUpdatedComponent();
// Assert: Assigned references
Assert.NotNull(capturedInstance1);
Assert.NotNull(capturedInstance2);
Assert.IsType<FakeComponent>(capturedInstance1);
Assert.IsType<FakeComponent>(capturedInstance2);
Assert.Same(capturedInstance1, capturedInstance2);
// Assert: Also in diff, even though we have no use for it there
// (it would be costly to exclude given how the array range is copied)
Assert.Collection(diff.Edits, edit =>
{
AssertEdit(edit, RenderTreeEditType.PrependFrame, 0);
Assert.Equal(0, edit.ReferenceFrameIndex);
});
Assert.Collection(referenceFrames,
frame =>
{
AssertFrame.Component<FakeComponent>(frame, 3, 0);
Assert.Same(capturedInstance1, frame.Component);
},
frame => AssertFrame.ComponentReferenceCapture(frame, assigner1, 1),
frame => AssertFrame.ComponentReferenceCapture(frame, assigner2, 2));
}
[Fact]
public void DoesNotInvokeAssignerAgainForRetainedComponents()
{
// Arrange
var refWriteCount = 0;
FakeComponent capturedInstance = null;
Action<object> assigner = val => { capturedInstance = (FakeComponent)val; refWriteCount++; };
oldTree.OpenComponent<FakeComponent>(0);
oldTree.AddComponentReferenceCapture(1, assigner);
oldTree.CloseComponent();
newTree.OpenComponent<FakeComponent>(0);
newTree.AddComponentReferenceCapture(1, assigner);
newTree.CloseComponent();
// Act
var (diff, referenceFrames) = GetSingleUpdatedComponent(initializeFromFrames: true);
// Assert: Did not invoke the capture action a second time
Assert.NotNull(capturedInstance);
Assert.IsType<FakeComponent>(capturedInstance);
Assert.Equal(1, refWriteCount);
Assert.Empty(diff.Edits);
Assert.Empty(referenceFrames);
}
private (RenderTreeDiff, RenderTreeFrame[]) GetSingleUpdatedComponent(bool initializeFromFrames = false)
{
var result = GetSingleUpdatedComponentWithBatch(initializeFromFrames);

View File

@ -950,6 +950,47 @@ namespace Microsoft.AspNetCore.Blazor.Test
&& edit.RemovedAttributeName == "disabled");
}
[Fact]
public void HandlesNestedElementCapturesDuringRefresh()
{
// This may seem like a very arbitrary test case, but at once stage there was a bug
// whereby the diff output was incorrect given a ref capture on an element whose
// parent element also had a ref capture
// Arrange
var attrValue = 0;
var component = new TestComponent(builder =>
{
builder.OpenElement(0, "parent elem");
builder.AddAttribute(1, "parent elem attr", attrValue);
builder.AddElementReferenceCapture(2, _ => { });
builder.OpenElement(3, "child elem");
builder.AddElementReferenceCapture(4, _ => { });
builder.AddContent(5, "child text");
builder.CloseElement();
builder.CloseElement();
});
var renderer = new TestRenderer();
renderer.AssignComponentId(component);
// Act: Update the attribute value on the parent
component.TriggerRender();
attrValue++;
component.TriggerRender();
// Assert
var latestBatch = renderer.Batches.Skip(1).Single();
var latestDiff = latestBatch.DiffsInOrder.Single();
Assert.Collection(latestDiff.Edits,
edit =>
{
Assert.Equal(RenderTreeEditType.SetAttribute, edit.Type);
Assert.Equal(0, edit.SiblingIndex);
AssertFrame.Attribute(latestBatch.ReferenceFrames[edit.ReferenceFrameIndex],
"parent elem attr", 1);
});
}
private class NoOpRenderer : Renderer
{
public NoOpRenderer() : base(new TestServiceProvider())

View File

@ -107,5 +107,19 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
AssertFrame.Sequence(frame, sequence);
Assert.True(string.IsNullOrWhiteSpace(frame.TextContent));
}
public static void ElementReferenceCapture(RenderTreeFrame frame, Action<ElementRef> action, int? sequence = null)
{
Assert.Equal(RenderTreeFrameType.ElementReferenceCapture, frame.FrameType);
Assert.Same(action, frame.ElementReferenceCaptureAction);
AssertFrame.Sequence(frame, sequence);
}
public static void ComponentReferenceCapture(RenderTreeFrame frame, Action<object> action, int? sequence = null)
{
Assert.Equal(RenderTreeFrameType.ComponentReferenceCapture, frame.FrameType);
Assert.Same(action, frame.ComponentReferenceCaptureAction);
AssertFrame.Sequence(frame, sequence);
}
}
}

View File

@ -0,0 +1,37 @@
@using Microsoft.AspNetCore.Blazor
<h1>Component capture</h1>
<p>
This shows how a component reference may be captured as a field value using 'ref' syntax.
This feature is intended only for cases where you're triggering an action on the child
instance. It should <strong>not</strong> be used as a way of mutating state in the child,
because that would bypass all the benefits of flowing parameters to children and re-rendering
automatically only when required.
</p>
@if (_toggleCapturedComponentPresence)
{
<div id="child-component">
<CounterComponent ref="_myChildCounter" />
</div>
}
<fieldset>
<legend>External controls</legend>
<button id="reset-child" onclick="@ResetChildCounter">Reset</button>
<label>
<input id="toggle-child" type="checkbox" bind="_toggleCapturedComponentPresence" />
Toggle counter presence
</label>
</fieldset>
@functions {
bool _toggleCapturedComponentPresence = true;
CounterComponent _myChildCounter;
void ResetChildCounter()
{
_myChildCounter.Reset();
}
}

View File

@ -15,4 +15,10 @@
{
currentCount++;
}
public void Reset()
{
currentCount = 0;
StateHasChanged();
}
}

View File

@ -0,0 +1,37 @@
@using Microsoft.AspNetCore.Blazor
@using Microsoft.AspNetCore.Blazor.Browser.Interop
<h1>Element capture</h1>
<p>
This shows how an element reference may be captured as a field value using 'ref' syntax and then
passed to JavaScript code, which receives the actual DOM element instance.
</p>
<p>
Note that 'ref' syntax is primarily intended for use with JavaScript interop. It is <strong>not</strong>
recommended to use it for mutating the DOM routinely. All DOM construction and mutation that can be
done declaratively is better, as it automatically happens at the correct time and with minimal diffs.
Plus, whenever you use 'ref', you will not be able to run the same code during unit tests or
server-side rendering. So it's always better to prefer declarative UI construction when possible.
</p>
@if (_toggleCapturedElementPresence)
{
<input id="capturedElement" ref="_myInput" />
}
<button onclick="@MakeInteropCall">Click me</button>
<label>
<input type="checkbox" bind="_toggleCapturedElementPresence" />
Toggle input
</label>
@functions {
int _count = 0;
bool _toggleCapturedElementPresence = true;
ElementRef _myInput;
void MakeInteropCall()
{
RegisteredFunction.Invoke<object>("setElementValue", _myInput, $"Clicks: {++_count}");
}
}

View File

@ -29,6 +29,8 @@
<option value="BasicTestApp.SvgComponent">SVG</option>
<option value="BasicTestApp.SvgWithChildComponent">SVG with child component</option>
<option value="BasicTestApp.LogicalElementInsertionCases">Logical element insertion cases</option>
<option value="BasicTestApp.ElementRefComponent">Element ref component</option>
<option value="BasicTestApp.ComponentRefComponent">Component ref component</option>
<!--<option value="BasicTestApp.RouterTest.Default">Router</option> Excluded because it requires additional setup to work correctly when loaded manually -->
</select>
&nbsp;
@ -54,6 +56,11 @@
var method = Blazor.platform.findMethod('BasicTestApp', 'BasicTestApp', 'Program', 'MountTestComponent');
Blazor.platform.callMethod(method, null, [Blazor.platform.toDotNetString(typeName)]);
}
// Used by ElementRefComponent
Blazor.registerFunction('setElementValue', function (element, newValue) {
element.value = newValue;
});
</script>
</body>
</html>