aspnetcore/src/Microsoft.AspNetCore.Blazor.../EventHandlerLoweringPass.cs

198 lines
7.9 KiB
C#

// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic;
using System.Linq;
using Microsoft.AspNetCore.Blazor.Shared;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
namespace Microsoft.AspNetCore.Blazor.Razor
{
internal class EventHandlerLoweringPass : IntermediateNodePassBase, IRazorOptimizationPass
{
public override int Order => 50;
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.
// Each usage will be represented by a tag helper property that is a descendant of either
// a component or element.
var references = documentNode.FindDescendantReferences<TagHelperPropertyIntermediateNode>();
var parents = new HashSet<IntermediateNode>();
for (var i = 0; i < references.Count; i++)
{
parents.Add(references[i].Parent);
}
foreach (var parent in parents)
{
ProcessDuplicates(parent);
}
for (var i = 0; i < references.Count; i++)
{
var reference = references[i];
var node = (TagHelperPropertyIntermediateNode)reference.Node;
if (!reference.Parent.Children.Contains(node))
{
// This node was removed as a duplicate, skip it.
continue;
}
if (node.TagHelper.IsEventHandlerTagHelper())
{
reference.Replace(RewriteUsage(reference.Parent, node));
}
}
}
private 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--)
{
var eventHandler = parent.Children[i] as TagHelperPropertyIntermediateNode;
if (eventHandler != null &&
eventHandler.TagHelper != null &&
eventHandler.TagHelper.IsEventHandlerTagHelper())
{
for (var j = 0; j < parent.Children.Count; j++)
{
var componentAttribute = parent.Children[j] as ComponentAttributeExtensionNode;
if (componentAttribute != null &&
componentAttribute.TagHelper != null &&
componentAttribute.TagHelper.IsComponentTagHelper() &&
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<TagHelperPropertyIntermediateNode>()
.Where(p => p.TagHelper?.IsEventHandlerTagHelper() ?? false)
.GroupBy(p => p.AttributeName)
.Where(g => g.Count() > 1);
foreach (var duplicate in duplicates)
{
parent.Diagnostics.Add(BlazorDiagnosticFactory.CreateEventHandler_Duplicates(
parent.Source,
duplicate.Key,
duplicate.ToArray()));
foreach (var property in duplicate)
{
parent.Children.Remove(property);
}
}
}
private IntermediateNode RewriteUsage(IntermediateNode parent, TagHelperPropertyIntermediateNode node)
{
var original = GetAttributeContent(node);
// 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 = node.TagHelper.GetEventArgsType();
var tokens = new List<IntermediateToken>()
{
new IntermediateToken()
{
Content = $"{BlazorApi.BindMethods.GetEventHandlerValue}<{eventArgsType}>(",
Kind = TokenKind.CSharp
},
original,
new IntermediateToken()
{
Content = $")",
Kind = TokenKind.CSharp
}
};
if (parent is HtmlElementIntermediateNode)
{
var result = new HtmlAttributeIntermediateNode()
{
AttributeName = node.AttributeName,
Source = node.Source,
Prefix = node.AttributeName + "=\"",
Suffix = "\"",
};
for (var i = 0; i < node.Diagnostics.Count; i++)
{
result.Diagnostics.Add(node.Diagnostics[i]);
}
result.Children.Add(new CSharpExpressionAttributeValueIntermediateNode());
for (var i = 0; i < tokens.Count; i++)
{
result.Children[0].Children.Add(tokens[i]);
}
return result;
}
else
{
var result = new ComponentAttributeExtensionNode(node);
result.Children.Clear();
result.Children.Add(new CSharpExpressionIntermediateNode());
for (var i = 0; i < tokens.Count; i++)
{
result.Children[0].Children.Add(tokens[i]);
}
return result;
}
}
private static IntermediateToken GetAttributeContent(TagHelperPropertyIntermediateNode 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());
}
}
}
}