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>
<!-- 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>
@ -294,6 +294,8 @@
Outputs="@(_BlazorTempAssembly);$(NonExistentFile)"
Condition="'$(DesignTimeBuild)'!='true'">
<MakeDir Directories="%(_BlazorTempAssembly.RelativeDir)" />
<!-- 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,
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 CloseComponent = nameof(CloseElement);
public static readonly string CloseComponent = nameof(CloseComponent);
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 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
{
/// <summary>
/// Directs a <see cref="DocumentWriter"/> to use <see cref="BlazorIntermediateNodeWriter"/>.
/// Directs a <see cref="DocumentWriter"/> to use <see cref="BlazorRuntimeNodeWriter"/>.
/// </summary>
internal class BlazorCodeTarget : CodeTarget
{
@ -25,7 +25,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
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>()

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.
// 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 Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
@ -51,6 +54,40 @@ namespace Microsoft.AspNetCore.Blazor.Razor
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(
SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition)

View File

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

View File

@ -14,7 +14,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
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; }
@ -27,9 +32,23 @@ namespace Microsoft.AspNetCore.Blazor.Razor
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.
imports.AddRange(GetHierarchicalImports(ProjectEngine.FileSystem, projectItem));
@ -45,22 +64,16 @@ namespace Microsoft.AspNetCore.Blazor.Razor
private class VirtualProjectItem : RazorProjectItem
{
private readonly byte[] _defaultImportBytes;
private readonly byte[] _bytes;
private VirtualProjectItem()
public VirtualProjectItem(string content)
{
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);
_defaultImportBytes = new byte[preamble.Length + contentBytes.Length];
preamble.CopyTo(_defaultImportBytes, 0);
contentBytes.CopyTo(_defaultImportBytes, preamble.Length);
_bytes = new byte[preamble.Length + contentBytes.Length];
preamble.CopyTo(_bytes, 0);
contentBytes.CopyTo(_bytes, preamble.Length);
}
public override string BasePath => null;
@ -71,9 +84,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public override bool Exists => true;
public static VirtualProjectItem Instance { get; } = new VirtualProjectItem();
public override Stream Read() => new MemoryStream(_defaultImportBytes);
public override Stream Read() => new MemoryStream(_bytes);
}
}
}

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>
/// Generates the C# code corresponding to Razor source document contents.
/// </summary>
internal class BlazorIntermediateNodeWriter : IntermediateNodeWriter
internal class BlazorRuntimeNodeWriter : BlazorNodeWriter
{
// 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)
@ -48,16 +48,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
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)
{
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(
SourceSpan? razorTokenPosition,
TextPosition htmlNodePosition)
@ -434,7 +591,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
codeWriter.WriteEndMethodInvocation();
}
public void BeginWriteAttribute(CodeWriter codeWriter, string key)
public override void BeginWriteAttribute(CodeWriter codeWriter, string key)
{
codeWriter
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}")
@ -444,11 +601,6 @@ namespace Microsoft.AspNetCore.Blazor.Razor
.WriteParameterSeparator();
}
public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
{
context.CodeWriter.WriteUsing(node.Content, endLine: true);
}
private static string GetContent(HtmlContentIntermediateNode node)
{
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
{
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 =
SymbolDisplayFormat.FullyQualifiedFormat
.WithGlobalNamespaceStyle(SymbolDisplayGlobalNamespaceStyle.Omitted)
@ -109,6 +111,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
pb.IsEnum = true;
}
if (property.kind == PropertyKind.Delegate)
{
pb.Metadata.Add(UIEventHandlerPropertyMetadata, bool.TrueString);
}
xml = property.property.GetDocumentationCommentXml();
if (!string.IsNullOrEmpty(xml))
{
@ -145,13 +152,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
continue;
}
var kind = PropertyKind.Default;
if (properties.ContainsKey(property.Name))
{
// Not visible
kind = PropertyKind.Ignored;
continue;
}
var kind = PropertyKind.Default;
if (property.Parameters.Length != 0)
{
// Indexer
@ -175,6 +182,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor
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));
}
@ -190,6 +206,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
Ignored,
Default,
Enum,
Delegate,
}
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,
// 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);
OffsetBuilderVarNumber(1);
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.
// 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.Runtime.InteropServices;
using Microsoft.AspNetCore.Blazor.Components;
namespace Microsoft.AspNetCore.Blazor.RenderTree
{
@ -224,5 +224,34 @@ namespace Microsoft.AspNetCore.Blazor.RenderTree
internal RenderTreeFrame WithRegionSubtreeLength(int 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.
// 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.CodeAnalysis.CSharp;
using Xunit;
@ -18,7 +21,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
using namespace Test
namespace Test
{
public class MyComponent : BlazorComponent
{
@ -34,6 +37,29 @@ using namespace Test
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]
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.
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.Test.Helpers;
using Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
{
public class ComponentRenderingRazorIntegrationTest : RazorIntegrationTestBase
{
internal override bool UseTwoPhaseCompilation => true;
[Fact]
public void SupportsChildComponentsViaTemporarySyntax()
public void Render_ChildComponent_Simple()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent($"<c:{testComponentTypeName} />");
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent/>");
// Act
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 1, 0));
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 1, 0));
}
[Fact]
public void CanPassParametersToComponents()
public void Render_ChildComponent_WithParameters()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var testObjectTypeName = FullTypeName<SomeType>();
// TODO: Once we have the improved component tooling and can allow syntax
// like StringProperty="My string" or BoolProperty=true, update this
// test to use that syntax.
var component = CompileToComponent($"<c:{testComponentTypeName}" +
$" IntProperty=@(123)" +
$" BoolProperty=@true" +
$" StringProperty=@(\"My string\")" +
$" ObjectProperty=@(new {testObjectTypeName}()) />");
// 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; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent
IntProperty=""123""
BoolProperty=""true""
StringProperty=""My string""
ObjectProperty=""new SomeType()""/>");
// Act
var frames = GetRenderTree(component);
// Assert
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 5, 0),
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 5, 0),
frame => AssertFrame.Attribute(frame, "IntProperty", 123, 1),
frame => AssertFrame.Attribute(frame, "BoolProperty", true, 2),
frame => AssertFrame.Attribute(frame, "StringProperty", "My string", 3),
frame =>
{
AssertFrame.Attribute(frame, "ObjectProperty", 4);
Assert.IsType<SomeType>(frame.AttributeValue);
Assert.Equal("Test.SomeType", frame.AttributeValue.GetType().FullName);
});
}
[Fact]
public void CanIncludeChildrenInComponents()
public void Render_ChildComponent_WithExplicitStringParameter()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent($"<c:{testComponentTypeName} MyAttr=@(\"abc\")>" +
$"Some text" +
$"<some-child a='1'>Nested text</some-child>" +
$"</c:{testComponentTypeName}>");
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
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);
// Assert: component frames are correct
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 3, 0),
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 3, 0),
frame => AssertFrame.Attribute(frame, "MyAttr", "abc", 1),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 2));
// Assert: Captured ChildContent frames are correct
var childFrames = GetFrames((RenderFragment)frames[2].AttributeValue);
Assert.Collection(childFrames,
Assert.Collection(
childFrames,
frame => AssertFrame.Text(frame, "Some text", 3),
frame => AssertFrame.Element(frame, "some-child", 3, 4),
frame => AssertFrame.Attribute(frame, "a", "1", 5),
@ -82,21 +288,33 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
}
[Fact]
public void CanNestComponentChildContent()
public void Render_ChildComponent_Nested()
{
// Arrange/Act
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent(
$"<c:{testComponentTypeName}>" +
$"<c:{testComponentTypeName}>" +
$"Some text" +
$"</c:{testComponentTypeName}>" +
$"</c:{testComponentTypeName}>");
// Arrange
AdditionalSyntaxTrees.Add(CSharpSyntaxTree.ParseText(@"
using Microsoft.AspNetCore.Blazor;
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
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);
// Assert: outer component frames are correct
Assert.Collection(frames,
frame => AssertFrame.Component<TestComponent>(frame, 2, 0),
Assert.Collection(
frames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 0),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 1));
// 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
// level, but we could change that part of the implementation if we wanted.
var innerFrames = GetFrames((RenderFragment)frames[1].AttributeValue).ToArray();
Assert.Collection(innerFrames,
frame => AssertFrame.Component<TestComponent>(frame, 2, 2),
Assert.Collection(
innerFrames,
frame => AssertFrame.Component(frame, "Test.MyComponent", 2, 2),
frame => AssertFrame.Attribute(frame, RenderTreeBuilder.ChildContent, 3));
// 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));
}
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 Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Razor;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
using Microsoft.AspNetCore.Razor.Language;
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()
{
AdditionalSyntaxTrees = new List<SyntaxTree>();
AdditionalRazorItems = new List<RazorProjectItem>();
Configuration = BlazorExtensionInitializer.DefaultConfiguration;
FileSystem = new VirtualRazorProjectFileSystem();
WorkingDirectory = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ArbitraryWindowsPath : ArbitraryMacLinuxPath;
DefaultBaseNamespace = "Test"; // Matches the default working directory
DefaultFileName = "TestComponent.cshtml";
}
internal List<RazorProjectItem> AdditionalRazorItems { get; }
internal List<SyntaxTree> AdditionalSyntaxTrees { get; }
internal virtual RazorConfiguration Configuration { get; }
@ -71,6 +76,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
internal virtual string DefaultBaseNamespace { get; }
internal virtual string DefaultFileName { get; }
internal virtual bool DesignTime { get; }
internal virtual VirtualRazorProjectFileSystem FileSystem { get; }
@ -82,6 +89,9 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{
return RazorProjectEngine.Create(configuration, FileSystem, b =>
{
// Turn off checksums, we're testing code generation.
b.Features.Add(new SuppressChecksum());
BlazorExtensionInitializer.Register(b);
b.Features.Add(new CompilationTagHelperFeature());
@ -92,58 +102,83 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
});
}
protected CompileToCSharpResult CompileToCSharp(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)
internal RazorProjectItem CreateProjectItem(string chtmlRelativePath, string cshtmlContent)
{
// FilePaths in Razor are **always** are of the form '/a/b/c.cshtml'
var filePath = cshtmlRelativePath.Replace('\\', '/');
var filePath = chtmlRelativePath.Replace('\\', '/');
if (!filePath.StartsWith('/'))
{
filePath = '/' + filePath;
}
var projectItem = new VirtualProjectItem(
cshtmlRootPath,
filePath,
Path.Combine(cshtmlRootPath, cshtmlRelativePath),
cshtmlRelativePath,
Encoding.UTF8.GetBytes(cshtmlContent));
return new VirtualProjectItem(
WorkingDirectory,
filePath,
Path.Combine(WorkingDirectory, chtmlRelativePath),
chtmlRelativePath,
Encoding.UTF8.GetBytes(cshtmlContent.TrimStart()));
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlContent)
{
return CompileToCSharp(DefaultFileName, cshtmlContent);
}
protected CompileToCSharpResult CompileToCSharp(string cshtmlRelativePath, string cshtmlContent)
{
if (UseTwoPhaseCompilation)
{
// The first phase won't include any metadata references for component discovery. This mirrors
// what the build does.
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
var projectItem = CreateProjectItem(cshtmlRelativePath, cshtmlContent);
codeDocument = projectEngine.Process(projectItem);
var declaration = new CompileToCSharpResult
{
BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees),
CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
};
// Result of doing 'temp' compilation
var tempAssembly = CompileToAssembly(declaration, BaseCompilation);
var tempAssembly = CompileToAssembly(declaration);
// Add the 'temp' compilation as a metadata reference
var references = BaseCompilation.References.Concat(new[] { tempAssembly.Compilation.ToMetadataReference() }).ToArray();
projectEngine = CreateProjectEngine(BlazorExtensionInitializer.DefaultConfiguration, references);
// Result of real code
codeDocument = projectEngine.Process(projectItem);
// Now update the any additional files
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
{
BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees),
CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
@ -155,9 +190,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// This will include the built-in Blazor components.
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
{
BaseCompilation = BaseCompilation.AddSyntaxTrees(AdditionalSyntaxTrees),
CodeDocument = codeDocument,
Code = codeDocument.GetCSharpDocument().GeneratedCode,
Diagnostics = codeDocument.GetCSharpDocument().Diagnostics,
@ -167,19 +204,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRelativePath, string cshtmlContent)
{
return CompileToAssembly(WorkingDirectory, cshtmlRelativePath, cshtmlContent);
}
protected CompileToAssemblyResult CompileToAssembly(string cshtmlRootDirectory, string cshtmlRelativePath, string cshtmlContent)
{
var cSharpResult = CompileToCSharp(cshtmlRootDirectory, cshtmlRelativePath, cshtmlContent);
var cSharpResult = CompileToCSharp(cshtmlRelativePath, cshtmlContent);
return CompileToAssembly(cSharpResult);
}
protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult, CSharpCompilation baseCompilation = null)
protected CompileToAssemblyResult CompileToAssembly(CompileToCSharpResult cSharpResult)
{
baseCompilation = baseCompilation ?? BaseCompilation;
if (cSharpResult.Diagnostics.Any())
{
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[]
{
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())
{
compilation.Emit(peStream);
var diagnostics = compilation
.GetDiagnostics()
.Where(d => d.Severity != DiagnosticSeverity.Hidden);
return new CompileToAssemblyResult
{
Compilation = compilation,
@ -206,11 +243,12 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
Assembly = diagnostics.Any() ? null : Assembly.Load(peStream.ToArray())
};
}
}
protected IComponent CompileToComponent(string cshtmlSource)
{
var assemblyResult = CompileToAssembly(WorkingDirectory, DefaultFileName, cshtmlSource);
var assemblyResult = CompileToAssembly(DefaultFileName, cshtmlSource);
var componentFullTypeName = $"{DefaultBaseNamespace}.{Path.GetFileNameWithoutExtension(DefaultFileName)}";
return CompileToComponent(assemblyResult, componentFullTypeName);
@ -223,8 +261,6 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
protected IComponent CompileToComponent(CompileToAssemblyResult assemblyResult, string fullTypeName)
{
Assert.Empty(assemblyResult.Diagnostics);
var componentType = assemblyResult.Assembly.GetType(fullTypeName);
if (componentType == null)
{
@ -255,6 +291,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
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 string Code { get; set; }
public IEnumerable<RazorDiagnostic> Diagnostics { get; set; }
@ -284,5 +322,62 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
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
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Whitespace(frame, 0),
frame => AssertFrame.Text(frame, "Hello", 1),
frame => AssertFrame.Whitespace(frame, 2),
frame => AssertFrame.Whitespace(frame, 3), // @((object)null)
frame => AssertFrame.Whitespace(frame, 4),
frame => AssertFrame.Text(frame, "123", 5),
frame => AssertFrame.Whitespace(frame, 6),
frame => AssertFrame.Text(frame, new object().ToString(), 7),
frame => AssertFrame.Whitespace(frame, 8));
frame => AssertFrame.Text(frame, "Hello", 0),
frame => AssertFrame.Whitespace(frame, 1),
frame => AssertFrame.Whitespace(frame, 2), // @((object)null)
frame => AssertFrame.Whitespace(frame, 3),
frame => AssertFrame.Text(frame, "123", 4),
frame => AssertFrame.Whitespace(frame, 5),
frame => AssertFrame.Text(frame, new object().ToString(), 6),
frame => AssertFrame.Whitespace(frame, 7));
}
[Fact]
@ -70,12 +69,11 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
// Assert
var frames = GetRenderTree(component);
Assert.Collection(frames,
frame => AssertFrame.Whitespace(frame, 0),
frame => AssertFrame.Text(frame, "First", 1),
frame => AssertFrame.Text(frame, "Second", 1),
frame => AssertFrame.Text(frame, "Third", 1),
frame => AssertFrame.Whitespace(frame, 2),
frame => AssertFrame.Whitespace(frame, 3));
frame => AssertFrame.Text(frame, "First", 0),
frame => AssertFrame.Text(frame, "Second", 0),
frame => AssertFrame.Text(frame, "Third", 0),
frame => AssertFrame.Whitespace(frame, 1),
frame => AssertFrame.Whitespace(frame, 2));
}
[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);
}
[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
// don't want to update the tests when that happens.
private TagHelperDescriptor[] ExcludeBuiltInComponents(TagHelperDescriptorProviderContext context)

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Xunit;
@ -58,10 +59,21 @@ namespace Microsoft.AspNetCore.Blazor.Test.Helpers
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
{
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(typeof(T), frame.ComponentType);
Assert.Equal(typeName, frame.ComponentType.FullName);
if (subtreeLength.HasValue)
{
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">
<Name>Microsoft.AspNetCore.Blazor.Razor.Extensions</Name>
<Private>False</Private>
<IncludeOutputGroupsInVSIX></IncludeOutputGroupsInVSIX>
<IncludeOutputGroupsInVSIXLocalOnly></IncludeOutputGroupsInVSIXLocalOnly>
<IncludeOutputGroupsInVSIX>
</IncludeOutputGroupsInVSIX>
<IncludeOutputGroupsInVSIXLocalOnly>
</IncludeOutputGroupsInVSIXLocalOnly>
</ProjectReference>
<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>
@ -146,7 +148,6 @@
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<Visible>false</Visible>
</Content>
<!--
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.
@ -194,23 +195,23 @@
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.VisualStudio.CoreUtility" Version="15.0.26201" />
<PackageReference Include="Microsoft.VisualStudio.Imaging" Version="15.0.26201" />
<PackageReference Include="Microsoft.VisualStudio.CoreUtility" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Imaging" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.OLE.Interop" Version="7.10.6071" />
<PackageReference Include="Microsoft.VisualStudio.SDK.EmbedInteropTypes" Version="15.0.10" />
<PackageReference Include="Microsoft.VisualStudio.Shell.15.0" Version="15.0.26201" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Framework" Version="15.0.26201" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop" Version="7.10.6071" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.10.0" Version="10.0.30319" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.11.0" Version="11.0.61030" />
<PackageReference Include="Microsoft.VisualStudio.SDK.EmbedInteropTypes" Version="15.0.16" />
<PackageReference Include="Microsoft.VisualStudio.Shell.15.0" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Framework" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop" Version="7.10.6072" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.10.0" Version="10.0.30320" />
<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.8.0" Version="8.0.50727" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.9.0" Version="9.0.30729" />
<PackageReference Include="Microsoft.VisualStudio.TextManager.Interop" Version="7.10.6070" />
<PackageReference Include="Microsoft.VisualStudio.TextManager.Interop.8.0" Version="8.0.50727" />
<PackageReference Include="Microsoft.VisualStudio.Threading" Version="15.0.240" />
<PackageReference Include="Microsoft.VisualStudio.Utilities" Version="15.0.26201" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="15.0.82" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.8.0" Version="8.0.50728" />
<PackageReference Include="Microsoft.VisualStudio.Shell.Interop.9.0" Version="9.0.30730" />
<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" />
<PackageReference Include="Microsoft.VisualStudio.Utilities" Version="15.6.27413" />
<PackageReference Include="Microsoft.VisualStudio.Validation" Version="15.3.15" />
<PackageReference Include="Microsoft.VSSDK.BuildTools" Version="15.5.100" />
</ItemGroup>
<ItemGroup>

View File

@ -6,6 +6,12 @@
<ItemGroup>
<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>

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