| File: Language\TagHelpers\Producers\EventHandlerTagHelperProducer.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.Immutable; using System.Threading; using Microsoft.AspNetCore.Razor.Language.Components; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Razor; namespace Microsoft.AspNetCore.Razor.Language.TagHelpers.Producers; internal sealed partial class EventHandlerTagHelperProducer : TagHelperProducer { private readonly INamedTypeSymbol _eventHandlerAttributeType; private EventHandlerTagHelperProducer(INamedTypeSymbol eventHandlerAttributeType) { _eventHandlerAttributeType = eventHandlerAttributeType; } public override TagHelperProducerKind Kind => TagHelperProducerKind.EventHandler; public override bool SupportsTypes => true; public override bool IsCandidateType(INamedTypeSymbol type) => type.DeclaredAccessibility == Accessibility.Public && type.Name == "EventHandlers"; public override void AddTagHelpersForType( INamedTypeSymbol type, ref TagHelperCollection.RefBuilder results, CancellationToken cancellationToken) { // 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. foreach (var attribute in type.GetAttributes()) { if (SymbolEqualityComparer.Default.Equals(attribute.AttributeClass, _eventHandlerAttributeType)) { if (!AttributeArgs.TryGet(attribute, out var args)) { // If this occurs, the [EventHandler] was defined incorrectly, so we can't create a tag helper. continue; } var typeName = type.GetDefaultDisplayString(); var namespaceName = type.ContainingNamespace.GetFullName(); results.Add(CreateTagHelper(typeName, namespaceName, type.Name, args)); } } } private readonly record struct AttributeArgs( string Attribute, INamedTypeSymbol EventArgsType, bool EnableStopPropagation = false, bool EnablePreventDefault = false) { public static bool TryGet(AttributeData attribute, out AttributeArgs args) { // EventHandlerAttribute has two constructors: // // - EventHandlerAttribute(string attributeName, Type eventArgsType); // - EventHandlerAttribute(string attributeName, Type eventArgsType, bool enableStopPropagation, bool enablePreventDefault); var arguments = attribute.ConstructorArguments; return TryGetFromTwoArguments(arguments, out args) || TryGetFromFourArguments(arguments, out args); static bool TryGetFromTwoArguments(ImmutableArray<TypedConstant> arguments, out AttributeArgs args) { // Ctor 1: EventHandlerAttribute(string attributeName, Type eventArgsType); if (arguments is [ { Value: string attributeName }, { Value: INamedTypeSymbol eventArgsType }]) { args = new(attributeName, eventArgsType); return true; } args = default; return false; } static bool TryGetFromFourArguments(ImmutableArray<TypedConstant> arguments, out AttributeArgs args) { // Ctor 2: EventHandlerAttribute(string attributeName, Type eventArgsType, bool enableStopPropagation, bool enablePreventDefault); // TODO: The enablePreventDefault and enableStopPropagation arguments are incorrectly swapped! // However, they have been that way since the 4-argument constructor variant was introduced // in https://github.com/dotnet/razor/commit/7635bba6ef2d3e6798d0846ceb96da6d5908e1b0. // Fixing this is tracked be https://github.com/dotnet/razor/issues/10497 if (arguments is [ { Value: string attributeName }, { Value: INamedTypeSymbol eventArgsType }, { Value: bool enablePreventDefault }, { Value: bool enableStopPropagation }]) { args = new(attributeName, eventArgsType, enableStopPropagation, enablePreventDefault); return true; } args = default; return false; } } } private static TagHelperDescriptor CreateTagHelper( string typeName, string typeNamespace, string typeNameIdentifier, AttributeArgs args) { var (attribute, eventArgsType, enableStopPropagation, enablePreventDefault) = args; var attributeName = "@" + attribute; var eventArgType = eventArgsType.GetDefaultDisplayString(); using var _ = TagHelperDescriptorBuilder.GetPooledInstance( TagHelperKind.EventHandler, attribute, ComponentsApi.AssemblyName, out var builder); builder.SetTypeName(typeName, typeNamespace, typeNameIdentifier); builder.CaseSensitive = true; builder.ClassifyAttributesOnly = true; builder.SetDocumentation( DocumentationDescriptor.From( DocumentationId.EventHandlerTagHelper, attributeName, eventArgType)); builder.SetMetadata(new EventHandlerMetadata() { EventArgsType = eventArgType }); builder.TagMatchingRule(rule => { rule.TagName = "*"; rule.Attribute(a => { a.Name = attributeName; a.NameComparison = RequiredAttributeNameComparison.FullMatch; a.IsDirectiveAttribute = true; }); }); if (enablePreventDefault) { builder.TagMatchingRule(rule => { rule.TagName = "*"; rule.Attribute(a => { a.Name = attributeName + ":preventDefault"; a.NameComparison = RequiredAttributeNameComparison.FullMatch; a.IsDirectiveAttribute = true; }); }); } if (enableStopPropagation) { builder.TagMatchingRule(rule => { rule.TagName = "*"; rule.Attribute(a => { a.Name = attributeName + ":stopPropagation"; a.NameComparison = RequiredAttributeNameComparison.FullMatch; a.IsDirectiveAttribute = true; }); }); } builder.BindAttribute(a => { a.SetDocumentation( DocumentationDescriptor.From( DocumentationId.EventHandlerTagHelper, attributeName, eventArgType)); a.Name = attributeName; // We want event handler directive attributes to default to C# context. a.TypeName = $"Microsoft.AspNetCore.Components.EventCallback<{eventArgType}>"; a.PropertyName = attribute; a.IsDirectiveAttribute = true; // Make this weakly typed (don't type check) - delegates have their own type-checking // logic that we don't want to interfere with. a.IsWeaklyTyped = true; if (enablePreventDefault) { a.BindAttributeParameter(parameter => { parameter.Name = "preventDefault"; parameter.PropertyName = "PreventDefault"; parameter.TypeName = typeof(bool).FullName; parameter.SetDocumentation( DocumentationDescriptor.From( DocumentationId.EventHandlerTagHelper_PreventDefault, attributeName)); }); } if (enableStopPropagation) { a.BindAttributeParameter(parameter => { parameter.Name = "stopPropagation"; parameter.PropertyName = "StopPropagation"; parameter.TypeName = typeof(bool).FullName; parameter.SetDocumentation( DocumentationDescriptor.From( DocumentationId.EventHandlerTagHelper_StopPropagation, attributeName)); }); } }); return builder.Build(); } }