Add event handlers as tag helpers
This change adds support for mapping DOM event handlers as tag helpers
that function in a bi-modal way.
This is a new first-class feature for DOM events, and replaces a few
workarounds like using `@onclick(...)` or `click=@{ ... }`. I haven't
removed those things yet, this is a first pass to get the new support
in, we'll remove those things when we're totally satisfied.
When used with a string like `<button onclick="foo" />` the result is
a simple HTML attribute .
But when used with an implicit expression like
`<button onclick="@Foo" />` or
`<button onclick="@(x => Clicked = true)" />` a C# function is bound to
the click event from the DOM.
This commit is contained in:
parent
3369208c28
commit
c3366bc956
|
|
@ -1,17 +1,17 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Extensions;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class BindLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
|
||||
{
|
||||
// Run after event handler pass
|
||||
public override int Order => base.Order + 50;
|
||||
|
||||
protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
|
||||
{
|
||||
var @namespace = documentNode.FindPrimaryNamespace();
|
||||
|
|
@ -35,7 +35,8 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
var attributeNode = node.Children[j] as ComponentAttributeExtensionNode;
|
||||
if (attributeNode != null &&
|
||||
attributeNode.TagHelper != null &&
|
||||
attributeNode.TagHelper.IsBindTagHelper())
|
||||
attributeNode.TagHelper.IsBindTagHelper() &&
|
||||
attributeNode.AttributeName.StartsWith("bind"))
|
||||
{
|
||||
RewriteUsage(node, j, attributeNode);
|
||||
}
|
||||
|
|
@ -153,11 +154,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
{
|
||||
// Skip anything we can't understand. It's important that we don't crash, that will bring down
|
||||
// the build.
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.CreateBindAttribute_InvalidSyntax(
|
||||
attributeNode.Source,
|
||||
attributeNode.AttributeName));
|
||||
return;
|
||||
}
|
||||
|
||||
var originalContent = GetAttributeContent(attributeNode);
|
||||
if (string.IsNullOrEmpty(originalContent))
|
||||
var original = GetAttributeContent(attributeNode);
|
||||
if (string.IsNullOrEmpty(original.Content))
|
||||
{
|
||||
// This can happen in error cases, the parser will already have flagged this
|
||||
// as an error, so ignore it.
|
||||
|
|
@ -166,13 +170,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
// Look for a matching format node. If we find one then we need to pass the format into the
|
||||
// two nodes we generate.
|
||||
string format = null;
|
||||
IntermediateToken format = null;
|
||||
if (TryGetFormatNode(node,
|
||||
attributeNode,
|
||||
valueAttributeName,
|
||||
out var formatNode))
|
||||
{
|
||||
// Don't write the format out as its own attribute;
|
||||
// Don't write the format out as its own attribute, just capture it as a string
|
||||
// or expression.
|
||||
node.Children.Remove(formatNode);
|
||||
format = GetAttributeContent(formatNode);
|
||||
}
|
||||
|
|
@ -190,23 +195,32 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
//
|
||||
// BindMethods.GetValue(<code>) OR
|
||||
// BindMethods.GetValue(<code>, <format>)
|
||||
//
|
||||
// For now, the way this is done isn't debuggable. But since the expression
|
||||
// passed here must be an LValue, it's probably not important.
|
||||
var valueNodeContent = format == null ?
|
||||
$"{BlazorApi.BindMethods.GetValue}({originalContent})" :
|
||||
$"{BlazorApi.BindMethods.GetValue}({originalContent}, {format})";
|
||||
valueAttributeNode.Children.Clear();
|
||||
valueAttributeNode.Children.Add(new CSharpExpressionIntermediateNode()
|
||||
|
||||
var expression = new CSharpExpressionIntermediateNode();
|
||||
valueAttributeNode.Children.Add(expression);
|
||||
|
||||
expression.Children.Add(new IntermediateToken()
|
||||
{
|
||||
Children =
|
||||
Content = $"{BlazorApi.BindMethods.GetValue}(",
|
||||
Kind = TokenKind.CSharp
|
||||
});
|
||||
expression.Children.Add(original);
|
||||
|
||||
if (!string.IsNullOrEmpty(format?.Content))
|
||||
{
|
||||
expression.Children.Add(new IntermediateToken()
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = valueNodeContent,
|
||||
Kind = TokenKind.CSharp
|
||||
},
|
||||
},
|
||||
Content = ", ",
|
||||
Kind = TokenKind.CSharp,
|
||||
});
|
||||
expression.Children.Add(format);
|
||||
}
|
||||
|
||||
expression.Children.Add(new IntermediateToken()
|
||||
{
|
||||
Content = ")",
|
||||
Kind = TokenKind.CSharp,
|
||||
});
|
||||
|
||||
var changeAttributeNode = new ComponentAttributeExtensionNode(attributeNode)
|
||||
|
|
@ -230,20 +244,19 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// BindMethods.SetValueHandler(__value => <code> = __value, <code>) OR
|
||||
// BindMethods.SetValueHandler(__value => <code> = __value, <code>, <format>)
|
||||
//
|
||||
// For now, the way this is done isn't debuggable. But since the expression
|
||||
// passed here must be an LValue, it's probably not important.
|
||||
// Note that the linemappings here are applied to the value attribute, not the change attribute.
|
||||
string changeAttributeContent = null;
|
||||
if (changeAttributeNode.BoundAttribute == null && format == null)
|
||||
{
|
||||
changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {originalContent} = __value, {originalContent})";
|
||||
changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})";
|
||||
}
|
||||
else if (changeAttributeNode.BoundAttribute == null && format != null)
|
||||
{
|
||||
changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {originalContent} = __value, {originalContent}, {format})";
|
||||
changeAttributeContent = $"{BlazorApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})";
|
||||
}
|
||||
else
|
||||
{
|
||||
changeAttributeContent = $"__value => {originalContent} = __value";
|
||||
changeAttributeContent = $"__value => {original.Content} = __value";
|
||||
}
|
||||
|
||||
changeAttributeNode.Children.Clear();
|
||||
|
|
@ -394,24 +407,25 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
return false;
|
||||
}
|
||||
|
||||
private static string GetAttributeContent(ComponentAttributeExtensionNode node)
|
||||
private static IntermediateToken GetAttributeContent(ComponentAttributeExtensionNode node)
|
||||
{
|
||||
if (node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute. We want to turn it into
|
||||
// an expression.
|
||||
return "\"" + ((IntermediateToken)htmlContentNode.Children.Single()).Content + "\"";
|
||||
var content = "\"" + ((IntermediateToken)htmlContentNode.Children.Single()).Content + "\"";
|
||||
return new IntermediateToken() { Kind = TokenKind.CSharp, Content = content };
|
||||
}
|
||||
else if (node.Children[0] is CSharpExpressionIntermediateNode cSharpNode)
|
||||
{
|
||||
// This case can be hit when the attribute has an explicit @ inside, which
|
||||
// 'escapes' any special sugar we provide for codegen.
|
||||
return ((IntermediateToken)cSharpNode.Children.Single()).Content;
|
||||
return ((IntermediateToken)cSharpNode.Children.Single());
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
return ((IntermediateToken)node.Children.Single()).Content;
|
||||
return ((IntermediateToken)node.Children.Single());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -82,11 +82,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
public static readonly string GetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.GetValue";
|
||||
|
||||
public static readonly string GetEventHandlerValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue";
|
||||
|
||||
public static readonly string SetValue = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValue";
|
||||
|
||||
public static readonly string SetValueHandler = "Microsoft.AspNetCore.Blazor.Components.BindMethods.SetValueHandler";
|
||||
}
|
||||
|
||||
public static class EventHandlerAttribute
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.EventHandlerAttribute";
|
||||
}
|
||||
|
||||
public static class UIEventHandler
|
||||
{
|
||||
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.UIEventHandler";
|
||||
|
|
|
|||
|
|
@ -105,5 +105,37 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
Environment.NewLine + string.Join(Environment.NewLine, attributes.Select(p => p.TagHelper.DisplayName)));
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor EventHandler_Duplicates =
|
||||
new RazorDiagnosticDescriptor(
|
||||
"BL9990",
|
||||
() => "The attribute '{0}' was matched by multiple event handlers attributes. Duplicates:{1}",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic CreateEventHandler_Duplicates(SourceSpan? source, string attribute, ComponentAttributeExtensionNode[] attributes)
|
||||
{
|
||||
var diagnostic = RazorDiagnostic.Create(
|
||||
EventHandler_Duplicates,
|
||||
source ?? SourceSpan.Undefined,
|
||||
attribute,
|
||||
Environment.NewLine + string.Join(Environment.NewLine, attributes.Select(p => p.TagHelper.DisplayName)));
|
||||
return diagnostic;
|
||||
}
|
||||
|
||||
public static readonly RazorDiagnosticDescriptor BindAttribute_InvalidSyntax =
|
||||
new RazorDiagnosticDescriptor(
|
||||
"BL9991",
|
||||
() => "The attribute names could not be inferred from bind attibute '{0}'. Bind attributes should be of the form" +
|
||||
"'bind', 'bind-value' or 'bind-value-change'",
|
||||
RazorDiagnosticSeverity.Error);
|
||||
|
||||
public static RazorDiagnostic CreateBindAttribute_InvalidSyntax(SourceSpan? source, string attribute)
|
||||
{
|
||||
var diagnostic = RazorDiagnostic.Create(
|
||||
BindAttribute_InvalidSyntax,
|
||||
source ?? SourceSpan.Undefined,
|
||||
attribute);
|
||||
return diagnostic;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -65,17 +65,18 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
builder.Features.Add(new ConfigureBlazorCodeGenerationOptions());
|
||||
|
||||
// Implementation of components
|
||||
// Blazor-specific passes, in order.
|
||||
builder.Features.Add(new ComponentDocumentClassifierPass());
|
||||
builder.Features.Add(new ComplexAttributeContentPass());
|
||||
builder.Features.Add(new ComponentLoweringPass());
|
||||
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
|
||||
|
||||
// Implementation of bind
|
||||
builder.Features.Add(new BindLoweringPass());
|
||||
builder.Features.Add(new BindTagHelperDescriptorProvider());
|
||||
builder.Features.Add(new EventHandlerLoweringPass());
|
||||
builder.Features.Add(new ComponentLoweringPass());
|
||||
builder.Features.Add(new OrphanTagHelperLoweringPass());
|
||||
|
||||
builder.Features.Add(new ComponentTagHelperDescriptorProvider());
|
||||
builder.Features.Add(new BindTagHelperDescriptorProvider());
|
||||
builder.Features.Add(new EventHandlerTagHelperDescriptorProvider());
|
||||
|
||||
if (builder.Configuration.ConfigurationName == DeclarationConfiguration.ConfigurationName)
|
||||
{
|
||||
// This is for 'declaration only' processing. We don't want to try and emit any method bodies during
|
||||
|
|
|
|||
|
|
@ -33,7 +33,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
public static readonly string RuntimeName = "Blazor.IComponent";
|
||||
|
||||
public readonly static string TagHelperKind = "Blazor.Component-0.1";
|
||||
}
|
||||
|
||||
public static class EventHandler
|
||||
{
|
||||
public static readonly string EventArgsType = "Blazor.EventHandler.EventArgs";
|
||||
|
||||
public static readonly string RuntimeName = "Blazor.None";
|
||||
|
||||
public readonly static string TagHelperKind = "Blazor.EventHandler-0.1";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,14 +33,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
private readonly ScopeStack _scopeStack = new ScopeStack();
|
||||
private string _unconsumedHtml;
|
||||
private IList<object> _currentAttributeValues;
|
||||
private List<IntermediateToken> _currentAttributeValues;
|
||||
private IDictionary<string, PendingAttribute> _currentElementAttributes = new Dictionary<string, PendingAttribute>();
|
||||
private IList<PendingAttributeToken> _currentElementAttributeTokens = new List<PendingAttributeToken>();
|
||||
private int _sourceSequence = 0;
|
||||
|
||||
private struct PendingAttribute
|
||||
{
|
||||
public object AttributeValue;
|
||||
public List<IntermediateToken> Values { get; set; }
|
||||
}
|
||||
|
||||
private struct PendingAttributeToken
|
||||
|
|
@ -180,19 +180,22 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// ... so to avoid losing whitespace, convert the prefix to a further token in the list
|
||||
if (!string.IsNullOrEmpty(node.Prefix))
|
||||
{
|
||||
_currentAttributeValues.Add(node.Prefix);
|
||||
_currentAttributeValues.Add(new IntermediateToken() { Kind = TokenKind.Html, Content = node.Prefix });
|
||||
}
|
||||
|
||||
_currentAttributeValues.Add((IntermediateToken)node.Children.Single());
|
||||
for (var i = 0; i < node.Children.Count; i++)
|
||||
{
|
||||
_currentAttributeValues.Add((IntermediateToken)node.Children[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public override void WriteHtmlAttribute(CodeRenderingContext context, HtmlAttributeIntermediateNode node)
|
||||
{
|
||||
_currentAttributeValues = new List<object>();
|
||||
_currentAttributeValues = new List<IntermediateToken>();
|
||||
context.RenderChildren(node);
|
||||
_currentElementAttributes[node.AttributeName] = new PendingAttribute
|
||||
{
|
||||
AttributeValue = _currentAttributeValues
|
||||
Values = _currentAttributeValues,
|
||||
};
|
||||
_currentAttributeValues = null;
|
||||
}
|
||||
|
|
@ -205,7 +208,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
}
|
||||
|
||||
var stringContent = ((IntermediateToken)node.Children.Single()).Content;
|
||||
_currentAttributeValues.Add(node.Prefix + stringContent);
|
||||
_currentAttributeValues.Add(new IntermediateToken() { Kind = TokenKind.Html, Content = node.Prefix + stringContent, });
|
||||
}
|
||||
|
||||
public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentIntermediateNode node)
|
||||
|
|
@ -263,14 +266,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
foreach (var attribute in nextTag.Attributes)
|
||||
{
|
||||
WriteAttribute(codeWriter, attribute.Key, attribute.Value);
|
||||
var token = new IntermediateToken() { Kind = TokenKind.Html, Content = attribute.Value };
|
||||
WriteAttribute(codeWriter, attribute.Key, new[] { token });
|
||||
}
|
||||
|
||||
if (_currentElementAttributes.Count > 0)
|
||||
{
|
||||
foreach (var pair in _currentElementAttributes)
|
||||
{
|
||||
WriteAttribute(codeWriter, pair.Key, pair.Value.AttributeValue);
|
||||
WriteAttribute(codeWriter, pair.Key, pair.Value.Values);
|
||||
}
|
||||
_currentElementAttributes.Clear();
|
||||
}
|
||||
|
|
@ -331,10 +335,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// The @bind(X, Y, Z, ...) syntax is special. We convert it to a pair of attributes:
|
||||
// [1] value=@BindMethods.GetValue(X, Y, Z, ...)
|
||||
var valueParams = bindMatch.Groups[1].Value;
|
||||
WriteAttribute(context.CodeWriter, "value", new IntermediateToken
|
||||
WriteAttribute(context.CodeWriter, "value", new[]
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $"{BlazorApi.BindMethods.GetValue}({valueParams})"
|
||||
new IntermediateToken
|
||||
{
|
||||
Kind = TokenKind.CSharp,
|
||||
Content = $"{BlazorApi.BindMethods.GetValue}({valueParams})"
|
||||
}
|
||||
});
|
||||
|
||||
// [2] @onchange(BindSetValue(parsed => { X = parsed; }, X, Y, Z, ...))
|
||||
|
|
@ -467,22 +474,14 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
// 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?.IsDelegateProperty() ?? false)
|
||||
{
|
||||
// We always surround the expression with the delegate constructor. This makes type
|
||||
// inference inside lambdas, and method group conversion do the right thing.
|
||||
IntermediateToken token = null;
|
||||
if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
|
||||
if ((node.Children[0] as CSharpExpressionIntermediateNode) != null)
|
||||
{
|
||||
token = cSharpNode.Children[0] as IntermediateToken;
|
||||
token = node.Children[0].Children[0] as IntermediateToken;
|
||||
}
|
||||
else
|
||||
{
|
||||
|
|
@ -498,11 +497,24 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
context.CodeWriter.Write(")");
|
||||
}
|
||||
}
|
||||
else if ((cSharpNode = node.Children[0] as CSharpExpressionIntermediateNode) != null)
|
||||
else if (node.Children[0] is CSharpExpressionIntermediateNode cSharpNode)
|
||||
{
|
||||
context.CodeWriter.Write(((IntermediateToken)cSharpNode.Children[0]).Content);
|
||||
// We don't allow mixed content in component attributes. If this happens, then
|
||||
// we should make sure that all of the tokens are the same kind. We report an
|
||||
// error if user code tries to do this, so this check is to catch bugs in the
|
||||
// compiler.
|
||||
for (var i = 0; i < cSharpNode.Children.Count; i++)
|
||||
{
|
||||
var token = (IntermediateToken)cSharpNode.Children[i];
|
||||
if (!token.IsCSharp)
|
||||
{
|
||||
throw new InvalidOperationException("Unexpected mixed content in a component.");
|
||||
}
|
||||
|
||||
context.CodeWriter.Write(token.Content);
|
||||
}
|
||||
}
|
||||
else if ((htmlNode = node.Children[0] as HtmlContentIntermediateNode) != null)
|
||||
else if (node.Children[0] is HtmlContentIntermediateNode htmlNode)
|
||||
{
|
||||
// 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);
|
||||
|
|
@ -549,7 +561,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
return document.Substring(tagToken.Position.Position + offset, tagToken.Name.Length);
|
||||
}
|
||||
|
||||
private void WriteAttribute(CodeWriter codeWriter, string key, object value)
|
||||
private void WriteAttribute(CodeWriter codeWriter, string key, IList<IntermediateToken> value)
|
||||
{
|
||||
BeginWriteAttribute(codeWriter, key);
|
||||
WriteAttributeValue(codeWriter, value);
|
||||
|
|
@ -586,64 +598,89 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static void WriteAttributeValue(CodeWriter writer, object value)
|
||||
// There are a few cases here, we need to handle:
|
||||
// - Pure HTML
|
||||
// - Pure CSharp
|
||||
// - Mixed HTML and CSharp
|
||||
//
|
||||
// Only the mixed case is complicated, we want to turn it into code that will concatenate
|
||||
// the values into a string at runtime.
|
||||
|
||||
private static void WriteAttributeValue(CodeWriter writer, IList<IntermediateToken> tokens)
|
||||
{
|
||||
if (value == null)
|
||||
if (tokens == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(value));
|
||||
throw new ArgumentNullException(nameof(tokens));
|
||||
}
|
||||
|
||||
switch (value)
|
||||
var hasHtml = false;
|
||||
var hasCSharp = false;
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
case string valueString:
|
||||
writer.WriteStringLiteral(valueString);
|
||||
break;
|
||||
case IntermediateToken token:
|
||||
if (tokens[i].IsCSharp)
|
||||
{
|
||||
hasCSharp |= true;
|
||||
}
|
||||
else
|
||||
{
|
||||
hasHtml |= true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasHtml && hasCSharp)
|
||||
{
|
||||
// If it's a C# expression, we have to wrap it in parens, otherwise things like ternary
|
||||
// expressions don't compose with concatenation. However, this is a little complicated
|
||||
// because C# tokens themselves aren't guaranteed to be distinct expressions. We want
|
||||
// to treat all contiguous C# tokens as a single expression.
|
||||
var insideCSharp = false;
|
||||
for (var i = 0; i < tokens.Count; i++)
|
||||
{
|
||||
var token = tokens[i];
|
||||
if (token.IsCSharp)
|
||||
{
|
||||
if (token.IsCSharp)
|
||||
if (!insideCSharp)
|
||||
{
|
||||
writer.Write(token.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
writer.WriteStringLiteral(token.Content);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case IEnumerable<object> concatenatedValues:
|
||||
{
|
||||
var first = true;
|
||||
foreach (var concatenatedValue in concatenatedValues)
|
||||
{
|
||||
if (first)
|
||||
{
|
||||
first = false;
|
||||
}
|
||||
else
|
||||
if (i != 0)
|
||||
{
|
||||
writer.Write(" + ");
|
||||
}
|
||||
|
||||
// If it's a C# expression, we have to wrap it in parens, otherwise
|
||||
// things like ternary expressions don't compose with concatenation
|
||||
var isCSharp = concatenatedValue is IntermediateToken intermediateToken
|
||||
&& intermediateToken.Kind == TokenKind.CSharp;
|
||||
if (isCSharp)
|
||||
{
|
||||
writer.Write("(");
|
||||
}
|
||||
|
||||
WriteAttributeValue(writer, concatenatedValue);
|
||||
|
||||
if (isCSharp)
|
||||
{
|
||||
writer.Write(")");
|
||||
}
|
||||
writer.Write("(");
|
||||
insideCSharp = true;
|
||||
}
|
||||
break;
|
||||
|
||||
writer.Write(token.Content);
|
||||
}
|
||||
default:
|
||||
throw new ArgumentException($"Unsupported attribute value type: {value.GetType().FullName}");
|
||||
else
|
||||
{
|
||||
if (insideCSharp)
|
||||
{
|
||||
writer.Write(")");
|
||||
insideCSharp = false;
|
||||
}
|
||||
|
||||
if (i != 0)
|
||||
{
|
||||
writer.Write(" + ");
|
||||
}
|
||||
|
||||
writer.WriteStringLiteral(token.Content);
|
||||
}
|
||||
}
|
||||
|
||||
if (insideCSharp)
|
||||
{
|
||||
writer.Write(")");
|
||||
}
|
||||
}
|
||||
else if (hasCSharp)
|
||||
{
|
||||
writer.Write(string.Join("", tokens.Select(t => t.Content)));
|
||||
}
|
||||
else if (hasHtml)
|
||||
{
|
||||
writer.WriteStringLiteral(string.Join("", tokens.Select(t => t.Content)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,6 +89,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count == 1 &&
|
||||
node.Children[0] is CSharpCodeIntermediateNode cSharpCodeNode)
|
||||
{
|
||||
// This is the case when an attribute contains a code block @{ ... }
|
||||
// We don't support this.
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count > 1)
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
|
|
|
|||
|
|
@ -81,33 +81,10 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
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 (HasComplexChildContent(propertyNode))
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
propertyNode,
|
||||
propertyNode.AttributeName));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
node.Children[i] = new ComponentAttributeExtensionNode(propertyNode);
|
||||
}
|
||||
else if (node.Children[i] is TagHelperHtmlAttributeIntermediateNode htmlNode)
|
||||
{
|
||||
if (HasComplexChildContent(htmlNode))
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.Create_UnsupportedComplexContent(
|
||||
htmlNode,
|
||||
htmlNode.AttributeName));
|
||||
node.Children.RemoveAt(i);
|
||||
continue;
|
||||
}
|
||||
|
||||
// For any nodes that don't map to a component property we won't have type information
|
||||
// but these should follow the same path through the runtime.
|
||||
var attributeNode = new ComponentAttributeExtensionNode(htmlNode);
|
||||
|
|
@ -154,31 +131,5 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool HasComplexChildContent(IntermediateNode node)
|
||||
{
|
||||
if (node.Children.Count == 1 &&
|
||||
node.Children[0] is HtmlAttributeIntermediateNode htmlNode &&
|
||||
htmlNode.Children.Count > 1)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count == 1 &&
|
||||
node.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.
|
||||
return true;
|
||||
}
|
||||
else if (node.Children.Count > 1)
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,158 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.AspNetCore.Razor.Language.Intermediate;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class EventHandlerLoweringPass : 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 event handler *usage* we need to rewrite the tag helper node to map to basic constructs.
|
||||
var nodes = documentNode.FindDescendantNodes<TagHelperIntermediateNode>();
|
||||
for (var i = 0; i < nodes.Count; i++)
|
||||
{
|
||||
var node = nodes[i];
|
||||
|
||||
ProcessDuplicates(node);
|
||||
|
||||
for (var j = node.Children.Count - 1; j >= 0; j--)
|
||||
{
|
||||
var attributeNode = node.Children[j] as ComponentAttributeExtensionNode;
|
||||
if (attributeNode != null &&
|
||||
attributeNode.TagHelper != null &&
|
||||
attributeNode.TagHelper.IsEventHandlerTagHelper())
|
||||
{
|
||||
RewriteUsage(node, j, attributeNode);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ProcessDuplicates(TagHelperIntermediateNode node)
|
||||
{
|
||||
// Reverse order because we will remove nodes.
|
||||
//
|
||||
// Each 'property' node could be duplicated if there are multiple tag helpers that match that
|
||||
// particular attribute. This is likely to happen when a component also defines something like
|
||||
// OnClick. We want to remove the 'onclick' and let it fall back to be handled by the component.
|
||||
for (var i = node.Children.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var attributeNode = node.Children[i] as ComponentAttributeExtensionNode;
|
||||
if (attributeNode != null &&
|
||||
attributeNode.TagHelper != null &&
|
||||
attributeNode.TagHelper.IsEventHandlerTagHelper())
|
||||
{
|
||||
for (var j = 0; j < node.Children.Count; j++)
|
||||
{
|
||||
var duplicate = node.Children[j] as ComponentAttributeExtensionNode;
|
||||
if (duplicate != null &&
|
||||
duplicate.TagHelper != null &&
|
||||
duplicate.TagHelper.IsComponentTagHelper() &&
|
||||
duplicate.AttributeName == attributeNode.AttributeName)
|
||||
{
|
||||
// Found a duplicate - remove the 'fallback' in favor of the
|
||||
// more specific tag helper.
|
||||
node.Children.RemoveAt(i);
|
||||
node.TagHelpers.Remove(attributeNode.TagHelper);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still have duplicates at this point then they are genuine conflicts.
|
||||
var duplicates = node.Children
|
||||
.OfType<ComponentAttributeExtensionNode>()
|
||||
.Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false)
|
||||
.GroupBy(p => p.AttributeName)
|
||||
.Where(g => g.Count() > 1);
|
||||
|
||||
foreach (var duplicate in duplicates)
|
||||
{
|
||||
node.Diagnostics.Add(BlazorDiagnosticFactory.CreateEventHandler_Duplicates(
|
||||
node.Source,
|
||||
duplicate.Key,
|
||||
duplicate.ToArray()));
|
||||
foreach (var property in duplicate)
|
||||
{
|
||||
node.Children.Remove(property);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RewriteUsage(TagHelperIntermediateNode node, int index, ComponentAttributeExtensionNode attributeNode)
|
||||
{
|
||||
var original = GetAttributeContent(attributeNode);
|
||||
if (string.IsNullOrEmpty(original.Content))
|
||||
{
|
||||
// This can happen in error cases, the parser will already have flagged this
|
||||
// as an error, so ignore it.
|
||||
return;
|
||||
}
|
||||
|
||||
var rewrittenNode = new ComponentAttributeExtensionNode(attributeNode);
|
||||
node.Children[index] = rewrittenNode;
|
||||
|
||||
// Now rewrite the content of the value node to look like:
|
||||
//
|
||||
// BindMethods.GetEventHandlerValue<TDelegate>(<code>)
|
||||
//
|
||||
// This method is overloaded on string and TDelegate, which means that it will put the code in the
|
||||
// correct context for intellisense when typing in the attribute.
|
||||
var eventArgsType = attributeNode.TagHelper.GetEventArgsType();
|
||||
|
||||
rewrittenNode.Children.Clear();
|
||||
rewrittenNode.Children.Add(new CSharpExpressionIntermediateNode()
|
||||
{
|
||||
Children =
|
||||
{
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = $"{BlazorApi.BindMethods.GetEventHandlerValue}<{eventArgsType}>(",
|
||||
Kind = TokenKind.CSharp
|
||||
},
|
||||
original,
|
||||
new IntermediateToken()
|
||||
{
|
||||
Content = $")",
|
||||
Kind = TokenKind.CSharp
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
private static IntermediateToken GetAttributeContent(ComponentAttributeExtensionNode node)
|
||||
{
|
||||
if (node.Children[0] is HtmlContentIntermediateNode htmlContentNode)
|
||||
{
|
||||
// This case can be hit for a 'string' attribute. We want to turn it into
|
||||
// an expression.
|
||||
var content = "\"" + ((IntermediateToken)htmlContentNode.Children.Single()).Content + "\"";
|
||||
return new IntermediateToken() { Content = content, Kind = TokenKind.CSharp, };
|
||||
}
|
||||
else if (node.Children[0] is CSharpExpressionIntermediateNode cSharpNode)
|
||||
{
|
||||
// This case can be hit when the attribute has an explicit @ inside, which
|
||||
// 'escapes' any special sugar we provide for codegen.
|
||||
return ((IntermediateToken)cSharpNode.Children.Single());
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is the common case for 'mixed' content
|
||||
return ((IntermediateToken)node.Children.Single());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,215 @@
|
|||
// 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 Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor
|
||||
{
|
||||
internal class EventHandlerTagHelperDescriptorProvider : ITagHelperDescriptorProvider
|
||||
{
|
||||
public int Order { get; set; }
|
||||
|
||||
public RazorEngine Engine { get; set; }
|
||||
|
||||
public void Execute(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
var compilation = context.GetCompilation();
|
||||
if (compilation == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bindMethods = compilation.GetTypeByMetadataName(BlazorApi.BindMethods.FullTypeName);
|
||||
if (bindMethods == null)
|
||||
{
|
||||
// If we can't find BindMethods, then just bail. We won't be able to compile the
|
||||
// generated code anyway.
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
var eventHandlerData = GetEventHandlerData(compilation);
|
||||
|
||||
foreach (var tagHelper in CreateEventHandlerTagHelpers(eventHandlerData))
|
||||
{
|
||||
context.Results.Add(tagHelper);
|
||||
}
|
||||
}
|
||||
|
||||
private List<EventHandlerData> GetEventHandlerData(Compilation compilation)
|
||||
{
|
||||
var eventHandlerAttribute = compilation.GetTypeByMetadataName(BlazorApi.EventHandlerAttribute.FullTypeName);
|
||||
if (eventHandlerAttribute == null)
|
||||
{
|
||||
// This won't likely happen, but just in case.
|
||||
return new List<EventHandlerData>();
|
||||
}
|
||||
|
||||
var types = new List<INamedTypeSymbol>();
|
||||
var visitor = new EventHandlerDataVisitor(types);
|
||||
|
||||
// Visit the primary output of this compilation, as well as all references.
|
||||
visitor.Visit(compilation.Assembly);
|
||||
foreach (var reference in compilation.References)
|
||||
{
|
||||
// We ignore .netmodules here - there really isn't a case where they are used by user code
|
||||
// even though the Roslyn APIs all support them.
|
||||
if (compilation.GetAssemblyOrModuleSymbol(reference) is IAssemblySymbol assembly)
|
||||
{
|
||||
visitor.Visit(assembly);
|
||||
}
|
||||
}
|
||||
|
||||
var results = new List<EventHandlerData>();
|
||||
|
||||
for (var i = 0; i < types.Count; i++)
|
||||
{
|
||||
var type = types[i];
|
||||
var attributes = type.GetAttributes();
|
||||
|
||||
// Not handling duplicates here for now since we're the primary ones extending this.
|
||||
// If we see users adding to the set of event handler constructs we will want to add deduplication
|
||||
// and potentially diagnostics.
|
||||
for (var j = 0; j < attributes.Length; j++)
|
||||
{
|
||||
var attribute = attributes[j];
|
||||
|
||||
if (attribute.AttributeClass == eventHandlerAttribute)
|
||||
{
|
||||
results.Add(new EventHandlerData(
|
||||
type.ContainingAssembly.Name,
|
||||
type.ToDisplayString(),
|
||||
(string)attribute.ConstructorArguments[0].Value,
|
||||
(INamedTypeSymbol)attribute.ConstructorArguments[1].Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private List<TagHelperDescriptor> CreateEventHandlerTagHelpers(List<EventHandlerData> data)
|
||||
{
|
||||
var results = new List<TagHelperDescriptor>();
|
||||
|
||||
for (var i = 0; i < data.Count; i++)
|
||||
{
|
||||
var entry = data[i];
|
||||
|
||||
var builder = TagHelperDescriptorBuilder.Create(BlazorMetadata.EventHandler.TagHelperKind, entry.Attribute, entry.Assembly);
|
||||
builder.Documentation = string.Format(
|
||||
Resources.EventHandlerTagHelper_Documentation,
|
||||
entry.Attribute,
|
||||
entry.EventArgsType.ToDisplayString());
|
||||
|
||||
builder.Metadata.Add(BlazorMetadata.SpecialKindKey, BlazorMetadata.EventHandler.TagHelperKind);
|
||||
builder.Metadata.Add(BlazorMetadata.EventHandler.EventArgsType, entry.EventArgsType.ToDisplayString());
|
||||
builder.Metadata[TagHelperMetadata.Runtime.Name] = BlazorMetadata.EventHandler.RuntimeName;
|
||||
|
||||
// WTE has a bug in 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
builder.SetTypeName(entry.TypeName);
|
||||
|
||||
builder.TagMatchingRule(rule =>
|
||||
{
|
||||
rule.TagName = "*";
|
||||
|
||||
rule.Attribute(a =>
|
||||
{
|
||||
a.Name = entry.Attribute;
|
||||
a.NameComparisonMode = RequiredAttributeDescriptor.NameComparisonMode.FullMatch;
|
||||
});
|
||||
});
|
||||
|
||||
builder.BindAttribute(a =>
|
||||
{
|
||||
a.Documentation = string.Format(
|
||||
Resources.EventHandlerTagHelper_Documentation,
|
||||
entry.Attribute,
|
||||
entry.EventArgsType.ToDisplayString());
|
||||
|
||||
a.Name = entry.Attribute;
|
||||
|
||||
// Use a string here so that we get HTML context by default.
|
||||
a.TypeName = typeof(string).FullName;
|
||||
|
||||
// WTE has a bug 15.7p1 where a Tag Helper without a display-name that looks like
|
||||
// a C# property will crash trying to create the toolips.
|
||||
a.SetPropertyName(entry.Attribute);
|
||||
});
|
||||
|
||||
results.Add(builder.Build());
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
private struct EventHandlerData
|
||||
{
|
||||
public EventHandlerData(
|
||||
string assembly,
|
||||
string typeName,
|
||||
string element,
|
||||
INamedTypeSymbol eventArgsType)
|
||||
{
|
||||
Assembly = assembly;
|
||||
TypeName = typeName;
|
||||
Attribute = element;
|
||||
EventArgsType = eventArgsType;
|
||||
}
|
||||
|
||||
public string Assembly { get; }
|
||||
|
||||
public string TypeName { get; }
|
||||
|
||||
public string Attribute { get; }
|
||||
|
||||
public INamedTypeSymbol EventArgsType { get; }
|
||||
}
|
||||
|
||||
private class EventHandlerDataVisitor : SymbolVisitor
|
||||
{
|
||||
private List<INamedTypeSymbol> _results;
|
||||
|
||||
public EventHandlerDataVisitor(List<INamedTypeSymbol> results)
|
||||
{
|
||||
_results = results;
|
||||
}
|
||||
|
||||
public override void VisitNamedType(INamedTypeSymbol symbol)
|
||||
{
|
||||
if (symbol.Name == "EventHandlers" && symbol.DeclaredAccessibility == Accessibility.Public)
|
||||
{
|
||||
_results.Add(symbol);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitNamespace(INamespaceSymbol symbol)
|
||||
{
|
||||
foreach (var member in symbol.GetMembers())
|
||||
{
|
||||
Visit(member);
|
||||
}
|
||||
}
|
||||
|
||||
public override void VisitAssembly(IAssemblySymbol symbol)
|
||||
{
|
||||
// This as a simple yet high-value optimization that excludes the vast majority of
|
||||
// assemblies that (by definition) can't contain a component.
|
||||
if (symbol.Name != null && !symbol.Name.StartsWith("System.", StringComparison.Ordinal))
|
||||
{
|
||||
Visit(symbol.GlobalNamespace);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -105,6 +105,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor {
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Sets the '{0}' attribute to the provided string or delegate value. A delegate value should be of type '{1}'..
|
||||
/// </summary>
|
||||
internal static string EventHandlerTagHelper_Documentation {
|
||||
get {
|
||||
return ResourceManager.GetString("EventHandlerTagHelper_Documentation", resourceCulture);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Looks up a localized string similar to Declares an interface implementation for the current document..
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -132,6 +132,9 @@
|
|||
<data name="BindTagHelper_Fallback_Format_Documentation" xml:space="preserve">
|
||||
<value>Specifies a format to convert the value specified by the corresponding bind attribute. For example: <code>format-value="..."</code> will apply a format string to the value specified in <code>bind-value-...</code>. The format string can currently only be used with expressions of type <code>DateTime</code>.</value>
|
||||
</data>
|
||||
<data name="EventHandlerTagHelper_Documentation" xml:space="preserve">
|
||||
<value>Sets the '{0}' attribute to the provided string or delegate value. A delegate value should be of type '{1}'.</value>
|
||||
</data>
|
||||
<data name="ImplementsDirective_Description" xml:space="preserve">
|
||||
<value>Declares an interface implementation for the current document.</value>
|
||||
</data>
|
||||
|
|
|
|||
|
|
@ -90,5 +90,28 @@ namespace Microsoft.AspNetCore.Blazor.Razor
|
|||
|
||||
return !tagHelper.Metadata.ContainsKey(BlazorMetadata.SpecialKindKey);
|
||||
}
|
||||
|
||||
public static bool IsEventHandlerTagHelper(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
return
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.SpecialKindKey, out var kind) &&
|
||||
string.Equals(BlazorMetadata.EventHandler.TagHelperKind, kind);
|
||||
}
|
||||
|
||||
public static string GetEventArgsType(this TagHelperDescriptor tagHelper)
|
||||
{
|
||||
if (tagHelper == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(tagHelper));
|
||||
}
|
||||
|
||||
tagHelper.Metadata.TryGetValue(BlazorMetadata.EventHandler.EventArgsType, out var result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,24 @@ namespace Microsoft.AspNetCore.Blazor.Components
|
|||
value == default ? null
|
||||
: (format == null ? value.ToString() : value.ToString(format));
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static string GetEventHandlerValue<T>(string value)
|
||||
where T : UIEventArgs
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
public static UIEventHandler GetEventHandlerValue<T>(Action<T> value)
|
||||
where T : UIEventArgs
|
||||
{
|
||||
return e => value((T)e);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Not intended to be used directly.
|
||||
/// </summary>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Components
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = true)]
|
||||
public sealed class EventHandlerAttribute : Attribute
|
||||
{
|
||||
public EventHandlerAttribute(string attributeName, Type eventArgsType)
|
||||
{
|
||||
if (attributeName == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(attributeName));
|
||||
}
|
||||
|
||||
if (eventArgsType == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(eventArgsType));
|
||||
}
|
||||
|
||||
AttributeName = attributeName;
|
||||
EventArgsType = eventArgsType;
|
||||
}
|
||||
|
||||
public string AttributeName { get; }
|
||||
|
||||
public Type EventArgsType { get; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
// 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;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Components
|
||||
{
|
||||
[EventHandler("onchange", typeof(UIChangeEventArgs))]
|
||||
[EventHandler("onclick", typeof(UIMouseEventArgs))]
|
||||
public static class EventHandlers
|
||||
{
|
||||
}
|
||||
}
|
||||
|
|
@ -488,24 +488,35 @@ namespace Test
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_Bind_With_IncorrectAttributeNames()
|
||||
public void Render_BindFallback_InvalidSyntax_TooManyParts()
|
||||
{
|
||||
//more than 3 parts
|
||||
Assert.Throws<InvalidOperationException>(() => CompileToComponent(@"
|
||||
// Arrange & Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""text"" bind-first-second-third=""Text"" />
|
||||
@functions {
|
||||
public string Text { get; set; } = ""text"";
|
||||
}"));
|
||||
}");
|
||||
|
||||
//ends with '-'
|
||||
Assert.Throws<InvalidOperationException>(() => CompileToComponent(@"
|
||||
// Assert
|
||||
var diagnostic = Assert.Single(generated.Diagnostics);
|
||||
Assert.Equal("BL9991", diagnostic.Id);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Render_BindFallback_InvalidSyntax_TrailingDash()
|
||||
{
|
||||
// Arrange & Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@addTagHelper *, TestAssembly
|
||||
<input type=""text"" bind-first-=""Text"" />
|
||||
@functions {
|
||||
public string Text { get; set; } = ""text"";
|
||||
}"));
|
||||
}");
|
||||
|
||||
// Assert
|
||||
var diagnostic = Assert.Single(generated.Diagnostics);
|
||||
Assert.Equal("BL9991", diagnostic.Id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,5 +205,56 @@ namespace Test
|
|||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventHandler_OnElement_WithString()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
<input onclick=""foo"" />");
|
||||
|
||||
// Assert
|
||||
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
|
||||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventHandler_OnElement_WithLambdaDelegate()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@using Microsoft.AspNetCore.Blazor
|
||||
<input onclick=""@(x => { })"" />");
|
||||
|
||||
// Assert
|
||||
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
|
||||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventHandler_OnElement_WithDelegate()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@using Microsoft.AspNetCore.Blazor
|
||||
<input onclick=""@OnClick"" />
|
||||
@functions {
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
}");
|
||||
|
||||
// Assert
|
||||
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
|
||||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -180,6 +180,22 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
frame => AssertFrame.Attribute(frame, "attr", "Hello, WORLD with number 246!", 1));
|
||||
}
|
||||
|
||||
// This test exercises the case where two IntermediateTokens are part of the same expression.
|
||||
// In these case they are split by a comment.
|
||||
[Fact]
|
||||
public void SupportsAttributesWithInterpolatedStringExpressionValues_SplitByComment()
|
||||
{
|
||||
// Arrange/Act
|
||||
var component = CompileToComponent(
|
||||
"@{ var myValue = \"world\"; var myNum=123; }"
|
||||
+ "<elem attr=\"Hello, @myValue.ToUpperInvariant() with number @(myN@* Blazor is Blawesome! *@um*2)!\" />");
|
||||
|
||||
// Assert
|
||||
Assert.Collection(GetRenderTree(component),
|
||||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "attr", "Hello, WORLD with number 246!", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsAttributesWithInterpolatedTernaryExpressionValues()
|
||||
{
|
||||
|
|
@ -220,7 +236,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
frame => AssertFrame.Element(frame, "elem", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "data-abc", "Hello", 1));
|
||||
}
|
||||
|
||||
|
||||
[Fact(Skip = "Currently broken due to #219. TODO: Once the issue is fixed, re-enable this test, remove the test below, and remove the implementation of its workaround.")]
|
||||
public void SupportsDataDashAttributesWithCSharpExpressionValues()
|
||||
{
|
||||
|
|
@ -465,6 +481,86 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
|
|||
frame => AssertFrame.Text(frame, "\n", 3));
|
||||
}
|
||||
|
||||
[Fact] // In this case, onclick is just a normal HTML attribute
|
||||
public void SupportsEventHandlerWithString()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
<button onclick=""function(){console.log('hello');};"" />");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "button", 2, 0),
|
||||
frame => AssertFrame.Attribute(frame, "onclick", "function(){console.log('hello');};", 1));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsEventHandlerWithLambda()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
<button onclick=""@(x => Clicked = true)"" />
|
||||
@functions {
|
||||
public bool Clicked { get; set; }
|
||||
}");
|
||||
|
||||
var clicked = component.GetType().GetProperty("Clicked");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "button", 2, 0),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "onclick", 1);
|
||||
|
||||
var func = Assert.IsType<UIEventHandler>(frame.AttributeValue);
|
||||
Assert.False((bool)clicked.GetValue(component));
|
||||
|
||||
func(new UIMouseEventArgs());
|
||||
Assert.True((bool)clicked.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Whitespace(frame, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsEventHandlerWithMethodGroup()
|
||||
{
|
||||
// Arrange
|
||||
var component = CompileToComponent(@"
|
||||
@using Microsoft.AspNetCore.Blazor
|
||||
<button onclick=""@OnClick"" @onclick(OnClick)/>
|
||||
@functions {
|
||||
public void OnClick(UIMouseEventArgs e) { Clicked = true; }
|
||||
public bool Clicked { get; set; }
|
||||
}");
|
||||
|
||||
var clicked = component.GetType().GetProperty("Clicked");
|
||||
|
||||
// Act
|
||||
var frames = GetRenderTree(component);
|
||||
|
||||
// Assert
|
||||
Assert.Collection(frames,
|
||||
frame => AssertFrame.Element(frame, "button", 2, 0),
|
||||
frame =>
|
||||
{
|
||||
AssertFrame.Attribute(frame, "onclick", 1);
|
||||
|
||||
var func = Assert.IsType<UIEventHandler>(frame.AttributeValue);
|
||||
Assert.False((bool)clicked.GetValue(component));
|
||||
|
||||
func(new UIMouseEventArgs());
|
||||
Assert.True((bool)clicked.GetValue(component));
|
||||
},
|
||||
frame => AssertFrame.Whitespace(frame, 2));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SupportsTwoWayBindingForBoolValues()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Build.Test
|
||||
{
|
||||
|
|
@ -258,5 +259,56 @@ namespace Test
|
|||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventHandler_OnElement_WithString()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
<input onclick=""foo"" />");
|
||||
|
||||
// Assert
|
||||
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
|
||||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventHandler_OnElement_WithLambdaDelegate()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@using Microsoft.AspNetCore.Blazor
|
||||
<input onclick=""@(x => { })"" />");
|
||||
|
||||
// Assert
|
||||
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
|
||||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventHandler_OnElement_WithDelegate()
|
||||
{
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
var generated = CompileToCSharp(@"
|
||||
@using Microsoft.AspNetCore.Blazor
|
||||
<input onclick=""@OnClick"" />
|
||||
@functions {
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
}");
|
||||
|
||||
// Assert
|
||||
AssertDocumentNodeMatchesBaseline(generated.CodeDocument);
|
||||
AssertCSharpDocumentMatchesBaseline(generated.CodeDocument);
|
||||
CompileToAssembly(generated);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
// <auto-generated/>
|
||||
#pragma warning disable 1591
|
||||
namespace Test
|
||||
{
|
||||
#line hidden
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
#line 1 "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__() {
|
||||
}
|
||||
#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 = Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(OnClick);
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
|
||||
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
Document -
|
||||
NamespaceDeclaration - - Test
|
||||
UsingDirective - (3:1,1 [12] ) - System
|
||||
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
|
||||
UsingDirective - (53:3,1 [17] ) - System.Linq
|
||||
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
|
||||
UsingDirective - (1:0,1 [33] x:\dir\subdir\Test\TestComponent.cshtml) - Microsoft.AspNetCore.Blazor
|
||||
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
|
||||
DesignTimeDirective -
|
||||
DirectiveToken - (14:0,14 [32] ) - "*, Microsoft.AspNetCore.Blazor"
|
||||
DirectiveToken - (14:0,14 [9] ) - "*, Test"
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - #pragma warning disable 0414
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - private static System.Object __o = null;
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - #pragma warning restore 0414
|
||||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlContent - (34:0,34 [2] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (34:0,34 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - <input
|
||||
HtmlAttribute - - =" - "
|
||||
CSharpExpressionAttributeValue - -
|
||||
IntermediateToken - - CSharp - Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(
|
||||
IntermediateToken - (53:1,17 [7] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - OnClick
|
||||
IntermediateToken - - CSharp - )
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - />
|
||||
HtmlContent - (64:1,28 [2] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (64:1,28 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
|
||||
CSharpCode - (78:2,12 [49] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (78:2,12 [49] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n void OnClick(UIMouseEventArgs e) {\n }\n
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
Source Location: (1:0,1 [33] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
|using Microsoft.AspNetCore.Blazor|
|
||||
Generated Location: (257:10,0 [33] )
|
||||
|using Microsoft.AspNetCore.Blazor|
|
||||
|
||||
Source Location: (53:1,17 [7] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
|OnClick|
|
||||
Generated Location: (1032:27,136 [7] )
|
||||
|OnClick|
|
||||
|
||||
Source Location: (78:2,12 [49] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
|
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
|
|
||||
Generated Location: (1155:31,12 [49] )
|
||||
|
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
|
|
||||
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// <auto-generated/>
|
||||
#pragma warning disable 1591
|
||||
namespace Test
|
||||
{
|
||||
#line hidden
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
#line 1 "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__() {
|
||||
}
|
||||
#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 = Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(x => { });
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
Document -
|
||||
NamespaceDeclaration - - Test
|
||||
UsingDirective - (3:1,1 [12] ) - System
|
||||
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
|
||||
UsingDirective - (53:3,1 [17] ) - System.Linq
|
||||
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
|
||||
UsingDirective - (1:0,1 [33] x:\dir\subdir\Test\TestComponent.cshtml) - Microsoft.AspNetCore.Blazor
|
||||
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
|
||||
DesignTimeDirective -
|
||||
DirectiveToken - (14:0,14 [32] ) - "*, Microsoft.AspNetCore.Blazor"
|
||||
DirectiveToken - (14:0,14 [9] ) - "*, Test"
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - #pragma warning disable 0414
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - private static System.Object __o = null;
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - #pragma warning restore 0414
|
||||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlContent - (34:0,34 [2] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (34:0,34 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - <input
|
||||
HtmlAttribute - - =" - "
|
||||
CSharpExpressionAttributeValue - -
|
||||
IntermediateToken - - CSharp - Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(
|
||||
IntermediateToken - (54:1,18 [8] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - x => { }
|
||||
IntermediateToken - - CSharp - )
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - />
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
Source Location: (1:0,1 [33] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
|using Microsoft.AspNetCore.Blazor|
|
||||
Generated Location: (257:10,0 [33] )
|
||||
|using Microsoft.AspNetCore.Blazor|
|
||||
|
||||
Source Location: (54:1,18 [8] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
|x => { }|
|
||||
Generated Location: (1032:27,136 [8] )
|
||||
|x => { }|
|
||||
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
// <auto-generated/>
|
||||
#pragma warning disable 1591
|
||||
namespace Test
|
||||
{
|
||||
#line hidden
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
|
||||
{
|
||||
#pragma warning disable 219
|
||||
private void __RazorDirectiveTokenHelpers__() {
|
||||
}
|
||||
#pragma warning restore 219
|
||||
#pragma warning disable 0414
|
||||
private static System.Object __o = null;
|
||||
#pragma warning restore 0414
|
||||
#pragma warning disable 1998
|
||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
__o = Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>("foo");
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
Document -
|
||||
NamespaceDeclaration - - Test
|
||||
UsingDirective - (3:1,1 [12] ) - System
|
||||
UsingDirective - (18:2,1 [32] ) - System.Collections.Generic
|
||||
UsingDirective - (53:3,1 [17] ) - System.Linq
|
||||
UsingDirective - (73:4,1 [28] ) - System.Threading.Tasks
|
||||
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
|
||||
DesignTimeDirective -
|
||||
DirectiveToken - (14:0,14 [32] ) - "*, Microsoft.AspNetCore.Blazor"
|
||||
DirectiveToken - (14:0,14 [9] ) - "*, Test"
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - #pragma warning disable 0414
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - private static System.Object __o = null;
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - #pragma warning restore 0414
|
||||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - <input
|
||||
HtmlAttribute - - =" - "
|
||||
CSharpExpressionAttributeValue - -
|
||||
IntermediateToken - - CSharp - Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(
|
||||
IntermediateToken - - CSharp - "foo"
|
||||
IntermediateToken - - CSharp - )
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - />
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// <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.OpenElement(0, "input");
|
||||
builder.AddAttribute(1, "onclick", Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(OnClick));
|
||||
builder.CloseElement();
|
||||
builder.AddContent(2, "\n");
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
#line 3 "x:\dir\subdir\Test\TestComponent.cshtml"
|
||||
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
|
||||
#line default
|
||||
#line hidden
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
Document -
|
||||
NamespaceDeclaration - - Test
|
||||
UsingDirective - (3:1,1 [14] ) - System
|
||||
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
|
||||
UsingDirective - (53:3,1 [19] ) - System.Linq
|
||||
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
|
||||
UsingDirective - (1:0,1 [35] x:\dir\subdir\Test\TestComponent.cshtml) - Microsoft.AspNetCore.Blazor
|
||||
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
|
||||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - <input
|
||||
HtmlAttribute - - =" - "
|
||||
CSharpExpressionAttributeValue - -
|
||||
IntermediateToken - - CSharp - Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(
|
||||
IntermediateToken - (53:1,17 [7] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - OnClick
|
||||
IntermediateToken - - CSharp - )
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - />
|
||||
HtmlContent - (64:1,28 [2] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (64:1,28 [2] x:\dir\subdir\Test\TestComponent.cshtml) - Html - \n
|
||||
CSharpCode - (78:2,12 [49] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
IntermediateToken - (78:2,12 [49] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - \n void OnClick(UIMouseEventArgs e) {\n }\n
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
Source Location: (78:2,12 [49] x:\dir\subdir\Test\TestComponent.cshtml)
|
||||
|
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
|
|
||||
Generated Location: (964:23,12 [49] )
|
||||
|
|
||||
void OnClick(UIMouseEventArgs e) {
|
||||
}
|
||||
|
|
||||
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// <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.OpenElement(0, "input");
|
||||
builder.AddAttribute(1, "onclick", Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(x => { }));
|
||||
builder.CloseElement();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
Document -
|
||||
NamespaceDeclaration - - Test
|
||||
UsingDirective - (3:1,1 [14] ) - System
|
||||
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
|
||||
UsingDirective - (53:3,1 [19] ) - System.Linq
|
||||
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
|
||||
UsingDirective - (1:0,1 [35] x:\dir\subdir\Test\TestComponent.cshtml) - Microsoft.AspNetCore.Blazor
|
||||
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
|
||||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - <input
|
||||
HtmlAttribute - - =" - "
|
||||
CSharpExpressionAttributeValue - -
|
||||
IntermediateToken - - CSharp - Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(
|
||||
IntermediateToken - (54:1,18 [8] x:\dir\subdir\Test\TestComponent.cshtml) - CSharp - x => { }
|
||||
IntermediateToken - - CSharp - )
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - />
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
// <auto-generated/>
|
||||
#pragma warning disable 1591
|
||||
namespace Test
|
||||
{
|
||||
#line hidden
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
public class TestComponent : Microsoft.AspNetCore.Blazor.Components.BlazorComponent
|
||||
{
|
||||
#pragma warning disable 1998
|
||||
protected override void BuildRenderTree(Microsoft.AspNetCore.Blazor.RenderTree.RenderTreeBuilder builder)
|
||||
{
|
||||
base.BuildRenderTree(builder);
|
||||
builder.OpenElement(0, "input");
|
||||
builder.AddAttribute(1, "onclick", Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>("foo"));
|
||||
builder.CloseElement();
|
||||
}
|
||||
#pragma warning restore 1998
|
||||
}
|
||||
}
|
||||
#pragma warning restore 1591
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Document -
|
||||
NamespaceDeclaration - - Test
|
||||
UsingDirective - (3:1,1 [14] ) - System
|
||||
UsingDirective - (18:2,1 [34] ) - System.Collections.Generic
|
||||
UsingDirective - (53:3,1 [19] ) - System.Linq
|
||||
UsingDirective - (73:4,1 [30] ) - System.Threading.Tasks
|
||||
ClassDeclaration - - public - TestComponent - Microsoft.AspNetCore.Blazor.Components.BlazorComponent -
|
||||
MethodDeclaration - - protected override - void - BuildRenderTree
|
||||
CSharpCode -
|
||||
IntermediateToken - - CSharp - base.BuildRenderTree(builder);
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - <input
|
||||
HtmlAttribute - - =" - "
|
||||
CSharpExpressionAttributeValue - -
|
||||
IntermediateToken - - CSharp - Microsoft.AspNetCore.Blazor.Components.BindMethods.GetEventHandlerValue<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>(
|
||||
IntermediateToken - - CSharp - "foo"
|
||||
IntermediateToken - - CSharp - )
|
||||
HtmlContent -
|
||||
IntermediateToken - - Html - />
|
||||
|
|
@ -0,0 +1,121 @@
|
|||
// 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.Linq;
|
||||
using Microsoft.AspNetCore.Razor.Language;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
using Microsoft.CodeAnalysis.Razor;
|
||||
using Xunit;
|
||||
|
||||
namespace Microsoft.AspNetCore.Blazor.Razor.Extensions
|
||||
{
|
||||
public class EventHandlerTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
|
||||
{
|
||||
[Fact]
|
||||
public void Excecute_EventHandler_CreatesDescriptor()
|
||||
{
|
||||
// Arrange
|
||||
var compilation = BaseCompilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(@"
|
||||
using System;
|
||||
using Microsoft.AspNetCore.Blazor;
|
||||
using Microsoft.AspNetCore.Blazor.Components;
|
||||
|
||||
namespace Test
|
||||
{
|
||||
[EventHandler(""onclick"", typeof(Action<UIMouseEventArgs>))]
|
||||
public class EventHandlers
|
||||
{
|
||||
}
|
||||
}
|
||||
"));
|
||||
|
||||
Assert.Empty(compilation.GetDiagnostics());
|
||||
|
||||
var context = TagHelperDescriptorProviderContext.Create();
|
||||
context.SetCompilation(compilation);
|
||||
|
||||
var provider = new EventHandlerTagHelperDescriptorProvider();
|
||||
|
||||
// Act
|
||||
provider.Execute(context);
|
||||
|
||||
// Assert
|
||||
var matches = GetEventHandlerTagHelpers(context);
|
||||
var item = Assert.Single(matches);
|
||||
|
||||
// These are features Event Handler Tag Helpers don't use. Verifying them once here and
|
||||
// then ignoring them.
|
||||
Assert.Empty(item.AllowedChildTags);
|
||||
Assert.Null(item.TagOutputHint);
|
||||
|
||||
// These are features that are invariants of all Event Handler Helpers. Verifying them once
|
||||
// here and then ignoring them.
|
||||
Assert.Empty(item.Diagnostics);
|
||||
Assert.False(item.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.EventHandler.TagHelperKind, item.Kind);
|
||||
Assert.Equal(BlazorMetadata.EventHandler.RuntimeName, item.Metadata[TagHelperMetadata.Runtime.Name]);
|
||||
Assert.False(item.IsDefaultKind());
|
||||
Assert.False(item.KindUsesDefaultTagHelperRuntime());
|
||||
|
||||
Assert.Equal(
|
||||
"Sets the 'onclick' attribute to the provided string or delegate value. " +
|
||||
"A delegate value should be of type 'System.Action<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>'.",
|
||||
item.Documentation);
|
||||
|
||||
// These are all trivally derived from the assembly/namespace/type name
|
||||
Assert.Equal("TestAssembly", item.AssemblyName);
|
||||
Assert.Equal("onclick", item.Name);
|
||||
Assert.Equal("Test.EventHandlers", item.DisplayName);
|
||||
Assert.Equal("Test.EventHandlers", item.GetTypeName());
|
||||
|
||||
// The tag matching rule for an event handler is just the attribute name
|
||||
var rule = Assert.Single(item.TagMatchingRules);
|
||||
Assert.Empty(rule.Diagnostics);
|
||||
Assert.False(rule.HasErrors);
|
||||
Assert.Null(rule.ParentTag);
|
||||
Assert.Equal("*", rule.TagName);
|
||||
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
|
||||
|
||||
var requiredAttribute = Assert.Single(rule.Attributes);
|
||||
Assert.Empty(requiredAttribute.Diagnostics);
|
||||
Assert.Equal("onclick", requiredAttribute.DisplayName);
|
||||
Assert.Equal("onclick", requiredAttribute.Name);
|
||||
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
|
||||
Assert.Null(requiredAttribute.Value);
|
||||
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
|
||||
|
||||
var attribute = Assert.Single(item.BoundAttributes);
|
||||
|
||||
// Invariants
|
||||
Assert.Empty(attribute.Diagnostics);
|
||||
Assert.False(attribute.HasErrors);
|
||||
Assert.Equal(BlazorMetadata.EventHandler.TagHelperKind, attribute.Kind);
|
||||
Assert.False(attribute.IsDefaultKind());
|
||||
Assert.False(attribute.HasIndexer);
|
||||
Assert.Null(attribute.IndexerNamePrefix);
|
||||
Assert.Null(attribute.IndexerTypeName);
|
||||
Assert.False(attribute.IsIndexerBooleanProperty);
|
||||
Assert.False(attribute.IsIndexerStringProperty);
|
||||
|
||||
Assert.Equal(
|
||||
"Sets the 'onclick' attribute to the provided string or delegate value. " +
|
||||
"A delegate value should be of type 'System.Action<Microsoft.AspNetCore.Blazor.UIMouseEventArgs>'.",
|
||||
attribute.Documentation);
|
||||
|
||||
Assert.Equal("onclick", attribute.Name);
|
||||
Assert.Equal("onclick", attribute.GetPropertyName());
|
||||
Assert.Equal("string Test.EventHandlers.onclick", attribute.DisplayName);
|
||||
|
||||
// Defined from the property type
|
||||
Assert.Equal("System.String", attribute.TypeName);
|
||||
Assert.True(attribute.IsStringProperty);
|
||||
Assert.False(attribute.IsBooleanProperty);
|
||||
Assert.False(attribute.IsEnum);
|
||||
}
|
||||
|
||||
private static TagHelperDescriptor[] GetEventHandlerTagHelpers(TagHelperDescriptorProviderContext context)
|
||||
{
|
||||
return ExcludeBuiltInComponents(context).Where(t => t.IsEventHandlerTagHelper()).ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
@addTagHelper "*, BasicTestApp"
|
||||
<PropertiesChangedHandlerChild SuppliedValue=@valueToSupply />
|
||||
<button onclick=@{ valueToSupply++; }>Increment</button>
|
||||
<button onclick=@(x => valueToSupply++)>Increment</button>
|
||||
|
||||
@functions {
|
||||
private int valueToSupply = 100;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
@ExampleFragment
|
||||
}
|
||||
|
||||
<button onclick=@{ showFragment = !showFragment; }>Toggle</button>
|
||||
<button onclick=@(_ => showFragment = !showFragment)>Toggle</button>
|
||||
<p>The end</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@
|
|||
<li><NavLink href="/subdir/RouterTest/WithParameters/Name/Steve/LastName/Sanderson">With parameters</NavLink></li>
|
||||
</ul>
|
||||
|
||||
<button onclick=@{ uriHelper.NavigateTo("RouterTest/Other"); }>
|
||||
<button onclick=@(x => uriHelper.NavigateTo("RouterTest/Other"))>
|
||||
Programmatic navigation
|
||||
</button>
|
||||
|
|
|
|||
Loading…
Reference in New Issue