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:
Ryan Nowak 2018-04-04 18:10:49 -07:00
parent 3369208c28
commit c3366bc956
39 changed files with 1405 additions and 168 deletions

View File

@ -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());
}
}
}

View File

@ -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";

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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

View File

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

View File

@ -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());
}
}
}
}

View File

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

View File

@ -105,6 +105,15 @@ namespace Microsoft.AspNetCore.Blazor.Razor {
}
}
/// <summary>
/// Looks up a localized string similar to Sets the &apos;{0}&apos; attribute to the provided string or delegate value. A delegate value should be of type &apos;{1}&apos;..
/// </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>

View File

@ -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: &lt;code&gt;format-value="..."&lt;/code&gt; will apply a format string to the value specified in &lt;code&gt;bind-value-...&lt;/code&gt;. The format string can currently only be used with expressions of type &lt;code&gt;DateTime&lt;/code&gt;.</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>

View File

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

View File

@ -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>

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

View File

@ -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
{
}
}

View File

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

View File

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

View File

@ -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()
{

View File

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

View File

@ -0,0 +1,40 @@
// <auto-generated/>
#pragma warning disable 1591
namespace Test
{
#line hidden
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
#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

View File

@ -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

View File

@ -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) {
}
|

View File

@ -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

View File

@ -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 - />

View File

@ -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 => { }|

View File

@ -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

View File

@ -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 - />

View File

@ -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

View File

@ -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

View File

@ -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) {
}
|

View File

@ -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

View File

@ -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 - />

View File

@ -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

View File

@ -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 - />

View File

@ -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();
}
}
}

View File

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

View File

@ -7,7 +7,7 @@
@ExampleFragment
}
<button onclick=@{ showFragment = !showFragment; }>Toggle</button>
<button onclick=@(_ => showFragment = !showFragment)>Toggle</button>
<p>The end</p>
</div>

View File

@ -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>