| File: Language\Components\ComponentEventHandlerLoweringPass.cs | Web Access |
| Project: src\roslyn\src\Razor\src\Compiler\Microsoft.CodeAnalysis.Razor.Compiler\src\Microsoft.CodeAnalysis.Razor.Compiler.csproj (Microsoft.CodeAnalysis.Razor.Compiler) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using System.Threading; using Microsoft.AspNetCore.Razor.Language.Extensions; using Microsoft.AspNetCore.Razor.Language.Intermediate; using Microsoft.AspNetCore.Razor.PooledObjects; namespace Microsoft.AspNetCore.Razor.Language.Components; internal sealed class ComponentEventHandlerLoweringPass : ComponentIntermediateNodePassBase, IRazorOptimizationPass { public override int Order => 50; protected override void ExecuteCore( RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode, CancellationToken cancellationToken) { if (!IsComponentDocument(documentNode)) { return; } 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. // Each usage will be represented by a tag helper property that is a descendant of either // a component or element. using var _ = SpecializedPools.GetPooledReferenceEqualityHashSet<IntermediateNode>(out var parents); var references = documentNode.FindDescendantReferences<TagHelperDirectiveAttributeIntermediateNode>(); foreach (var reference in references) { parents.Add(reference.Parent); } // We need to do something similar for directive attribute parameters like @onclick:preventDefault. var parameterReferences = documentNode.FindDescendantReferences<TagHelperDirectiveAttributeParameterIntermediateNode>(); foreach (var parameterReference in parameterReferences) { parents.Add(parameterReference.Parent); } foreach (var parent in parents) { ProcessDuplicates(parent); } foreach (var reference in references) { var node = reference.Node; if (!reference.Parent.Children.Contains(node)) { // This node was removed as a duplicate, skip it. continue; } if (node.TagHelper.Kind == TagHelperKind.EventHandler) { reference.Replace(RewriteUsage(reference.Parent, node)); } } foreach (var parameterReference in parameterReferences) { var node = parameterReference.Node; if (!parameterReference.Parent.Children.Contains(node)) { // This node was removed as a duplicate, skip it. continue; } if (node.TagHelper.Kind == TagHelperKind.EventHandler) { parameterReference.Replace(RewriteParameterUsage(node)); } } } private static void ProcessDuplicates(IntermediateNode parent) { // 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 = parent.Children.Count - 1; i >= 0; i--) { if (parent.Children[i] is TagHelperPropertyIntermediateNode eventHandler && eventHandler.TagHelper != null && eventHandler.TagHelper.Kind == TagHelperKind.EventHandler) { for (var j = 0; j < parent.Children.Count; j++) { if (parent.Children[j] is ComponentAttributeIntermediateNode componentAttribute && componentAttribute.TagHelper != null && componentAttribute.TagHelper.Kind == TagHelperKind.Component && componentAttribute.AttributeName == eventHandler.AttributeName) { // Found a duplicate - remove the 'fallback' in favor of the component's own handling. parent.Children.RemoveAt(i); break; } } } } // If we still have duplicates at this point then they are genuine conflicts. var duplicates = parent.Children .OfType<TagHelperDirectiveAttributeIntermediateNode>() .Where(p => p.TagHelper?.Kind == TagHelperKind.EventHandler) .GroupBy(p => p.AttributeName) .Where(g => g.Count() > 1); foreach (var duplicate in duplicates) { parent.AddDiagnostic(ComponentDiagnosticFactory.CreateEventHandler_Duplicates( parent.Source, duplicate.Key, duplicate.ToArray())); foreach (var property in duplicate) { parent.Children.Remove(property); } } var parameterDuplicates = parent.Children .OfType<TagHelperDirectiveAttributeParameterIntermediateNode>() .Where(p => p.TagHelper.Kind == TagHelperKind.EventHandler) .GroupBy(p => p.AttributeName) .Where(g => g.Count() > 1); foreach (var duplicate in parameterDuplicates) { parent.AddDiagnostic(ComponentDiagnosticFactory.CreateEventHandlerParameter_Duplicates( parent.Source, duplicate.Key, duplicate.ToArray())); foreach (var property in duplicate) { parent.Children.Remove(property); } } } private static IntermediateNode RewriteUsage(IntermediateNode parent, TagHelperDirectiveAttributeIntermediateNode node) { var original = GetAttributeContent(node); if (original.Length == 0) { // This can happen in error cases, the parser will already have flagged this // as an error, so ignore it. return node; } // Now rewrite the content of the value node to look like: // // EventCallback.Factory.Create<T>(this, <code>) // // This method is overloaded on string and T, which means that it will put the code in the // correct context for intellisense when typing in the attribute. var eventArgsType = node.TagHelper.GetEventArgsType().AssumeNotNull(); using var tokens = new PooledArrayBuilder<IntermediateToken>(capacity: original.Length + 2); tokens.Add( IntermediateNodeFactory.CSharpToken($"global::{ComponentsApi.EventCallback.FactoryAccessor}.{ComponentsApi.EventCallbackFactory.CreateMethod}<{TypeNameHelper.GetGloballyQualifiedNameIfNeeded(eventArgsType)}>(this, ")); tokens.AddRange(original); tokens.Add(IntermediateNodeFactory.CSharpToken(")")); var attributeName = node.AttributeName; if (parent is MarkupElementIntermediateNode) { var result = new HtmlAttributeIntermediateNode() { OriginalAttributeName = node.OriginalAttributeName, AttributeName = attributeName, Source = node.Source, Prefix = attributeName + "=\"", Suffix = "\"", }; result.AddDiagnosticsFromNode(node); var attributeValueNode = new CSharpExpressionAttributeValueIntermediateNode(); result.Children.Add(attributeValueNode); foreach (var token in tokens) { attributeValueNode.Children.Add(token); } return result; } else { var result = ComponentAttributeIntermediateNode.From(node, addChildren: false); result.OriginalAttributeName = node.OriginalAttributeName; var expressionNode = new CSharpExpressionIntermediateNode(); foreach (var token in tokens) { expressionNode.Children.Add(token); } result.Children.Add(expressionNode); return result; } } private static ImmutableArray<IntermediateToken> GetAttributeContent(IntermediateNode node) { var nodes = node.FindDescendantNodes<TemplateIntermediateNode>(); var template = nodes.Length > 0 ? nodes[0] : null; if (template != null) { // See comments in TemplateDiagnosticPass node.AddDiagnostic(ComponentDiagnosticFactory.Create_TemplateInvalidLocation(template.Source)); return [IntermediateNodeFactory.CSharpToken(string.Empty)]; } if (node.Children.Count == 1 && node.Children[0] is HtmlContentIntermediateNode htmlContentNode) { // This case can be hit for a 'string' attribute. We want to turn it into // an expression. var tokens = htmlContentNode.FindDescendantNodes<IntermediateToken>(); var content = "\"" + string.Join(string.Empty, tokens.Select(t => t.Content.Replace("\"", "\\\""))) + "\""; return [IntermediateNodeFactory.CSharpToken(content)]; } return node.FindDescendantNodes<IntermediateToken>(); } private static IntermediateNode RewriteParameterUsage(TagHelperDirectiveAttributeParameterIntermediateNode node) { // Now rewrite the node to look like: // // builder.AddEventPreventDefaultAttribute(2, "onclick", true); // If minimized. // or // builder.AddEventPreventDefaultAttribute(2, "onclick", someBoolExpression); // If a bool expression is provided in the value. string eventHandlerMethod; if (node.BoundAttributeParameter.Name == "preventDefault") { eventHandlerMethod = ComponentsApi.RenderTreeBuilder.AddEventPreventDefaultAttribute; } else if (node.BoundAttributeParameter.Name == "stopPropagation") { eventHandlerMethod = ComponentsApi.RenderTreeBuilder.AddEventStopPropagationAttribute; } else { // Unsupported event handler attribute parameter. This can only happen if bound attribute descriptor // is configured to expect a parameter other than 'preventDefault' and 'stopPropagation'. return node; } var result = ComponentAttributeIntermediateNode.From(node, addChildren: false); result.OriginalAttributeName = node.OriginalAttributeName; result.AddAttributeMethodName = eventHandlerMethod; if (node.AttributeStructure != AttributeStructure.Minimized) { var tokens = GetAttributeContent(node); var expressionNode = new CSharpExpressionIntermediateNode(); result.Children.Add(expressionNode); expressionNode.Children.AddRange(tokens); } return result; } }