Implement components as tag helpers

Implements Component code generation and tooling support end to end
udditionally adds some default `@addTagHelper` directives to make
programming in Blazor a little nicer.

Components are discovered as Tag Helpers using Razor's extensibility
during the build/IDE process. This drives the code generation during
build and lights up a bunch of editor features.

Add
This commit is contained in:
Ryan Nowak 2018-03-01 21:22:57 -08:00 committed by Steve Sanderson
parent b47e3095ee
commit 601e7914f7
30 changed files with 2729 additions and 180 deletions

View File

@ -39,7 +39,7 @@
</RazorExtension> </RazorExtension>
<!-- Path used for the temporary compilation we produce for component discovery --> <!-- Path used for the temporary compilation we produce for component discovery -->
<_BlazorTempAssembly Include="$(IntermediateOutputPath)$(TargetName).BlazorTemp.dll" /> <_BlazorTempAssembly Include="$(IntermediateOutputPath)BlazorTemp\$(TargetName).dll" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
@ -294,6 +294,8 @@
Outputs="@(_BlazorTempAssembly);$(NonExistentFile)" Outputs="@(_BlazorTempAssembly);$(NonExistentFile)"
Condition="'$(DesignTimeBuild)'!='true'"> Condition="'$(DesignTimeBuild)'!='true'">
<MakeDir Directories="%(_BlazorTempAssembly.RelativeDir)" />
<!-- These two compiler warnings are raised when a reference is bound to a different version <!-- These two compiler warnings are raised when a reference is bound to a different version
than specified in the assembly reference version number. MSBuild raises the same warning in this case, than specified in the assembly reference version number. MSBuild raises the same warning in this case,
so the compiler warning would be redundant. --> so the compiler warning would be redundant. -->

View File

@ -46,7 +46,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string OpenComponent = nameof(OpenComponent); public static readonly string OpenComponent = nameof(OpenComponent);
public static readonly string CloseComponent = nameof(CloseElement); public static readonly string CloseComponent = nameof(CloseComponent);
public static readonly string AddContent = nameof(AddContent); public static readonly string AddContent = nameof(AddContent);
@ -65,5 +65,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string SetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValue"; public static readonly string SetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValue";
} }
public static class UIEventHandler
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.UIEventHandler";
}
} }
} }

View File

@ -9,7 +9,7 @@ using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
namespace Microsoft.AspNetCore.Blazor.Razor namespace Microsoft.AspNetCore.Blazor.Razor
{ {
/// <summary> /// <summary>
/// Directs a <see cref="DocumentWriter"/> to use <see cref="BlazorIntermediateNodeWriter"/>. /// Directs a <see cref="DocumentWriter"/> to use <see cref="BlazorRuntimeNodeWriter"/>.
/// </summary> /// </summary>
internal class BlazorCodeTarget : CodeTarget internal class BlazorCodeTarget : CodeTarget
{ {
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public override IntermediateNodeWriter CreateNodeWriter() public override IntermediateNodeWriter CreateNodeWriter()
{ {
return _options.DesignTime ? (IntermediateNodeWriter)new DesignTimeNodeWriter() : new BlazorIntermediateNodeWriter(); return _options.DesignTime ? (BlazorNodeWriter)new BlazorDesignTimeNodeWriter() : new BlazorRuntimeNodeWriter();
} }
public override TExtension GetExtension<TExtension>() public override TExtension GetExtension<TExtension>()

View File

@ -0,0 +1,499 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
// Based on the DesignTimeNodeWriter from Razor repo.
internal class BlazorDesignTimeNodeWriter : BlazorNodeWriter
{
private readonly ScopeStack _scopeStack = new ScopeStack();
private readonly static string DesignTimeVariable = "__o";
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
if (node.Source.HasValue)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
{
context.AddSourceMappingFor(node);
context.CodeWriter.WriteUsing(node.Content);
}
}
else
{
context.CodeWriter.WriteUsing(node.Content);
}
}
public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
if (node.Children.Count == 0)
{
return;
}
if (node.Source != null)
{
using (context.CodeWriter.BuildLinePragma(node.Source.Value))
{
var offset = DesignTimeVariable.Length + " = ".Length;
context.CodeWriter.WritePadding(offset, node.Source, context);
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
for (var i = 0; i < node.Children.Count; i++)
{
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
{
context.AddSourceMappingFor(token);
context.CodeWriter.Write(token.Content);
}
else
{
// There may be something else inside the expression like a Template or another extension node.
context.RenderNode(node.Children[i]);
}
}
context.CodeWriter.WriteLine(";");
}
}
else
{
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
for (var i = 0; i < node.Children.Count; i++)
{
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
{
context.CodeWriter.Write(token.Content);
}
else
{
// There may be something else inside the expression like a Template or another extension node.
context.RenderNode(node.Children[i]);
}
}
context.CodeWriter.WriteLine(";");
}
}
public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
var isWhitespaceStatement = true;
for (var i = 0; i < node.Children.Count; i++)
{
var token = node.Children[i] as IntermediateToken;
if (token == null || !string.IsNullOrWhiteSpace(token.Content))
{
isWhitespaceStatement = false;
break;
}
}
IDisposable linePragmaScope = null;
if (node.Source != null)
{
if (!isWhitespaceStatement)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(node.Source.Value);
}
context.CodeWriter.WritePadding(0, node.Source.Value, context);
}
else if (isWhitespaceStatement)
{
// Don't write whitespace if there is no line mapping for it.
return;
}
for (var i = 0; i < node.Children.Count; i++)
{
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
{
context.AddSourceMappingFor(token);
context.CodeWriter.Write(token.Content);
}
else
{
// There may be something else inside the statement like an extension node.
context.RenderNode(node.Children[i]);
}
}
if (linePragmaScope != null)
{
linePragmaScope.Dispose();
}
else
{
context.CodeWriter.WriteLine();
}
}
public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
context.RenderChildren(node);
}
public override void WriteHtmlAttributeValue(CodeRenderingContext context, HtmlAttributeValueIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
context.RenderChildren(node);
}
public override void WriteCSharpExpressionAttributeValue(CodeRenderingContext context, CSharpExpressionAttributeValueIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
if (node.Children.Count == 0)
{
return;
}
var firstChild = node.Children[0];
if (firstChild.Source != null)
{
using (context.CodeWriter.BuildLinePragma(firstChild.Source.Value))
{
var offset = DesignTimeVariable.Length + " = ".Length;
context.CodeWriter.WritePadding(offset, firstChild.Source, context);
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
for (var i = 0; i < node.Children.Count; i++)
{
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
{
context.AddSourceMappingFor(token);
context.CodeWriter.Write(token.Content);
}
else
{
// There may be something else inside the expression like a Template or another extension node.
context.RenderNode(node.Children[i]);
}
}
context.CodeWriter.WriteLine(";");
}
}
else
{
context.CodeWriter.WriteStartAssignment(DesignTimeVariable);
for (var i = 0; i < node.Children.Count; i++)
{
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
{
if (token.Source != null)
{
context.AddSourceMappingFor(token);
}
context.CodeWriter.Write(token.Content);
}
else
{
// There may be something else inside the expression like a Template or another extension node.
context.RenderNode(node.Children[i]);
}
}
context.CodeWriter.WriteLine(";");
}
}
public override void WriteCSharpCodeAttributeValue(CodeRenderingContext context, CSharpCodeAttributeValueIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
for (var i = 0; i < node.Children.Count; i++)
{
if (node.Children[i] is IntermediateToken token && token.IsCSharp)
{
IDisposable linePragmaScope = null;
var isWhitespaceStatement = string.IsNullOrWhiteSpace(token.Content);
if (token.Source != null)
{
if (!isWhitespaceStatement)
{
linePragmaScope = context.CodeWriter.BuildLinePragma(token.Source.Value);
}
context.CodeWriter.WritePadding(0, token.Source.Value, context);
}
else if (isWhitespaceStatement)
{
// Don't write whitespace if there is no line mapping for it.
continue;
}
context.AddSourceMappingFor(token);
context.CodeWriter.Write(token.Content);
if (linePragmaScope != null)
{
linePragmaScope.Dispose();
}
else
{
context.CodeWriter.WriteLine();
}
}
else
{
// There may be something else inside the statement like an extension node.
context.RenderNode(node.Children[i]);
}
}
}
public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// Do nothing
}
public override void BeginWriteAttribute(CodeWriter codeWriter, string key)
{
codeWriter
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}")
.Write("-1")
.WriteParameterSeparator()
.WriteStringLiteral(key)
.WriteParameterSeparator();
}
public override void WriteComponentOpen(CodeRenderingContext context, ComponentOpenExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// Do nothing
}
public override void WriteComponentClose(CodeRenderingContext context, ComponentCloseExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// Do nothing
}
public override void WriteComponentBody(CodeRenderingContext context, ComponentBodyExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// We need to be aware of the blazor scope-tracking concept in design-time code generation
// because each component creates a lambda scope for its child content.
//
// We're hacking it a bit here by just forcing every component to have an empty lambda
_scopeStack.OpenScope(node.TagName, isComponent: true);
_scopeStack.IncrementCurrentScopeChildCount(context);
context.RenderChildren(node);
_scopeStack.CloseScope(context, node.TagName, isComponent: true, source: node.Source);
}
public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// For design time we only care about the case where the attribute has c# code.
//
// We also limit component attributes to simple cases. However there is still a lot of complexity
// to handle here, since there are a few different cases for how an attribute might be structured.
//
// This rougly follows the design of the runtime writer for simplicity.
if (node.AttributeStructure == AttributeStructure.Minimized)
{
// Do nothhing
}
else if (
node.Children.Count != 1 ||
node.Children[0] is HtmlContentIntermediateNode htmlNode && htmlNode.Children.Count != 1 ||
node.Children[0] is CSharpExpressionIntermediateNode cSharpNode && cSharpNode.Children.Count != 1)
{
// We don't expect this to happen, we just want to know if it can.
throw new InvalidOperationException("Attribute nodes should either be minimized or a single content node.");
}
else if (node.BoundAttribute.IsUIEventHandlerProperty())
{
// See the runtime version of this code for a thorough description of what we're doing here
if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
{
// This is an escaped event handler
context.CodeWriter.Write(DesignTimeVariable);
context.CodeWriter.Write(" = ");
context.CodeWriter.Write("new ");
context.CodeWriter.Write(node.BoundAttribute.TypeName);
context.CodeWriter.Write("(");
context.CodeWriter.WriteLine();
WriteCSharpToken(context, ((IntermediateToken)cSharpNode.Children[0]));
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();
}
else
{
context.CodeWriter.Write(DesignTimeVariable);
context.CodeWriter.Write(" = ");
context.CodeWriter.Write("new ");
context.CodeWriter.Write(node.BoundAttribute.TypeName);
context.CodeWriter.Write("(e => ");
WriteCSharpToken(context, ((IntermediateToken)node.Children[0]));
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();
}
}
else if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
{
// This is the case when an attribute has an explicit C# transition like:
// <MyComponent Foo="@bar" />
context.CodeWriter.Write(DesignTimeVariable);
context.CodeWriter.Write(" = ");
WriteCSharpToken(context, ((IntermediateToken)cSharpNode.Children[0]));
context.CodeWriter.Write(";");
context.CodeWriter.WriteLine();
}
else if ((htmlNode = node.Children[0] as HtmlContentIntermediateNode) != null)
{
// Do nothing
}
else if (node.Children[0] is IntermediateToken token && token.IsCSharp)
{
context.CodeWriter.Write(DesignTimeVariable);
context.CodeWriter.Write(" = ");
WriteCSharpToken(context, token);
context.CodeWriter.Write(";");
context.CodeWriter.WriteLine();
}
}
private void WriteCSharpToken(CodeRenderingContext context, IntermediateToken token)
{
if (string.IsNullOrWhiteSpace(token.Content))
{
return;
}
if (token.Source?.FilePath == null)
{
context.CodeWriter.Write(token.Content);
return;
}
using (context.CodeWriter.BuildLinePragma(token.Source))
{
context.CodeWriter.WritePadding(0, token.Source.Value, context);
context.AddSourceMappingFor(token);
context.CodeWriter.Write(token.Content);
}
}
}
}

View File

@ -1,8 +1,11 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using AngleSharp; using AngleSharp;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor namespace Microsoft.AspNetCore.Blazor.Razor
{ {
@ -51,6 +54,40 @@ namespace Microsoft.AspNetCore.Blazor.Razor
return RazorDiagnostic.Create(MismatchedClosingTagKind, span ?? SourceSpan.Undefined, tagName, kind, expectedKind); return RazorDiagnostic.Create(MismatchedClosingTagKind, span ?? SourceSpan.Undefined, tagName, kind, expectedKind);
} }
public static readonly RazorDiagnosticDescriptor MultipleComponents = new RazorDiagnosticDescriptor(
"BL9985",
() => "Multiple components use the tag '{0}'. Components: {1}",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_MultipleComponents(SourceSpan? span, string tagName, IEnumerable<TagHelperDescriptor> components)
{
return RazorDiagnostic.Create(MultipleComponents, span ?? SourceSpan.Undefined, tagName, string.Join(", ", components.Select(c => c.DisplayName)));
}
public static readonly RazorDiagnosticDescriptor UnsupportedComplexContent = new RazorDiagnosticDescriptor(
"BL9986",
() => "Component attributes do not support complex content (mixed C# and markup). Attribute: '{0}', text '{1}'",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_UnsupportedComplexContent(
SourceSpan? source,
TagHelperPropertyIntermediateNode node,
IntermediateNodeCollection children)
{
var content = string.Join("", children.OfType<IntermediateToken>().Select(t => t.Content));
return RazorDiagnostic.Create(UnsupportedComplexContent, source ?? SourceSpan.Undefined, node.AttributeName, content);
}
public static readonly RazorDiagnosticDescriptor UnboundComponentAttribute = new RazorDiagnosticDescriptor(
"BL9987",
() => "The component '{0}' does not have an attribute named '{1}'.",
RazorDiagnosticSeverity.Error);
public static RazorDiagnostic Create_UnboundComponentAttribute(SourceSpan? source, string componentType, TagHelperHtmlAttributeIntermediateNode node)
{
return RazorDiagnostic.Create(UnboundComponentAttribute, source ?? SourceSpan.Undefined, componentType, node.AttributeName);
}
private static SourceSpan? CalculateSourcePosition( private static SourceSpan? CalculateSourcePosition(
SourceSpan? razorTokenPosition, SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition) TextPosition htmlNodePosition)

View File

@ -65,6 +65,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions()); builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
builder.Features.Add(new ComponentDocumentClassifierPass()); builder.Features.Add(new ComponentDocumentClassifierPass());
builder.Features.Add(new ComponentLoweringPass());
builder.Features.Add(new ComponentTagHelperDescriptorProvider()); builder.Features.Add(new ComponentTagHelperDescriptorProvider());

View File

@ -14,7 +14,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{ {
private const string ImportsFileName = "_ViewImports.cshtml"; private const string ImportsFileName = "_ViewImports.cshtml";
public RazorProjectItem DefaultImports => VirtualProjectItem.Instance; private const string DefaultUsingImportContent = @"
@using System
@using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
";
public RazorProjectEngine ProjectEngine { get; set; } public RazorProjectEngine ProjectEngine { get; set; }
@ -27,9 +32,23 @@ namespace Microsoft.AspNetCore.Blazor.Razor
var imports = new List<RazorProjectItem>() var imports = new List<RazorProjectItem>()
{ {
VirtualProjectItem.Instance, new VirtualProjectItem(DefaultUsingImportContent),
new VirtualProjectItem(@"@addTagHelper ""*, Microsoft.AspNetCore.Blazor"""),
}; };
// Try and infer a namespace from the project directory. We don't yet have the ability to pass
// the namespace through from the project.
if (projectItem.PhysicalPath != null && projectItem.FilePath != null)
{
var trimLength = projectItem.FilePath.Length + (projectItem.FilePath.StartsWith("/") ? 0 : 1);
var baseDirectory = projectItem.PhysicalPath.Substring(0, projectItem.PhysicalPath.Length - trimLength);
var baseNamespace = Path.GetFileName(baseDirectory);
if (!string.IsNullOrEmpty(baseNamespace))
{
imports.Add(new VirtualProjectItem($@"@addTagHelper ""*, {baseNamespace}"""));
}
}
// We add hierarchical imports second so any default directive imports can be overridden. // We add hierarchical imports second so any default directive imports can be overridden.
imports.AddRange(GetHierarchicalImports(ProjectEngine.FileSystem, projectItem)); imports.AddRange(GetHierarchicalImports(ProjectEngine.FileSystem, projectItem));
@ -45,22 +64,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor
private class VirtualProjectItem : RazorProjectItem private class VirtualProjectItem : RazorProjectItem
{ {
private readonly byte[] _defaultImportBytes; private readonly byte[] _bytes;
private VirtualProjectItem() public VirtualProjectItem(string content)
{ {
var preamble = Encoding.UTF8.GetPreamble(); var preamble = Encoding.UTF8.GetPreamble();
var content = @"
@using System
@using System.Collections.Generic
@using System.Linq
@using System.Threading.Tasks
";
var contentBytes = Encoding.UTF8.GetBytes(content); var contentBytes = Encoding.UTF8.GetBytes(content);
_defaultImportBytes = new byte[preamble.Length + contentBytes.Length]; _bytes = new byte[preamble.Length + contentBytes.Length];
preamble.CopyTo(_defaultImportBytes, 0); preamble.CopyTo(_bytes, 0);
contentBytes.CopyTo(_defaultImportBytes, preamble.Length); contentBytes.CopyTo(_bytes, preamble.Length);
} }
public override string BasePath => null; public override string BasePath => null;
@ -71,9 +84,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public override bool Exists => true; public override bool Exists => true;
public static VirtualProjectItem Instance { get; } = new VirtualProjectItem(); public override Stream Read() => new MemoryStream(_bytes);
public override Stream Read() => new MemoryStream(_defaultImportBytes);
} }
} }
} }

View File

@ -0,0 +1,31 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal abstract class BlazorNodeWriter : IntermediateNodeWriter
{
public sealed override void BeginWriterScope(CodeRenderingContext context, string writer)
{
throw new NotImplementedException(nameof(BeginWriterScope));
}
public sealed override void EndWriterScope(CodeRenderingContext context)
{
throw new NotImplementedException(nameof(EndWriterScope));
}
public abstract void BeginWriteAttribute(CodeWriter codeWriter, string key);
public abstract void WriteComponentOpen(CodeRenderingContext context, ComponentOpenExtensionNode node);
public abstract void WriteComponentClose(CodeRenderingContext context, ComponentCloseExtensionNode node);
public abstract void WriteComponentBody(CodeRenderingContext context, ComponentBodyExtensionNode node);
public abstract void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node);
}
}

View File

@ -19,7 +19,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
/// <summary> /// <summary>
/// Generates the C# code corresponding to Razor source document contents. /// Generates the C# code corresponding to Razor source document contents.
/// </summary> /// </summary>
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter internal class BlazorRuntimeNodeWriter : BlazorNodeWriter
{ {
// Per the HTML spec, the following elements are inherently self-closing // Per the HTML spec, the following elements are inherently self-closing
// For example, <img> is the same as <img /> (and therefore it cannot contain descendants) // For example, <img> is the same as <img /> (and therefore it cannot contain descendants)
@ -48,16 +48,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public IntermediateToken AttributeValue; public IntermediateToken AttributeValue;
} }
public override void BeginWriterScope(CodeRenderingContext context, string writer)
{
throw new System.NotImplementedException(nameof(BeginWriterScope));
}
public override void EndWriterScope(CodeRenderingContext context)
{
throw new System.NotImplementedException(nameof(EndWriterScope));
}
public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node) public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node)
{ {
var isWhitespaceStatement = true; var isWhitespaceStatement = true;
@ -373,6 +363,173 @@ namespace Microsoft.AspNetCore.Blazor.Razor
} }
} }
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
{
context.CodeWriter.WriteUsing(node.Content, endLine: true);
}
public override void WriteComponentOpen(CodeRenderingContext context, ComponentOpenExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// The start tag counts as a child from a markup point of view.
_scopeStack.IncrementCurrentScopeChildCount(context);
// builder.OpenComponent<TComponent>(42);
context.CodeWriter.Write(_scopeStack.BuilderVarName);
context.CodeWriter.Write(".");
context.CodeWriter.Write(BlazorApi.RenderTreeBuilder.OpenComponent);
context.CodeWriter.Write("<");
context.CodeWriter.Write(node.TypeName);
context.CodeWriter.Write(">(");
context.CodeWriter.Write((_sourceSequence++).ToString());
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();
}
public override void WriteComponentClose(CodeRenderingContext context, ComponentCloseExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// The close tag counts as a child from a markup point of view.
_scopeStack.IncrementCurrentScopeChildCount(context);
// builder.OpenComponent<TComponent>(42);
context.CodeWriter.Write(_scopeStack.BuilderVarName);
context.CodeWriter.Write(".");
context.CodeWriter.Write(BlazorApi.RenderTreeBuilder.CloseComponent);
context.CodeWriter.Write("();");
context.CodeWriter.WriteLine();
}
public override void WriteComponentBody(CodeRenderingContext context, ComponentBodyExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
_scopeStack.OpenScope(node.TagName, isComponent: true);
context.RenderChildren(node);
_scopeStack.CloseScope(context, node.TagName, isComponent: true, source: node.Source);
}
public override void WriteComponentAttribute(CodeRenderingContext context, ComponentAttributeExtensionNode node)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (node == null)
{
throw new ArgumentNullException(nameof(node));
}
// builder.OpenComponent<TComponent>(42);
context.CodeWriter.Write(_scopeStack.BuilderVarName);
context.CodeWriter.Write(".");
context.CodeWriter.Write(BlazorApi.RenderTreeBuilder.AddAttribute);
context.CodeWriter.Write("(");
context.CodeWriter.Write((_sourceSequence++).ToString());
context.CodeWriter.Write(", ");
context.CodeWriter.WriteStringLiteral(node.AttributeName);
context.CodeWriter.Write(", ");
if (node.AttributeStructure == AttributeStructure.Minimized)
{
// Minimized attributes always map to 'true'
context.CodeWriter.Write("true");
}
else if (
node.Children.Count != 1 ||
node.Children[0] is HtmlContentIntermediateNode htmlNode && htmlNode.Children.Count != 1 ||
node.Children[0] is CSharpExpressionIntermediateNode cSharpNode && cSharpNode.Children.Count != 1)
{
// We don't expect this to happen, we just want to know if it can.
throw new InvalidOperationException("Attribute nodes should either be minimized or a single content node.");
}
else if (node.BoundAttribute.IsUIEventHandlerProperty())
{
// This is a UIEventHandler property. We do some special code generation for this
// case so that it's easier to write for common cases.
//
// Example:
// <MyComponent OnClick="Foo()"/>
// --> builder.AddAttribute(X, "OnClick", new UIEventHandler((e) => Foo()));
//
// The constructor is important because we want to put type inference into a state where
// we know the delegate's type should be UIEventHandler. AddAttribute has an overload that
// accepts object, so without the 'new UIEventHandler' things will get ugly.
//
// The escape for this behavior is to prefix the expression with @. This is similar to
// how escaping works for ModelExpression in MVC.
// Example:
// <MyComponent OnClick="@Foo"/>
// --> builder.AddAttribute(X, "OnClick", new UIEventHandler(Foo));
if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
{
// This is an escaped event handler;
context.CodeWriter.Write("new ");
context.CodeWriter.Write(node.BoundAttribute.TypeName);
context.CodeWriter.Write("(");
context.CodeWriter.Write(((IntermediateToken)cSharpNode.Children[0]).Content);
context.CodeWriter.Write(")");
}
else
{
context.CodeWriter.Write("new ");
context.CodeWriter.Write(node.BoundAttribute.TypeName);
context.CodeWriter.Write("(");
context.CodeWriter.Write("e => ");
context.CodeWriter.Write(((IntermediateToken)node.Children[0]).Content);
context.CodeWriter.Write(")");
}
}
else if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
{
context.CodeWriter.Write(((IntermediateToken)cSharpNode.Children[0]).Content);
}
else if ((htmlNode = node.Children[0] as HtmlContentIntermediateNode) != null)
{
// This is how string attributes are lowered by default, a single HTML node with a single HTML token.
context.CodeWriter.WriteStringLiteral(((IntermediateToken)htmlNode.Children[0]).Content);
}
else if (node.Children[0] is IntermediateToken token)
{
// This is what we expect for non-string nodes.
context.CodeWriter.Write(((IntermediateToken)node.Children[0]).Content);
}
else
{
throw new InvalidOperationException("Unexpected node type " + node.Children[0].GetType().FullName);
}
context.CodeWriter.Write(");");
context.CodeWriter.WriteLine();
}
private SourceSpan? CalculateSourcePosition( private SourceSpan? CalculateSourcePosition(
SourceSpan? razorTokenPosition, SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition) TextPosition htmlNodePosition)
@ -434,7 +591,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
codeWriter.WriteEndMethodInvocation(); codeWriter.WriteEndMethodInvocation();
} }
public void BeginWriteAttribute(CodeWriter codeWriter, string key) public override void BeginWriteAttribute(CodeWriter codeWriter, string key)
{ {
codeWriter codeWriter
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}") .WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}")
@ -444,11 +601,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
.WriteParameterSeparator(); .WriteParameterSeparator();
} }
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
{
context.CodeWriter.WriteUsing(node.Content, endLine: true);
}
private static string GetContent(HtmlContentIntermediateNode node) private static string GetContent(HtmlContentIntermediateNode node)
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();

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 System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class ComponentAttributeExtensionNode : ExtensionIntermediateNode
{
public ComponentAttributeExtensionNode()
{
}
public ComponentAttributeExtensionNode(TagHelperPropertyIntermediateNode propertyNode)
{
if (propertyNode == null)
{
throw new ArgumentNullException(nameof(propertyNode));
}
AttributeName = propertyNode.AttributeName;
AttributeStructure = propertyNode.AttributeStructure;
BoundAttribute = propertyNode.BoundAttribute;
IsIndexerNameMatch = propertyNode.IsIndexerNameMatch;
Source = propertyNode.Source;
TagHelper = propertyNode.TagHelper;
for (var i = 0; i < propertyNode.Children.Count; i++)
{
Children.Add(propertyNode.Children[i]);
}
for (var i = 0; i < propertyNode.Diagnostics.Count; i++)
{
Diagnostics.Add(propertyNode.Diagnostics[i]);
}
}
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
public string AttributeName { get; set; }
public AttributeStructure AttributeStructure { get; set; }
public BoundAttributeDescriptor BoundAttribute { get; set; }
public string FieldName { get; set; }
public bool IsIndexerNameMatch { get; set; }
public string PropertyName { get; set; }
public TagHelperDescriptor TagHelper { get; set; }
public override void Accept(IntermediateNodeVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
AcceptExtensionNode<ComponentAttributeExtensionNode>(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.WriteComponentAttribute(context, this);
}
}
}

View File

@ -0,0 +1,69 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
public sealed class ComponentBodyExtensionNode : ExtensionIntermediateNode
{
public ComponentBodyExtensionNode()
{
}
public ComponentBodyExtensionNode(TagHelperBodyIntermediateNode bodyNode)
{
if (bodyNode == null)
{
throw new ArgumentNullException(nameof(bodyNode));
}
Source = bodyNode.Source;
for (var i = 0; i < bodyNode.Children.Count; i++)
{
Children.Add(bodyNode.Children[i]);
}
for (var i = 0; i < bodyNode.Diagnostics.Count; i++)
{
Diagnostics.Add(bodyNode.Diagnostics[i]);
}
}
public override IntermediateNodeCollection Children { get; } = new IntermediateNodeCollection();
public TagMode TagMode { get; set; }
public string TagName { get; set; }
public override void Accept(IntermediateNodeVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
AcceptExtensionNode<ComponentBodyExtensionNode>(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.WriteComponentBody(context, this);
}
}
}

View File

@ -0,0 +1,40 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class ComponentCloseExtensionNode : ExtensionIntermediateNode
{
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
public override void Accept(IntermediateNodeVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
AcceptExtensionNode<ComponentCloseExtensionNode>(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.WriteComponentClose(context, this);
}
}
}

View File

@ -0,0 +1,135 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Extensions;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class ComponentLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
{
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;
}
// For each component *usage* we need to rewrite the tag helper node to map to the relevant component
// APIs.
var nodes = documentNode.FindDescendantNodes<TagHelperIntermediateNode>();
for (var i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
if (node.TagHelpers.Count > 1)
{
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_MultipleComponents(node.Source, node.TagName, node.TagHelpers));
}
RewriteUsage(node, node.TagHelpers[0]);
}
}
private void RewriteUsage(TagHelperIntermediateNode node, TagHelperDescriptor tagHelper)
{
// Ignore Kind here. Some versions of Razor have a bug in the serializer that ignores it.
// We need to surround the contents of the node with open and close nodes to ensure the component
// is scoped correctly.
node.Children.Insert(0, new ComponentOpenExtensionNode()
{
TypeName = tagHelper.GetTypeName(),
});
for (var i = node.Children.Count - 1; i >= 0; i--)
{
if (node.Children[i] is TagHelperBodyIntermediateNode bodyNode)
{
// Replace with a node that we recognize so that it we can do proper scope tracking.
//
// Note that we force the body node to be last, this is done to push it after the
// attribute nodes. This gives us the ordering we want for the render tree.
node.Children.RemoveAt(i);
node.Children.Add(new ComponentBodyExtensionNode(bodyNode)
{
TagMode = node.TagMode,
TagName = node.TagName,
});
}
}
node.Children.Add(new ComponentCloseExtensionNode());
// Now we need to rewrite any set property nodes to call the appropriate AddAttribute api.
for (var i = node.Children.Count - 1; i >= 0; i--)
{
if (node.Children[i] is TagHelperPropertyIntermediateNode propertyNode &&
propertyNode.TagHelper == tagHelper)
{
// We don't support 'complex' content for components (mixed C# and markup) right now.
// It's not clear yet if Blazor will have a good scenario to use these constructs.
//
// This is where a lot of the complexity in the Razor/TagHelpers model creeps in and we
// might be able to avoid it if these features aren't needed.
if (propertyNode.Children.Count == 1 &&
propertyNode.Children[0] is HtmlAttributeIntermediateNode htmlNode &&
htmlNode.Children.Count > 1)
{
// This case can be hit for a 'string' attribute
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
propertyNode.Source,
propertyNode,
htmlNode.Children));
node.Children.RemoveAt(i);
continue;
}
if (propertyNode.Children.Count == 1 &&
propertyNode.Children[0] is CSharpExpressionIntermediateNode cSharpNode &&
cSharpNode.Children.Count > 1)
{
// This case can be hit when the attribute has an explicit @ inside, which
// 'escapes' any special sugar we provide for codegen.
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
propertyNode.Source,
propertyNode,
cSharpNode.Children));
node.Children.RemoveAt(i);
continue;
}
else if (propertyNode.Children.Count > 1)
{
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
propertyNode.Source,
propertyNode,
propertyNode.Children));
node.Children.RemoveAt(i);
continue;
}
node.Children[i] = new ComponentAttributeExtensionNode(propertyNode)
{
PropertyName = propertyNode.BoundAttribute.GetPropertyName(),
};
}
}
// Add an error and remove any nodes that don't map to a component property.
for (var i = node.Children.Count - 1; i >= 0; i--)
{
if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode attributeNode)
{
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnboundComponentAttribute(
attributeNode.Source,
tagHelper.GetTypeName(),
attributeNode));
node.Children.RemoveAt(i);
}
}
}
}
}

View File

@ -0,0 +1,42 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class ComponentOpenExtensionNode : ExtensionIntermediateNode
{
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
public string TypeName { get; set; }
public override void Accept(IntermediateNodeVisitor visitor)
{
if (visitor == null)
{
throw new ArgumentNullException(nameof(visitor));
}
AcceptExtensionNode<ComponentOpenExtensionNode>(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.WriteComponentOpen(context, this);
}
}
}

View File

@ -11,8 +11,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{ {
internal class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider internal class ComponentTagHelperDescriptorProvider : RazorEngineFeatureBase, ITagHelperDescriptorProvider
{ {
public readonly static string ComponentTagHelperKind = ComponentDocumentClassifierPass.ComponentDocumentKind; public static readonly string UIEventHandlerPropertyMetadata = "Blazor.IsUIEventHandler";
public readonly static string ComponentTagHelperKind = ComponentDocumentClassifierPass.ComponentDocumentKind;
private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat = private static readonly SymbolDisplayFormat FullNameTypeDisplayFormat =
SymbolDisplayFormat.FullyQualifiedFormat SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted) .WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
@ -109,6 +111,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
pb.IsEnum = true; pb.IsEnum = true;
} }
if (property.kind == PropertyKind.Delegate)
{
pb.Metadata.Add(UIEventHandlerPropertyMetadata, bool.TrueString);
}
xml = property.property.GetDocumentationCommentXml(); xml = property.property.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xml)) if (!string.IsNullOrEmpty(xml))
{ {
@ -145,13 +152,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
continue; continue;
} }
var kind = PropertyKind.Default;
if (properties.ContainsKey(property.Name)) if (properties.ContainsKey(property.Name))
{ {
// Not visible // Not visible
kind = PropertyKind.Ignored; continue;
} }
var kind = PropertyKind.Default;
if (property.Parameters.Length != 0) if (property.Parameters.Length != 0)
{ {
// Indexer // Indexer
@ -175,6 +182,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor
kind = PropertyKind.Enum; kind = PropertyKind.Enum;
} }
if (kind == PropertyKind.Default &&
property.Type.TypeKind == TypeKind.Delegate &&
property.Type.ToDisplayString(FullNameTypeDisplayFormat) == BlazorApi.UIEventHandler.FullTypeName)
{
// For delegate types we do some special code generation when the type
// UIEventHandler.
kind = PropertyKind.Delegate;
}
properties.Add(property.Name, (property, kind)); properties.Add(property.Name, (property, kind));
} }
@ -190,6 +206,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
Ignored, Ignored,
Default, Default,
Enum, Enum,
Delegate,
} }
private class ComponentTypeVisitor : SymbolVisitor private class ComponentTypeVisitor : SymbolVisitor

View File

@ -71,7 +71,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{ {
// When we're about to insert the first child into a component, // When we're about to insert the first child into a component,
// it's time to open a new lambda // it's time to open a new lambda
var blazorNodeWriter = (BlazorIntermediateNodeWriter)context.NodeWriter; var blazorNodeWriter = (BlazorNodeWriter)context.NodeWriter;
blazorNodeWriter.BeginWriteAttribute(context.CodeWriter, BlazorApi.RenderTreeBuilder.ChildContent); blazorNodeWriter.BeginWriteAttribute(context.CodeWriter, BlazorApi.RenderTreeBuilder.ChildContent);
OffsetBuilderVarNumber(1); OffsetBuilderVarNumber(1);
context.CodeWriter.Write($"({BlazorApi.RenderFragment.FullTypeName})("); context.CodeWriter.Write($"({BlazorApi.RenderFragment.FullTypeName})(");

View File

@ -0,0 +1,24 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Razor.Language;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal static class TagHelperBoundAttributeDescriptorExtensions
{
public static bool IsUIEventHandlerProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
var key = ComponentTagHelperDescriptorProvider.UIEventHandlerPropertyMetadata;
return
attribute.Metadata.TryGetValue(key, out var value) &&
string.Equals(value, bool.TrueString);
}
}
}

View File

@ -1,9 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.AspNetCore.Blazor.Components;
using System; using System;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using Microsoft.AspNetCore.Blazor.Components;
namespace Microsoft.AspNetCore.Blazor.RenderTree namespace Microsoft.AspNetCore.Blazor.RenderTree
{ {
@ -224,5 +224,34 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal RenderTreeFrame WithRegionSubtreeLength(int regionSubtreeLength) internal RenderTreeFrame WithRegionSubtreeLength(int regionSubtreeLength)
=> new RenderTreeFrame(Sequence, regionSubtreeLength: regionSubtreeLength); => new RenderTreeFrame(Sequence, regionSubtreeLength: regionSubtreeLength);
// Just to be nice for debugging and unit tests.
public override string ToString()
{
switch (FrameType)
{
case RenderTreeFrameType.Attribute:
return $"Attribute: (seq={Sequence}, id={AttributeEventHandlerId}) '{AttributeName}'='{AttributeValue}'";
case RenderTreeFrameType.Component:
return $"Component: (seq={Sequence}, len={ComponentSubtreeLength}) {ComponentType}";
case RenderTreeFrameType.Element:
return $"Element: (seq={Sequence}, len={ElementSubtreeLength}) {ElementName}";
case RenderTreeFrameType.Region:
return $"Region: (seq={Sequence}, len={RegionSubtreeLength})";
case RenderTreeFrameType.Text:
return $"Text: (seq={Sequence}, len=n/a) {EscapeNewlines(TextContent)}";
}
return base.ToString();
}
private static string EscapeNewlines(string text)
{
return text.Replace("\n", "\\n").Replace("\r\n", "\\r\\n");
}
} }
} }

View File

@ -1,6 +1,9 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
using System.Text;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Xunit; using Xunit;
@ -18,7 +21,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@" AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using namespace Test namespace Test
{ {
public class MyComponent : BlazorComponent public class MyComponent : BlazorComponent
{ {
@ -34,6 +37,29 @@ using namespace Test
Assert.Single(bindings.TagHelpers, t => t.Name == "Test.MyComponent"); Assert.Single(bindings.TagHelpers, t => t.Name == "Test.MyComponent");
} }
[Fact]
public void ComponentDiscovery_CanFindComponent_WithNamespace_DefinedinCSharp()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test.AnotherNamespace
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Act
var result = CompileToCSharp("@addTagHelper *, TestAssembly");
// Assert
var bindings = result.CodeDocument.GetTagHelperContext();
Assert.Single(bindings.TagHelpers, t => t.Name == "Test.AnotherNamespace.MyComponent");
}
[Fact] [Fact]
public void ComponentDiscovery_CanFindComponent_DefinedinCshtml() public void ComponentDiscovery_CanFindComponent_DefinedinCshtml()
{ {

View File

@ -2,79 +2,285 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Linq; using System.Linq;
using System.Reflection;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Layouts;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers; using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Microsoft.CodeAnalysis.CSharp;
using Xunit; using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test namespace Microsoft.AspNetCore.Blazor.Build.Test
{ {
public class ComponentRenderingRazorIntegrationTest : RazorIntegrationTestBase public class ComponentRenderingRazorIntegrationTest : RazorIntegrationTestBase
{ {
internal override bool UseTwoPhaseCompilation => true;
[Fact] [Fact]
public void SupportsChildComponentsViaTemporarySyntax() public void Render_ChildComponent_Simple()
{ {
// Arrange/Act // Arrange
var testComponentTypeName = FullTypeName<TestComponent>(); AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
var component = CompileToComponent($"<c:{testComponentTypeName} />"); using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent/>");
// Act
var frames = GetRenderTree(component); var frames = GetRenderTree(component);
// Assert // Assert
Assert.Collection(frames, Assert.Collection(
frame => AssertFrame.Component<TestComponent>(frame, 1, 0)); frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 1, 0));
} }
[Fact] [Fact]
public void CanPassParametersToComponents() public void Render_ChildComponent_WithParameters()
{ {
// Arrange/Act // Arrange
var testComponentTypeName = FullTypeName<TestComponent>(); AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
var testObjectTypeName = FullTypeName<SomeType>(); using Microsoft.AspNetCore.Blazor.Components;
// TODO: Once we have the improved component tooling and can allow syntax
// like StringProperty="My string" or BoolProperty=true, update this namespace Test
// test to use that syntax. {
var component = CompileToComponent($"<c:{testComponentTypeName}" + public class SomeType
$" IntProperty=@(123)" + {
$" BoolProperty=@true" + }
$" StringProperty=@(\"My string\")" +
$" ObjectProperty=@(new {testObjectTypeName}()) />"); public class MyComponent : BlazorComponent
{
public int IntProperty { get; set; }
public bool BoolProperty { get; set; }
public string StringProperty { get; set; }
public SomeType ObjectProperty { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent
IntProperty=""123""
BoolProperty=""true""
StringProperty=""My string""
ObjectProperty=""new SomeType()""/>");
// Act
var frames = GetRenderTree(component); var frames = GetRenderTree(component);
// Assert // Assert
Assert.Collection(frames, Assert.Collection(
frame => AssertFrame.Component<TestComponent>(frame, 5, 0), frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 5, 0),
frame => AssertFrame.Attribute(frame, "IntProperty", 123, 1), frame => AssertFrame.Attribute(frame, "IntProperty", 123, 1),
frame => AssertFrame.Attribute(frame, "BoolProperty", true, 2), frame => AssertFrame.Attribute(frame, "BoolProperty", true, 2),
frame => AssertFrame.Attribute(frame, "StringProperty", "My string", 3), frame => AssertFrame.Attribute(frame, "StringProperty", "My string", 3),
frame => frame =>
{ {
AssertFrame.Attribute(frame, "ObjectProperty", 4); AssertFrame.Attribute(frame, "ObjectProperty", 4);
Assert.IsType<SomeType>(frame.AttributeValue); Assert.Equal("Test.SomeType", frame.AttributeValue.GetType().FullName);
}); });
} }
[Fact] [Fact]
public void CanIncludeChildrenInComponents() public void Render_ChildComponent_WithExplicitStringParameter()
{ {
// Arrange/Act // Arrange
var testComponentTypeName = FullTypeName<TestComponent>(); AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
var component = CompileToComponent($"<c:{testComponentTypeName} MyAttr=@(\"abc\")>" + using Microsoft.AspNetCore.Blazor.Components;
$"Some text" +
$"<some-child a='1'>Nested text</some-child>" + namespace Test
$"</c:{testComponentTypeName}>"); {
public class MyComponent : BlazorComponent
{
public string StringProperty { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent StringProperty=""@(42.ToString())"" />");
// Act
var frames = GetRenderTree(component);
// Assert
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 0),
frame => AssertFrame.Attribute(frame, "StringProperty", "42", 1));
}
[Fact]
public void Render_ChildComponent_WithLambdaEventHandler()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using System;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public UIEventHandler OnClick { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent OnClick=""Increment()""/>
@functions {
private int counter;
private void Increment() {
counter++;
}
}");
// Act
var frames = GetRenderTree(component);
// Assert
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 0),
frame =>
{
AssertFrame.Attribute(frame, "OnClick", 1);
// The handler will have been assigned to a lambda
var handler = Assert.IsType<UIEventHandler>(frame.AttributeValue);
Assert.Equal("Test.TestComponent", handler.Target.GetType().FullName);
},
frame => AssertFrame.Whitespace(frame, 2));
}
[Fact]
public void Render_ChildComponent_WithExplicitEventHandler()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using System;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public UIEventHandler OnClick { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
@using Microsoft.AspNetCore.Blazor
<MyComponent OnClick=""@Increment""/>
@functions {
private int counter;
private void Increment(UIEventArgs e) {
counter++;
}
}");
// Act
var frames = GetRenderTree(component);
// Assert
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 0),
frame =>
{
AssertFrame.Attribute(frame, "OnClick", 1);
// The handler will have been assigned to a lambda
var handler = Assert.IsType<UIEventHandler>(frame.AttributeValue);
Assert.Equal("Test.TestComponent", handler.Target.GetType().FullName);
Assert.Equal("Increment", handler.Method.Name);
},
frame => AssertFrame.Whitespace(frame, 2));
}
[Fact]
public void Render_ChildComponent_WithMinimizedBoolAttribute()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public bool BoolProperty { get; set; }
}
}"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent BoolProperty />");
// Act
var frames = GetRenderTree(component);
// Assert
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 0),
frame => AssertFrame.Attribute(frame, "BoolProperty", true, 1));
}
[Fact]
public void Render_ChildComponent_WithChildContent()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public string MyAttr { get; set; }
public RenderFragment ChildContent { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent MyAttr=""abc"">Some text<some-child a='1'>Nested text</some-child></MyComponent>");
// Act
var frames = GetRenderTree(component); var frames = GetRenderTree(component);
// Assert: component frames are correct // Assert: component frames are correct
Assert.Collection(frames, Assert.Collection(
frame => AssertFrame.Component<TestComponent>(frame, 3, 0), frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
frame => AssertFrame.Attribute(frame, "MyAttr", "abc", 1), frame => AssertFrame.Attribute(frame, "MyAttr", "abc", 1),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2)); frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2));
// Assert: Captured ChildContent frames are correct // Assert: Captured ChildContent frames are correct
var childFrames = GetFrames((RenderFragment)frames[2].AttributeValue); var childFrames = GetFrames((RenderFragment)frames[2].AttributeValue);
Assert.Collection(childFrames, Assert.Collection(
childFrames,
frame => AssertFrame.Text(frame, "Some text", 3), frame => AssertFrame.Text(frame, "Some text", 3),
frame => AssertFrame.Element(frame, "some-child", 3, 4), frame => AssertFrame.Element(frame, "some-child", 3, 4),
frame => AssertFrame.Attribute(frame, "a", "1", 5), frame => AssertFrame.Attribute(frame, "a", "1", 5),
@ -82,21 +288,33 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
} }
[Fact] [Fact]
public void CanNestComponentChildContent() public void Render_ChildComponent_Nested()
{ {
// Arrange/Act // Arrange
var testComponentTypeName = FullTypeName<TestComponent>(); AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
var component = CompileToComponent( using Microsoft.AspNetCore.Blazor;
$"<c:{testComponentTypeName}>" + using Microsoft.AspNetCore.Blazor.Components;
$"<c:{testComponentTypeName}>" +
$"Some text" + namespace Test
$"</c:{testComponentTypeName}>" + {
$"</c:{testComponentTypeName}>"); public class MyComponent : BlazorComponent
{
public RenderFragment ChildContent { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent><MyComponent>Some text</MyComponent></MyComponent>");
// Act
var frames = GetRenderTree(component); var frames = GetRenderTree(component);
// Assert: outer component frames are correct // Assert: outer component frames are correct
Assert.Collection(frames, Assert.Collection(
frame => AssertFrame.Component<TestComponent>(frame, 2, 0), frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 0),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 1)); frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 1));
// Assert: first level of ChildContent is correct // Assert: first level of ChildContent is correct
@ -106,26 +324,15 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// As an implementation detail, it happens that they do follow on from the parent // As an implementation detail, it happens that they do follow on from the parent
// level, but we could change that part of the implementation if we wanted. // level, but we could change that part of the implementation if we wanted.
var innerFrames = GetFrames((RenderFragment)frames[1].AttributeValue).ToArray(); var innerFrames = GetFrames((RenderFragment)frames[1].AttributeValue).ToArray();
Assert.Collection(innerFrames, Assert.Collection(
frame => AssertFrame.Component<TestComponent>(frame, 2, 2), innerFrames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 2),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 3)); frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 3));
// Assert: second level of ChildContent is correct // Assert: second level of ChildContent is correct
Assert.Collection(GetFrames((RenderFragment)innerFrames[1].AttributeValue), Assert.Collection(
GetFrames((RenderFragment)innerFrames[1].AttributeValue),
frame => AssertFrame.Text(frame, "Some text", 4)); frame => AssertFrame.Text(frame, "Some text", 4));
} }
public class SomeType { }
public class TestComponent : IComponent
{
public void Init(RenderHandle renderHandle)
{
}
public void SetParameters(ParameterCollection parameters)
{
}
}
} }
} }

View File

@ -5,7 +5,6 @@ using System.Reflection;
using System.Text; using System.Text;
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Razor; using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Microsoft.AspNetCore.Razor.Language; using Microsoft.AspNetCore.Razor.Language;
using Xunit; using Xunit;

View File

@ -0,0 +1,431 @@
// 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.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class DesignTimeCodeGenerationRazorIntegrationTest : RazorIntegrationTestBase
{
internal override bool DesignTime => true;
internal override bool UseTwoPhaseCompilation => true;
[Fact]
public void CodeGeneration_ChildComponent_WithParameters()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class SomeType
{
}
public class MyComponent : BlazorComponent
{
public int IntProperty { get; set; }
public bool BoolProperty { get; set; }
public string StringProperty { get; set; }
public SomeType ObjectProperty { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent
IntProperty=""123""
BoolProperty=""true""
StringProperty=""My string""
ObjectProperty=""new SomeType()""/>");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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);
__o =
#line 3 ""x:\dir\subdir\Test\TestComponent.cshtml""
123
#line default
#line hidden
;
__o =
#line 4 ""x:\dir\subdir\Test\TestComponent.cshtml""
true
#line default
#line hidden
;
__o =
#line 6 ""x:\dir\subdir\Test\TestComponent.cshtml""
new SomeType()
#line default
#line hidden
;
builder.AddAttribute(-1, ""ChildContent"", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
}
));
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithExplicitStringParameter()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public string StringProperty { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent StringProperty=""@(42.ToString())"" />");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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);
__o =
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
42.ToString()
#line default
#line hidden
;
builder.AddAttribute(-1, ""ChildContent"", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
}
));
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithLambdaEventHandler()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using System;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public UIEventHandler OnClick { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent OnClick=""Increment()""/>
@functions {
private int counter;
private void Increment() {
counter++;
}
}");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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);
__o = new Microsoft.AspNetCore.Blazor.UIEventHandler(e =>
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
Increment()
#line default
#line hidden
);
builder.AddAttribute(-1, ""ChildContent"", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
}
));
}
#pragma warning restore 1998
#line 4 ""x:\dir\subdir\Test\TestComponent.cshtml""
private int counter;
private void Increment() {
counter++;
}
#line default
#line hidden
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithExplicitEventHandler()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using System;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public UIEventHandler OnClick { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
@using Microsoft.AspNetCore.Blazor
<MyComponent OnClick=""@Increment""/>
@functions {
private int counter;
private void Increment(UIEventArgs e) {
counter++;
}
}");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#line 2 ""x:\dir\subdir\Test\TestComponent.cshtml""
using Microsoft.AspNetCore.Blazor;
#line default
#line hidden
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);
__o = new Microsoft.AspNetCore.Blazor.UIEventHandler(
#line 3 ""x:\dir\subdir\Test\TestComponent.cshtml""
Increment
#line default
#line hidden
);
builder.AddAttribute(-1, ""ChildContent"", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
}
));
}
#pragma warning restore 1998
#line 5 ""x:\dir\subdir\Test\TestComponent.cshtml""
private int counter;
private void Increment(UIEventArgs e) {
counter++;
}
#line default
#line hidden
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim());
}
[Fact]
public void CodeGeneration_ChildComponent_WithChildContent()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public string MyAttr { get; set; }
public RenderFragment ChildContent { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent MyAttr=""abc"">Some text<some-child a='1'>Nested text</some-child></MyComponent>");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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) => {
}
));
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim());
}
}
}

View File

@ -56,14 +56,19 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public RazorIntegrationTestBase() public RazorIntegrationTestBase()
{ {
AdditionalSyntaxTrees = new List<SyntaxTree>(); AdditionalSyntaxTrees = new List<SyntaxTree>();
AdditionalRazorItems = new List<RazorProjectItem>();
Configuration = BlazorExtensionInitializer.DefaultConfiguration; Configuration = BlazorExtensionInitializer.DefaultConfiguration;
FileSystem = new VirtualRazorProjectFileSystem(); FileSystem = new VirtualRazorProjectFileSystem();
WorkingDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ArbitraryWindowsPath : ArbitraryMacLinuxPath; WorkingDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ArbitraryWindowsPath : ArbitraryMacLinuxPath;
DefaultBaseNamespace = "Test"; // Matches the default working directory DefaultBaseNamespace = "Test"; // Matches the default working directory
DefaultFileName = "TestComponent.cshtml"; DefaultFileName = "TestComponent.cshtml";
} }
internal List<RazorProjectItem> AdditionalRazorItems { get; }
internal List<SyntaxTree> AdditionalSyntaxTrees { get; } internal List<SyntaxTree> AdditionalSyntaxTrees { get; }
internal virtual RazorConfiguration Configuration { get; } internal virtual RazorConfiguration Configuration { get; }
@ -71,6 +76,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
internal virtual string DefaultBaseNamespace { get; } internal virtual string DefaultBaseNamespace { get; }
internal virtual string DefaultFileName { get; } internal virtual string DefaultFileName { get; }
internal virtual bool DesignTime { get; }
internal virtual VirtualRazorProjectFileSystem FileSystem { get; } internal virtual VirtualRazorProjectFileSystem FileSystem { get; }
@ -82,6 +89,9 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{ {
return RazorProjectEngine.Create(configuration, FileSystem, b => return RazorProjectEngine.Create(configuration, FileSystem, b =>
{ {
// Turn off checksums, we're testing code generation.
b.Features.Add(new SuppressChecksum());
BlazorExtensionInitializer.Register(b); BlazorExtensionInitializer.Register(b);
b.Features.Add(new CompilationTagHelperFeature()); b.Features.Add(new CompilationTagHelperFeature());
@ -92,58 +102,83 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
}); });
} }
protected CompileToCSharpResult CompileToCSharp(string cshtmlContent) internal RazorProjectItem CreateProjectItem(string chtmlRelativePath, string cshtmlContent)
{
return CompileToCSharp(WorkingDirectory, DefaultFileName, cshtmlContent);
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, string cshtmlContent)
{
return CompileToCSharp(WorkingDirectory, cshtmlRelativePath, cshtmlContent);
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlRootPath, string cshtmlRelativePath, string cshtmlContent)
{ {
// FilePaths in Razor are **always** are of the form '/a/b/c.cshtml' // FilePaths in Razor are **always** are of the form '/a/b/c.cshtml'
var filePath = cshtmlRelativePath.Replace('\\', '/'); var filePath = chtmlRelativePath.Replace('\\', '/');
if (!filePath.StartsWith('/')) if (!filePath.StartsWith('/'))
{ {
filePath = '/' + filePath; filePath = '/' + filePath;
} }
var projectItem = new VirtualProjectItem( return new VirtualProjectItem(
cshtmlRootPath, WorkingDirectory,
filePath, filePath,
Path.Combine(cshtmlRootPath, cshtmlRelativePath), Path.Combine(WorkingDirectory, chtmlRelativePath),
cshtmlRelativePath, chtmlRelativePath,
Encoding.UTF8.GetBytes(cshtmlContent)); Encoding.UTF8.GetBytes(cshtmlContent.TrimStart()));
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlContent)
{
return CompileToCSharp(DefaultFileName, cshtmlContent);
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, string cshtmlContent)
{
if (UseTwoPhaseCompilation) if (UseTwoPhaseCompilation)
{ {
// The first phase won't include any metadata references for component discovery. This mirrors // The first phase won't include any metadata references for component discovery. This mirrors
// what the build does. // what the build does.
var projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DeclarationConfiguration, Array.Empty<MetadataReference>()); var projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DeclarationConfiguration, Array.Empty<MetadataReference>());
var codeDocument = projectEngine.Process(projectItem);
RazorCodeDocument codeDocument;
foreach (var item in AdditionalRazorItems)
{
// Result of generating declarations
codeDocument = projectEngine.Process(item);
Assert.Empty(codeDocument.GetCSharpDocument().Diagnostics);
var syntaxTree = CSharpSyntaxTree.ParseText(codeDocument.GetCSharpDocument().GeneratedCode, path: item.FilePath);
AdditionalSyntaxTrees.Add(syntaxTree);
}
// Result of generating declarations // Result of generating declarations
var projectItem = CreateProjectItem(cshtmlRelativePath, cshtmlContent);
codeDocument = projectEngine.Process(projectItem);
var declaration = new CompileToCSharpResult var declaration = new CompileToCSharpResult
{ {
BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees),
CodeDocument = codeDocument, CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode, Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics, Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
}; };
// Result of doing 'temp' compilation // Result of doing 'temp' compilation
var tempAssembly = CompileToAssembly(declaration, BaseCompilation); var tempAssembly = CompileToAssembly(declaration);
// Add the 'temp' compilation as a metadata reference // Add the 'temp' compilation as a metadata reference
var references = BaseCompilation.References.Concat(new[] { tempAssembly.Compilation.ToMetadataReference() }).ToArray(); var references = BaseCompilation.References.Concat(new[] { tempAssembly.Compilation.ToMetadataReference() }).ToArray();
projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DefaultConfiguration, references); projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DefaultConfiguration, references);
// Result of real code // Now update the any additional files
codeDocument = projectEngine.Process(projectItem); foreach (var item in AdditionalRazorItems)
{
// Result of generating declarations
codeDocument = projectEngine.Process(item);
Assert.Empty(codeDocument.GetCSharpDocument().Diagnostics);
// Replace the 'declaration' syntax tree
var syntaxTree = CSharpSyntaxTree.ParseText(codeDocument.GetCSharpDocument().GeneratedCode, path: item.FilePath);
AdditionalSyntaxTrees.RemoveAll(st => st.FilePath == item.FilePath);
AdditionalSyntaxTrees.Add(syntaxTree);
}
// Result of real code generation for the document under test
codeDocument = DesignTime ? projectEngine.ProcessDesignTime(projectItem) : projectEngine.Process(projectItem);
return new CompileToCSharpResult return new CompileToCSharpResult
{ {
BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees),
CodeDocument = codeDocument, CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode, Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics, Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
@ -155,9 +190,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// This will include the built-in Blazor components. // This will include the built-in Blazor components.
var projectEngine = CreateProjectEngine(Configuration, BaseCompilation.References.ToArray()); var projectEngine = CreateProjectEngine(Configuration, BaseCompilation.References.ToArray());
var codeDocument = projectEngine.Process(projectItem); var projectItem = CreateProjectItem(cshtmlRelativePath, cshtmlContent);
var codeDocument = DesignTime ? projectEngine.ProcessDesignTime(projectItem) : projectEngine.Process(projectItem);
return new CompileToCSharpResult return new CompileToCSharpResult
{ {
BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees),
CodeDocument = codeDocument, CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode, Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics, Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
@ -167,19 +204,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, string cshtmlContent) protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, string cshtmlContent)
{ {
return CompileToAssembly(WorkingDirectory, cshtmlRelativePath, cshtmlContent); var cSharpResult = CompileToCSharp(cshtmlRelativePath, cshtmlContent);
}
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRootDirectory, string cshtmlRelativePath, string cshtmlContent)
{
var cSharpResult = CompileToCSharp(cshtmlRootDirectory, cshtmlRelativePath, cshtmlContent);
return CompileToAssembly(cSharpResult); return CompileToAssembly(cSharpResult);
} }
protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult, CSharpCompilation baseCompilation = null) protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult)
{ {
baseCompilation = baseCompilation ?? BaseCompilation;
if (cSharpResult.Diagnostics.Any()) if (cSharpResult.Diagnostics.Any())
{ {
var diagnosticsLog = string.Join(Environment.NewLine, cSharpResult.Diagnostics.Select(d => d.ToString()).ToArray()); var diagnosticsLog = string.Join(Environment.NewLine, cSharpResult.Diagnostics.Select(d => d.ToString()).ToArray());
@ -188,17 +218,24 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
var syntaxTrees = new[] var syntaxTrees = new[]
{ {
CSharpSyntaxTree.ParseText(cSharpResult.Code) CSharpSyntaxTree.ParseText(cSharpResult.Code),
}; };
var compilation = baseCompilation.AddSyntaxTrees(syntaxTrees).AddSyntaxTrees(AdditionalSyntaxTrees); var compilation = cSharpResult.BaseCompilation.AddSyntaxTrees(syntaxTrees);
var diagnostics = compilation
.GetDiagnostics()
.Where(d => d.Severity != DiagnosticSeverity.Hidden);
if (diagnostics.Any())
{
throw new CompilationFailedException(compilation);
}
using (var peStream = new MemoryStream()) using (var peStream = new MemoryStream())
{ {
compilation.Emit(peStream); compilation.Emit(peStream);
var diagnostics = compilation
.GetDiagnostics()
.Where(d => d.Severity != DiagnosticSeverity.Hidden);
return new CompileToAssemblyResult return new CompileToAssemblyResult
{ {
Compilation = compilation, Compilation = compilation,
@ -206,11 +243,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray()) Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray())
}; };
} }
} }
protected IComponent CompileToComponent(string cshtmlSource) protected IComponent CompileToComponent(string cshtmlSource)
{ {
var assemblyResult = CompileToAssembly(WorkingDirectory, DefaultFileName, cshtmlSource); var assemblyResult = CompileToAssembly(DefaultFileName, cshtmlSource);
var componentFullTypeName = $"{DefaultBaseNamespace}.{Path.GetFileNameWithoutExtension(DefaultFileName)}"; var componentFullTypeName = $"{DefaultBaseNamespace}.{Path.GetFileNameWithoutExtension(DefaultFileName)}";
return CompileToComponent(assemblyResult, componentFullTypeName); return CompileToComponent(assemblyResult, componentFullTypeName);
@ -223,8 +261,6 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected IComponent CompileToComponent(CompileToAssemblyResult assemblyResult, string fullTypeName) protected IComponent CompileToComponent(CompileToAssemblyResult assemblyResult, string fullTypeName)
{ {
Assert.Empty(assemblyResult.Diagnostics);
var componentType = assemblyResult.Assembly.GetType(fullTypeName); var componentType = assemblyResult.Assembly.GetType(fullTypeName);
if (componentType == null) if (componentType == null)
{ {
@ -255,6 +291,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected class CompileToCSharpResult protected class CompileToCSharpResult
{ {
// A compilation that can be used *with* this code to compile an assembly
public Compilation BaseCompilation { get; set; }
public RazorCodeDocument CodeDocument { get; set; } public RazorCodeDocument CodeDocument { get; set; }
public string Code { get; set; } public string Code { get; set; }
public IEnumerable<RazorDiagnostic> Diagnostics { get; set; } public IEnumerable<RazorDiagnostic> Diagnostics { get; set; }
@ -284,5 +322,62 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
LatestBatchReferenceFrames = renderBatch.ReferenceFrames.ToArray(); LatestBatchReferenceFrames = renderBatch.ReferenceFrames.ToArray();
} }
} }
private class CompilationFailedException : XunitException
{
public CompilationFailedException(Compilation compilation)
{
Compilation = compilation;
}
public Compilation Compilation { get; }
public override string Message
{
get
{
var builder = new StringBuilder();
builder.AppendLine("Compilation failed: ");
var diagnostics = Compilation.GetDiagnostics();
var syntaxTreesWithErrors = new HashSet<SyntaxTree>();
foreach (var diagnostic in diagnostics)
{
builder.AppendLine(diagnostic.ToString());
if (diagnostic.Location.IsInSource)
{
syntaxTreesWithErrors.Add(diagnostic.Location.SourceTree);
}
}
if (syntaxTreesWithErrors.Any())
{
builder.AppendLine();
builder.AppendLine();
foreach (var syntaxTree in syntaxTreesWithErrors)
{
builder.AppendLine($"File {syntaxTree.FilePath ?? "unknown"}:");
builder.AppendLine(syntaxTree.GetText().ToString());
}
}
return builder.ToString();
}
}
}
private class SuppressChecksum : IConfigureRazorCodeGenerationOptionsFeature
{
public int Order => 0;
public RazorEngine Engine { get; set; }
public void Configure(RazorCodeGenerationOptionsBuilder options)
{
options.SuppressChecksum = true;
}
}
} }
} }

View File

@ -43,15 +43,14 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Assert // Assert
var frames = GetRenderTree(component); var frames = GetRenderTree(component);
Assert.Collection(frames, Assert.Collection(frames,
frame => AssertFrame.Whitespace(frame, 0), frame => AssertFrame.Text(frame, "Hello", 0),
frame => AssertFrame.Text(frame, "Hello", 1), frame => AssertFrame.Whitespace(frame, 1),
frame => AssertFrame.Whitespace(frame, 2), frame => AssertFrame.Whitespace(frame, 2), // @((object)null)
frame => AssertFrame.Whitespace(frame, 3), // @((object)null) frame => AssertFrame.Whitespace(frame, 3),
frame => AssertFrame.Whitespace(frame, 4), frame => AssertFrame.Text(frame, "123", 4),
frame => AssertFrame.Text(frame, "123", 5), frame => AssertFrame.Whitespace(frame, 5),
frame => AssertFrame.Whitespace(frame, 6), frame => AssertFrame.Text(frame, new object().ToString(), 6),
frame => AssertFrame.Text(frame, new object().ToString(), 7), frame => AssertFrame.Whitespace(frame, 7));
frame => AssertFrame.Whitespace(frame, 8));
} }
[Fact] [Fact]
@ -70,12 +69,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Assert // Assert
var frames = GetRenderTree(component); var frames = GetRenderTree(component);
Assert.Collection(frames, Assert.Collection(frames,
frame => AssertFrame.Whitespace(frame, 0), frame => AssertFrame.Text(frame, "First", 0),
frame => AssertFrame.Text(frame, "First", 1), frame => AssertFrame.Text(frame, "Second", 0),
frame => AssertFrame.Text(frame, "Second", 1), frame => AssertFrame.Text(frame, "Third", 0),
frame => AssertFrame.Text(frame, "Third", 1), frame => AssertFrame.Whitespace(frame, 1),
frame => AssertFrame.Whitespace(frame, 2), frame => AssertFrame.Whitespace(frame, 2));
frame => AssertFrame.Whitespace(frame, 3));
} }
[Fact] [Fact]

View File

@ -0,0 +1,374 @@
// 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.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class RuntimeCodeGenerationRazorIntegrationTest : RazorIntegrationTestBase
{
internal override bool UseTwoPhaseCompilation => true;
[Fact]
public void CodeGeneration_ChildComponent_Simple()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent />");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithParameters()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class SomeType
{
}
public class MyComponent : BlazorComponent
{
public int IntProperty { get; set; }
public bool BoolProperty { get; set; }
public string StringProperty { get; set; }
public SomeType ObjectProperty { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent
IntProperty=""123""
BoolProperty=""true""
StringProperty=""My string""
ObjectProperty=""new SomeType()""/>");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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, ""IntProperty"", 123);
builder.AddAttribute(2, ""BoolProperty"", true);
builder.AddAttribute(3, ""StringProperty"", ""My string"");
builder.AddAttribute(4, ""ObjectProperty"", new SomeType());
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithExplicitStringParameter()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public string StringProperty { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent StringProperty=""@(42.ToString())"" />");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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, ""StringProperty"", 42.ToString());
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithLambdaEventHandler()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using System;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public UIEventHandler OnClick { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent OnClick=""Increment()""/>
@functions {
private int counter;
private void Increment() {
counter++;
}
}");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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, ""OnClick"", new Microsoft.AspNetCore.Blazor.UIEventHandler(e => Increment()));
builder.CloseComponent();
builder.AddContent(2, ""\n\n"");
}
#pragma warning restore 1998
private int counter;
private void Increment() {
counter++;
}
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim(), ignoreLineEndingDifferences: true);
}
[Fact]
public void CodeGeneration_ChildComponent_WithExplicitEventHandler()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using System;
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public UIEventHandler OnClick { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
@using Microsoft.AspNetCore.Blazor
<MyComponent OnClick=""@Increment""/>
@functions {
private int counter;
private void Increment(UIEventArgs e) {
counter++;
}
}");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Blazor;
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, ""OnClick"", new Microsoft.AspNetCore.Blazor.UIEventHandler(Increment));
builder.CloseComponent();
builder.AddContent(2, ""\n\n"");
}
#pragma warning restore 1998
private int counter;
private void Increment(UIEventArgs e) {
counter++;
}
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim());
}
[Fact]
public void CodeGeneration_ChildComponent_WithChildContent()
{
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public string MyAttr { get; set; }
public RenderFragment ChildContent { get; set; }
}
}
"));
// Act
var generated = CompileToCSharp(@"
@addTagHelper *, TestAssembly
<MyComponent MyAttr=""abc"">Some text<some-child a='1'>Nested text</some-child></MyComponent>");
// Assert
CompileToAssembly(generated);
Assert.Equal(@"
// <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, ""MyAttr"", ""abc"");
builder.AddAttribute(2, ""ChildContent"", (Microsoft.AspNetCore.Blazor.RenderFragment)((builder2) => {
builder2.AddContent(3, ""Some text"");
builder2.OpenElement(4, ""some-child"");
builder2.AddAttribute(5, ""a"", ""1"");
builder2.AddContent(6, ""Nested text"");
builder2.CloseElement();
}
));
builder.CloseComponent();
}
#pragma warning restore 1998
}
}
#pragma warning restore 1591
".Trim(), generated.Code.Trim());
}
}
}

View File

@ -282,6 +282,53 @@ namespace Test
Assert.False(attribute.IsStringProperty); Assert.False(attribute.IsStringProperty);
} }
[Fact] // UIEventHandler properties have some special intellisense behavior
public void Excecute_UIEventHandlerProperty_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public UIEventHandler OnClick { get; set; }
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new ComponentTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var components = ExcludeBuiltInComponents(context);
var component = Assert.Single(components);
Assert.Equal("TestAssembly", component.AssemblyName);
Assert.Equal("Test.MyComponent", component.Name);
var attribute = Assert.Single(component.BoundAttributes);
Assert.Equal("OnClick", attribute.Name);
Assert.Equal(BlazorApi.UIEventHandler.FullTypeName, attribute.TypeName);
Assert.False(attribute.HasIndexer);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
Assert.False(attribute.IsStringProperty);
Assert.True(attribute.IsUIEventHandlerProperty());
}
// For simplicity in testing, exlude the built-in components. We'll add more and we // For simplicity in testing, exlude the built-in components. We'll add more and we
// don't want to update the tests when that happens. // don't want to update the tests when that happens.
private TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context) private TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context)

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Blazor.Components; using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree; using Microsoft.AspNetCore.Blazor.RenderTree;
using Xunit; using Xunit;
@ -58,10 +59,21 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
Assert.Equal(attributeValue, frame.AttributeValue); Assert.Equal(attributeValue, frame.AttributeValue);
} }
public static void Attribute(RenderTreeFrame frame, string attributeName, Action<object> attributeValidator, int? sequence = null)
{
AssertFrame.Attribute(frame, attributeName, sequence);
attributeValidator(frame.AttributeValue);
}
public static void Component<T>(RenderTreeFrame frame, int? subtreeLength = null, int? sequence = null) where T : IComponent public static void Component<T>(RenderTreeFrame frame, int? subtreeLength = null, int? sequence = null) where T : IComponent
{
Component(frame, typeof(T).FullName, subtreeLength, sequence);
}
public static void Component(RenderTreeFrame frame, string typeName, int? subtreeLength = null, int? sequence = null)
{ {
Assert.Equal(RenderTreeFrameType.Component, frame.FrameType); Assert.Equal(RenderTreeFrameType.Component, frame.FrameType);
Assert.Equal(typeof(T), frame.ComponentType); Assert.Equal(typeName, frame.ComponentType.FullName);
if (subtreeLength.HasValue) if (subtreeLength.HasValue)
{ {
Assert.Equal(subtreeLength.Value, frame.ComponentSubtreeLength); Assert.Equal(subtreeLength.Value, frame.ComponentSubtreeLength);

View File

@ -122,8 +122,10 @@
<ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor.Razor.Extensions\Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj"> <ProjectReference Include="..\..\src\Microsoft.AspNetCore.Blazor.Razor.Extensions\Microsoft.AspNetCore.Blazor.Razor.Extensions.csproj">
<Name>Microsoft.AspNetCore.Blazor.Razor.Extensions</Name> <Name>Microsoft.AspNetCore.Blazor.Razor.Extensions</Name>
<Private>False</Private> <Private>False</Private>
<IncludeOutputGroupsInVSIX></IncludeOutputGroupsInVSIX> <IncludeOutputGroupsInVSIX>
<IncludeOutputGroupsInVSIXLocalOnly></IncludeOutputGroupsInVSIXLocalOnly> </IncludeOutputGroupsInVSIX>
<IncludeOutputGroupsInVSIXLocalOnly>
</IncludeOutputGroupsInVSIXLocalOnly>
</ProjectReference> </ProjectReference>
<Content Include="..\..\src\Microsoft.AspNetCore.Blazor.Razor.Extensions\bin\$(Configuration)\net461\Microsoft.AspNetCore.Blazor.Razor.Extensions.dll"> <Content Include="..\..\src\Microsoft.AspNetCore.Blazor.Razor.Extensions\bin\$(Configuration)\net461\Microsoft.AspNetCore.Blazor.Razor.Extensions.dll">
<Link>Microsoft.AspNetCore.Blazor.Razor.Extensions.dll</Link> <Link>Microsoft.AspNetCore.Blazor.Razor.Extensions.dll</Link>
@ -146,7 +148,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible> <Visible>false</Visible>
</Content> </Content>
<!-- <!--
This is built as a P2P by Microsoft.VisualStudio.LanguageServices.Blazor. This is required, adding the P2P This is built as a P2P by Microsoft.VisualStudio.LanguageServices.Blazor. This is required, adding the P2P
to this project will cause a NU1201 error that doesn't have a workaround. to this project will cause a NU1201 error that doesn't have a workaround.
@ -194,23 +195,23 @@
<WarningLevel>4</WarningLevel> <WarningLevel>4</WarningLevel>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.CoreUtility" Version="15.0.26201" /> <PackageReference Include="Microsoft.VisualStudio.CoreUtility" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Imaging" Version="15.0.26201" /> <PackageReference Include="Microsoft.VisualStudio.Imaging" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.OLE.Interop" Version="7.10.6071" /> <PackageReference Include="Microsoft.VisualStudio.OLE.Interop" Version="7.10.6071" />
<PackageReference Include="Microsoft.VisualStudio.SDK.EmbedInteropTypes" Version="15.0.10" /> <PackageReference Include="Microsoft.VisualStudio.SDK.EmbedInteropTypes" Version="15.0.16" />
<PackageReference Include="Microsoft.VisualStudio.Shell.15.0" Version="15.0.26201" /> <PackageReference Include="Microsoft.VisualStudio.Shell.15.0" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Framework" Version="15.0.26201" /> <PackageReference Include="Microsoft.VisualStudio.Shell.Framework" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop" Version="7.10.6071" /> <PackageReference Include="Microsoft.VisualStudio.Shell.Interop" Version="7.10.6072" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.10.0" Version="10.0.30319" /> <PackageReference Include="Microsoft.VisualStudio.Shell.Interop.10.0" Version="10.0.30320" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.11.0" Version="11.0.61030" /> <PackageReference Include="Microsoft.VisualStudio.Shell.Interop.11.0" Version="11.0.61031" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.12.0" Version="12.0.30110" /> <PackageReference Include="Microsoft.VisualStudio.Shell.Interop.12.0" Version="12.0.30110" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.8.0" Version="8.0.50727" /> <PackageReference Include="Microsoft.VisualStudio.Shell.Interop.8.0" Version="8.0.50728" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.9.0" Version="9.0.30729" /> <PackageReference Include="Microsoft.VisualStudio.Shell.Interop.9.0" Version="9.0.30730" />
<PackageReference Include="Microsoft.VisualStudio.TextManager.Interop" Version="7.10.6070" /> <PackageReference Include="Microsoft.VisualStudio.TextManager.Interop" Version="7.10.6071" />
<PackageReference Include="Microsoft.VisualStudio.TextManager.Interop.8.0" Version="8.0.50727" /> <PackageReference Include="Microsoft.VisualStudio.TextManager.Interop.8.0" Version="8.0.50728" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="15.0.240" /> <PackageReference Include="Microsoft.VisualStudio.Threading" Version="15.6.46" />
<PackageReference Include="Microsoft.VisualStudio.Utilities" Version="15.0.26201" /> <PackageReference Include="Microsoft.VisualStudio.Utilities" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="15.0.82" /> <PackageReference Include="Microsoft.VisualStudio.Validation" Version="15.3.15" />
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="15.5.100" /> <PackageReference Include="Microsoft.VSSDK.BuildTools" Version="15.5.100" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -6,6 +6,12 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Razor.Workspaces" Version="$(RazorPackageVersion)" /> <PackageReference Include="Microsoft.CodeAnalysis.Razor.Workspaces" Version="$(RazorPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.CoreUtility" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.LanguageServices.Razor" Version="$(RazorPackageVersion)" />
<PackageReference Include="Microsoft.VisualStudio.Text.UI.Wpf" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.TextManager.Interop" Version="7.10.6071" />
<PackageReference Include="Microsoft.VisualStudio.TextManager.Interop.8.0" Version="8.0.50728" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="15.6.46" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@ -0,0 +1,175 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel.Composition;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Razor;
using Microsoft.VisualStudio.Editor.Razor;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Editor;
using Microsoft.VisualStudio.Utilities;
namespace Microsoft.VisualStudio.LanguageServices.Blazor
{
[ContentType(RazorLanguage.ContentType)]
[TextViewRole(PredefinedTextViewRoles.Editable)]
[Export(typeof(IWpfTextViewConnectionListener))]
internal class BlazorOpenDocumentTracker : IWpfTextViewConnectionListener
{
private readonly RazorEditorFactoryService _editorFactory;
private readonly Workspace _workspace;
private readonly HashSet<IWpfTextView> _openViews;
private Type _codeGeneratorType;
[ImportingConstructor]
public BlazorOpenDocumentTracker(
RazorEditorFactoryService editorFactory,
[Import(typeof(VisualStudioWorkspace))] Workspace workspace)
{
if (editorFactory == null)
{
throw new ArgumentNullException(nameof(editorFactory));
}
if (workspace == null)
{
throw new ArgumentNullException(nameof(workspace));
}
_editorFactory = editorFactory;
_workspace = workspace;
_openViews = new HashSet<IWpfTextView>();
_workspace.WorkspaceChanged += Workspace_WorkspaceChanged;
}
public Workspace Workspace => _workspace;
public void SubjectBuffersConnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
_openViews.Add(textView);
}
public void SubjectBuffersDisconnected(IWpfTextView textView, ConnectionReason reason, Collection<ITextBuffer> subjectBuffers)
{
if (textView == null)
{
throw new ArgumentException(nameof(textView));
}
if (subjectBuffers == null)
{
throw new ArgumentNullException(nameof(subjectBuffers));
}
_openViews.Remove(textView);
}
// We're watching the Roslyn workspace for changes specifically because we want
// to know when the language service has processed a file change.
//
// It might be more elegant to use a file watcher rather than sniffing workspace events
// but there would be a delay between the file watcher and Roslyn processing the update.
private void Workspace_WorkspaceChanged(object sender, WorkspaceChangeEventArgs e)
{
switch (e.Kind)
{
case WorkspaceChangeKind.DocumentAdded:
case WorkspaceChangeKind.DocumentChanged:
case WorkspaceChangeKind.DocumentInfoChanged:
case WorkspaceChangeKind.DocumentReloaded:
case WorkspaceChangeKind.DocumentRemoved:
var document = e.NewSolution.GetDocument(e.DocumentId);
if (document == null || document.FilePath == null)
{
break;
}
if (!document.FilePath.EndsWith(".g.i.cs"))
{
break;
}
OnDeclarationsChanged();
break;
}
}
private void OnDeclarationsChanged()
{
// This is a design-time Razor file change.Go poke all of the open
// Razor documents and tell them to update.
var buffers = _openViews
.SelectMany(v => v.BufferGraph.GetTextBuffers(b => b.ContentType.IsOfType("RazorCSharp")))
.Distinct()
.ToArray();
if (_codeGeneratorType == null)
{
try
{
var assembly = Assembly.Load("Microsoft.VisualStudio.Web.Editors.Razor.4_0, Version=15.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a");
_codeGeneratorType = assembly.GetType("Microsoft.VisualStudio.Web.Editors.Razor.RazorCodeGenerator");
}
catch (Exception)
{
// If this fails, just unsubscribe. We won't be able to do our work, so just don't waste time.
_workspace.WorkspaceChanged -= Workspace_WorkspaceChanged;
}
}
foreach (var buffer in buffers)
{
try
{
var tryGetFromBuffer = _codeGeneratorType.GetMethod("TryGetFromBuffer", BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic);
var args = new object[] { buffer, null };
if (!(bool)tryGetFromBuffer.Invoke(null, args) || args[1] == null)
{
continue;
}
var field = _codeGeneratorType.GetField("_tagHelperDescriptorResolver", BindingFlags.Instance | BindingFlags.NonPublic);
var resolver = field.GetValue(args[1]);
if (resolver == null)
{
continue;
}
var reset = resolver.GetType().GetMethod("ResetTagHelperDescriptors", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
if (reset == null)
{
continue;
}
reset.Invoke(resolver, Array.Empty<object>());
}
catch (Exception)
{
// If this fails, just unsubscribe. We won't be able to do our work, so just don't waste time.
_workspace.WorkspaceChanged -= Workspace_WorkspaceChanged;
}
}
}
}
}