Add compiler support for EventCallback (dotnet/aspnetcore-tooling#222)

* Add new APIs to ComponentShim

* Add new APIs to ComponentsApi

* Add EventCallback metadata

* Add discovery of EventCallback properties

* Errata for Runtime APIs

* Add ability to use EventCallback as parameter

* Add support for bind to component

* Use EventCallback for bind-... to elements

* Use EventCallback<T> for event handlers
\n\nCommit migrated from a0b6bc0e52
This commit is contained in:
Ryan Nowak 2019-02-19 12:39:33 -08:00 committed by GitHub
parent 1243e07e56
commit 54e79153ec
20 changed files with 963 additions and 76 deletions

View File

@ -55,6 +55,8 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
public static readonly string DelegateSignatureKey = "Blazor.DelegateSignature";
public static readonly string EventCallbackKey = "Blazor.EventCallback";
public static readonly string WeaklyTypedKey = "Blazor.IsWeaklyTyped";
public static readonly string RuntimeName = "Blazor.IComponent";

View File

@ -158,9 +158,12 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
// there will be errors. In general the errors that come from C# in this case are good enough
// to understand the problem.
//
// The BindMethods calls are required in this case because to give us a good experience. They
// We also support and encourage the use of EventCallback<> with bind. So in the above example
// the ValueChanged property could be an Action<> or an EventCallback<>.
//
// The BindMethods calls are required with Action<> because to give us a good experience. They
// use overloading to ensure that can get an Action<object> that will convert and set an arbitrary
// value.
// value. We have a similar set of APIs to use with EventCallback<>.
//
// We also assume that the element will be treated as a component for now because
// multiple passes handle 'special' tag helpers. We have another pass that translates
@ -207,67 +210,28 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
format = GetAttributeContent(formatNode);
}
// Now rewrite the content of the value node to look like:
//
// BindMethods.GetValue(<code>) OR
// BindMethods.GetValue(<code>, <format>)
var valueExpressionTokens = new List<IntermediateToken>();
valueExpressionTokens.Add(new IntermediateToken()
var changeExpressionTokens = new List<IntermediateToken>();
if (changeAttribute != null && changeAttribute.IsDelegateProperty())
{
Content = $"{ComponentsApi.BindMethods.GetValue}(",
Kind = TokenKind.CSharp
});
valueExpressionTokens.Add(original);
if (!string.IsNullOrEmpty(format?.Content))
{
valueExpressionTokens.Add(new IntermediateToken()
{
Content = ", ",
Kind = TokenKind.CSharp,
});
valueExpressionTokens.Add(format);
}
valueExpressionTokens.Add(new IntermediateToken()
{
Content = ")",
Kind = TokenKind.CSharp,
});
// Now rewrite the content of the change-handler node. There are two cases we care about
// here. If it's a component attribute, then don't use the 'BindMethods wrapper. We expect
// component attributes to always 'match' on type.
//
// __value => <code> = __value
//
// For general DOM attributes, we need to be able to create a delegate that accepts UIEventArgs
// so we use BindMethods.SetValueHandler
//
// BindMethods.SetValueHandler(__value => <code> = __value, <code>) OR
// BindMethods.SetValueHandler(__value => <code> = __value, <code>, <format>)
//
// Note that the linemappings here are applied to the value attribute, not the change attribute.
string changeExpressionContent = null;
if (changeAttribute == null && format == null)
{
changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})";
}
else if (changeAttribute == null && format != null)
{
changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})";
RewriteNodesForDelegateBind(
original,
format,
valueAttribute,
changeAttribute,
valueExpressionTokens,
changeExpressionTokens);
}
else
{
changeExpressionContent = $"__value => {original.Content} = __value";
RewriteNodesForEventCallbackBind(
original,
format,
valueAttribute,
changeAttribute,
valueExpressionTokens,
changeExpressionTokens);
}
var changeExpressionTokens = new List<IntermediateToken>()
{
new IntermediateToken()
{
Content = changeExpressionContent,
Kind = TokenKind.CSharp
}
};
if (parent is MarkupElementIntermediateNode)
{
@ -521,6 +485,151 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
return false;
}
private void RewriteNodesForDelegateBind(
IntermediateToken original,
IntermediateToken format,
BoundAttributeDescriptor valueAttribute,
BoundAttributeDescriptor changeAttribute,
List<IntermediateToken> valueExpressionTokens,
List<IntermediateToken> changeExpressionTokens)
{
// Now rewrite the content of the value node to look like:
//
// BindMethods.GetValue(<code>) OR
// BindMethods.GetValue(<code>, <format>)
valueExpressionTokens.Add(new IntermediateToken()
{
Content = $"{ComponentsApi.BindMethods.GetValue}(",
Kind = TokenKind.CSharp
});
valueExpressionTokens.Add(original);
if (!string.IsNullOrEmpty(format?.Content))
{
valueExpressionTokens.Add(new IntermediateToken()
{
Content = ", ",
Kind = TokenKind.CSharp,
});
valueExpressionTokens.Add(format);
}
valueExpressionTokens.Add(new IntermediateToken()
{
Content = ")",
Kind = TokenKind.CSharp,
});
// Now rewrite the content of the change-handler node. There are two cases we care about
// here. If it's a component attribute, then don't use the 'BindMethods' wrapper. We expect
// component attributes to always 'match' on type.
//
// __value => <code> = __value
//
// For general DOM attributes, we need to be able to create a delegate that accepts UIEventArgs
// so we use BindMethods.SetValueHandler
//
// BindMethods.SetValueHandler(__value => <code> = __value, <code>) OR
// BindMethods.SetValueHandler(__value => <code> = __value, <code>, <format>)
//
// Note that the linemappings here are applied to the value attribute, not the change attribute.
string changeExpressionContent;
if (changeAttribute == null && format == null)
{
// DOM
changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content})";
}
else if (changeAttribute == null && format != null)
{
// DOM + format
changeExpressionContent = $"{ComponentsApi.BindMethods.SetValueHandler}(__value => {original.Content} = __value, {original.Content}, {format.Content})";
}
else
{
// Component
changeExpressionContent = $"__value => {original.Content} = __value";
}
changeExpressionTokens.Add(new IntermediateToken()
{
Content = changeExpressionContent,
Kind = TokenKind.CSharp
});
}
private void RewriteNodesForEventCallbackBind(
IntermediateToken original,
IntermediateToken format,
BoundAttributeDescriptor valueAttribute,
BoundAttributeDescriptor changeAttribute,
List<IntermediateToken> valueExpressionTokens,
List<IntermediateToken> changeExpressionTokens)
{
// Now rewrite the content of the value node to look like:
//
// BindMethods.GetValue(<code>) OR
// BindMethods.GetValue(<code>, <format>)
valueExpressionTokens.Add(new IntermediateToken()
{
Content = $"{ComponentsApi.BindMethods.GetValue}(",
Kind = TokenKind.CSharp
});
valueExpressionTokens.Add(original);
if (!string.IsNullOrEmpty(format?.Content))
{
valueExpressionTokens.Add(new IntermediateToken()
{
Content = ", ",
Kind = TokenKind.CSharp,
});
valueExpressionTokens.Add(format);
}
valueExpressionTokens.Add(new IntermediateToken()
{
Content = ")",
Kind = TokenKind.CSharp,
});
// Now rewrite the content of the change-handler node. There are two cases we care about
// here. If it's a component attribute, then don't use the 'CreateBinder' wrapper. We expect
// component attributes to always 'match' on type.
//
// The really tricky part of this is that we CANNOT write the type name of of the EventCallback we
// intend to create. Doing so would really complicate the story for how we deal with generic types,
// since the generic type lowering pass runs after this. To keep this simple we're relying on
// the compiler to resolve overloads for us.
//
// EventCallbackFactory.CreateInferred(this, __value => <code> = __value, <code>)
//
// For general DOM attributes, we need to be able to create a delegate that accepts UIEventArgs
// so we use 'CreateBinder'
//
// EventCallbackFactory.CreateBinder(this, __value => <code> = __value, <code>) OR
// EventCallbackFactory.CreateBinder(this, __value => <code> = __value, <code>, <format>)
//
// Note that the linemappings here are applied to the value attribute, not the change attribute.
string changeExpressionContent;
if (changeAttribute == null && format == null)
{
// DOM
changeExpressionContent = $"{ComponentsApi.EventCallback.FactoryAccessor}.{ComponentsApi.EventCallbackFactory.CreateBinderMethod}(this, __value => {original.Content} = __value, {original.Content})";
}
else if (changeAttribute == null && format != null)
{
// DOM + format
changeExpressionContent = $"{ComponentsApi.EventCallback.FactoryAccessor}.{ComponentsApi.EventCallbackFactory.CreateBinderMethod}(this, __value => {original.Content} = __value, {original.Content}, {format.Content})";
}
else
{
// Component
changeExpressionContent = $"{ComponentsApi.EventCallback.FactoryAccessor}.{ComponentsApi.EventCallbackFactory.CreateInferredMethod}(this, __value => {original.Content} = __value, {original.Content})";
}
changeExpressionTokens.Add(new IntermediateToken()
{
Content = changeExpressionContent,
Kind = TokenKind.CSharp
});
}
private static IntermediateToken GetAttributeContent(TagHelperPropertyIntermediateNode node)
{
var template = node.FindDescendantNodes<TemplateIntermediateNode>().FirstOrDefault();

View File

@ -541,6 +541,55 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
context.CodeWriter.Write(")");
}
}
else if (node.BoundAttribute?.IsEventCallbackProperty() ?? false)
{
// This is the case where we are writing an EventCallback (a delegate with super-powers).
//
// An event callback can either be passed verbatim, or it can be created by the EventCallbackFactory.
// Since we don't look at the code the user typed inside the attribute value, this is always
// resolved via overloading.
if (canTypeCheck && NeedsTypeCheck(node))
{
context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
context.CodeWriter.Write("<");
context.CodeWriter.Write(node.TypeName);
context.CodeWriter.Write(">");
context.CodeWriter.Write("(");
}
// Microsoft.AspNetCore.Components.EventCallback.Factory.Create(this, ...) OR
// Microsoft.AspNetCore.Components.EventCallback.Factory.Create<T>(this, ...)
context.CodeWriter.Write(ComponentsApi.EventCallback.FactoryAccessor);
context.CodeWriter.Write(".");
context.CodeWriter.Write(ComponentsApi.EventCallbackFactory.CreateMethod);
if (node.TryParseEventCallbackTypeArgument(out var argument))
{
context.CodeWriter.Write("<");
context.CodeWriter.Write(argument);
context.CodeWriter.Write(">");
}
context.CodeWriter.Write("(");
context.CodeWriter.Write("this");
context.CodeWriter.Write(", ");
context.CodeWriter.WriteLine();
for (var i = 0; i < tokens.Count; i++)
{
WriteCSharpToken(context, tokens[i]);
}
context.CodeWriter.Write(")");
if (canTypeCheck && NeedsTypeCheck(node))
{
context.CodeWriter.Write(")");
}
}
else
{
// This is the case when an attribute contains C# code

View File

@ -123,16 +123,16 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
// Now rewrite the content of the value node to look like:
//
// BindMethods.GetEventHandlerValue<TDelegate>(<code>)
// EventCallback.Factory.Create<T>(this, <code>)
//
// This method is overloaded on string and TDelegate, which means that it will put the code in the
// 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();
var tokens = new List<IntermediateToken>()
{
new IntermediateToken()
{
Content = $"{ComponentsApi.BindMethods.GetEventHandlerValue}<{eventArgsType}>(",
Content = $"{ComponentsApi.EventCallback.FactoryAccessor}.{ComponentsApi.EventCallbackFactory.CreateMethod}<{eventArgsType}>(this, ",
Kind = TokenKind.CSharp
},
new IntermediateToken()

View File

@ -218,6 +218,11 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
// This is a weakly typed delegate, treat it as Action<object>
attribute.TypeName = "System.Action<System.Object>";
}
else if (attribute.TypeName == null && (attribute.BoundAttribute?.IsEventCallbackProperty() ?? false))
{
// This is a weakly typed event-callback, treat it as EventCallback (non-generic)
attribute.TypeName = ComponentsApi.EventCallback.FullTypeName;
}
else if (attribute.TypeName == null)
{
// This is a weakly typed attribute, treat it as System.Object

View File

@ -460,7 +460,7 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
}
else
{
// See comments in BlazorDesignTimeNodeWriter for a description of the cases that are possible.
// See comments in ComponentDesignTimeNodeWriter for a description of the cases that are possible.
var tokens = GetCSharpTokens(node);
if ((node.BoundAttribute?.IsDelegateProperty() ?? false) ||
(node.BoundAttribute?.IsChildContentProperty() ?? false))
@ -482,6 +482,47 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
context.CodeWriter.Write(")");
}
}
else if (node.BoundAttribute?.IsEventCallbackProperty() ?? false)
{
if (canTypeCheck && NeedsTypeCheck(node))
{
context.CodeWriter.Write(ComponentsApi.RuntimeHelpers.TypeCheck);
context.CodeWriter.Write("<");
context.CodeWriter.Write(node.TypeName);
context.CodeWriter.Write(">");
context.CodeWriter.Write("(");
}
// Microsoft.AspNetCore.Components.EventCallback.Factory.Create(this, ...) OR
// Microsoft.AspNetCore.Components.EventCallback.Factory.Create<T>(this, ...)
context.CodeWriter.Write(ComponentsApi.EventCallback.FactoryAccessor);
context.CodeWriter.Write(".");
context.CodeWriter.Write(ComponentsApi.EventCallbackFactory.CreateMethod);
if (node.TryParseEventCallbackTypeArgument(out var argument))
{
context.CodeWriter.Write("<");
context.CodeWriter.Write(argument);
context.CodeWriter.Write(">");
}
context.CodeWriter.Write("(");
context.CodeWriter.Write("this");
context.CodeWriter.Write(", ");
for (var i = 0; i < tokens.Count; i++)
{
context.CodeWriter.Write(tokens[i].Content);
}
context.CodeWriter.Write(")");
if (canTypeCheck && NeedsTypeCheck(node))
{
context.CodeWriter.Write(")");
}
}
else
{
if (canTypeCheck && NeedsTypeCheck(node))

View File

@ -129,5 +129,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.ElementRef";
}
public static class EventCallback
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Components.EventCallback";
public static readonly string MetadataName = FullTypeName;
public static readonly string FactoryAccessor = FullTypeName + ".Factory";
}
public static class EventCallbackOfT
{
public static readonly string MetadataName = "Microsoft.AspNetCore.Components.EventCallback`1";
}
public static class EventCallbackFactory
{
public static readonly string CreateMethod = "Create";
public static readonly string CreateInferredMethod = "CreateInferred";
public static readonly string CreateBinderMethod = "CreateBinder";
}
}
}

View File

@ -20,6 +20,25 @@ namespace Microsoft.AspNetCore.Razor.Language.Components
string.Equals(value, bool.TrueString);
}
/// <summary>
/// Gets a value indicating whether the attribute is of type <c>EventCallback</c> or
/// <c>EventCallback{T}</c>
/// </summary>
/// <param name="attribute">The <see cref="BoundAttributeDescriptor"/>.</param>
/// <returns><c>true</c> if the attribute is an event callback, otherwise <c>false</c>.</returns>
public static bool IsEventCallbackProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)
{
throw new ArgumentNullException(nameof(attribute));
}
var key = BlazorMetadata.Component.EventCallbackKey;
return
attribute.Metadata.TryGetValue(key, out var value) &&
string.Equals(value, bool.TrueString);
}
public static bool IsGenericTypedProperty(this BoundAttributeDescriptor attribute)
{
if (attribute == null)

View File

@ -101,5 +101,42 @@ namespace Microsoft.AspNetCore.Razor.Language.Intermediate
formatter.WriteProperty(nameof(TagHelper), TagHelper?.DisplayName);
formatter.WriteProperty(nameof(TypeName), TypeName);
}
public bool TryParseEventCallbackTypeArgument(out string argument)
{
// This is ugly and ad-hoc, but for various layering reasons we can't just use Roslyn APIs
// to parse this. We need to parse this just before we write it out to the code generator,
// so we can't compute it up front either.
if (BoundAttribute == null || !BoundAttribute.IsEventCallbackProperty())
{
throw new InvalidOperationException("This attribute is not an EventCallback attribute.");
}
if (string.Equals(TypeName, ComponentsApi.EventCallback.FullTypeName, StringComparison.Ordinal))
{
// Non-Generic
argument = null;
return false;
}
if (TypeName != null &&
TypeName.Length > ComponentsApi.EventCallback.FullTypeName.Length + "<>".Length &&
TypeName.StartsWith(ComponentsApi.EventCallback.FullTypeName, StringComparison.Ordinal) &&
TypeName[ComponentsApi.EventCallback.FullTypeName.Length] == '<' &&
TypeName[TypeName.Length - 1] == '>')
{
// OK this is promising.
//
// Chop off leading `...EventCallback<` and let the length so the ending `>` is cut off as well.
argument = TypeName.Substring(ComponentsApi.EventCallback.FullTypeName.Length + 1, TypeName.Length - (ComponentsApi.EventCallback.FullTypeName.Length + "<>".Length));
return true;
}
// If we get here this is a failure. This should only happen if someone manages to mangle the name with extensibility.
// We don't really want to crash though.
argument = null;
return false;
}
}
}

View File

@ -33,7 +33,7 @@ namespace Microsoft.CodeAnalysis.Razor
// We generate:
// <input type="text"
// value="@BindMethods.GetValue(FirstName)"
// onchange="@BindMethods.SetValue(__value => FirstName = __value, FirstName)"/>
// onchange="@EventCallbackFactory.CreateBinder(this, __value => FirstName = __value, FirstName)"/>
//
// This isn't very different from code the user could write themselves - thus the pronouncement
// that bind is very much like a macro.
@ -356,7 +356,13 @@ namespace Microsoft.CodeAnalysis.Razor
for (var i = 0; i < tagHelper.BoundAttributes.Count; i++)
{
var changeAttribute = tagHelper.BoundAttributes[i];
if (!changeAttribute.Name.EndsWith("Changed") || !changeAttribute.IsDelegateProperty())
if (!changeAttribute.Name.EndsWith("Changed") ||
// Allow the ValueChanged attribute to be a delegate or EventCallback<>.
//
// We assume that the Delegate or EventCallback<> has a matching type, and the C# compiler will help
// you figure figure it out if you did it wrongly.
(!changeAttribute.IsDelegateProperty() && !changeAttribute.IsEventCallbackProperty()))
{
continue;
}
@ -367,7 +373,7 @@ namespace Microsoft.CodeAnalysis.Razor
var expressionAttributeName = valueAttributeName + "Expression";
for (var j = 0; j < tagHelper.BoundAttributes.Count; j++)
{
if (tagHelper.BoundAttributes[j].Name == valueAttributeName && !tagHelper.BoundAttributes[j].IsDelegateProperty())
if (tagHelper.BoundAttributes[j].Name == valueAttributeName)
{
valueAttribute = tagHelper.BoundAttributes[j];

View File

@ -43,7 +43,7 @@ namespace Microsoft.CodeAnalysis.Razor
// We need to see private members too
compilation = WithMetadataImportOptionsAll(compilation);
var symbols = BlazorSymbols.Create(compilation);
var symbols = ComponentSymbols.Create(compilation);
var types = new List<INamedTypeSymbol>();
var visitor = new ComponentTypeVisitor(symbols, types);
@ -81,7 +81,7 @@ namespace Microsoft.CodeAnalysis.Razor
return compilation.WithOptions(newCompilationOptions);
}
private TagHelperDescriptor CreateDescriptor(BlazorSymbols symbols, INamedTypeSymbol type)
private TagHelperDescriptor CreateDescriptor(ComponentSymbols symbols, INamedTypeSymbol type)
{
var typeName = type.ToDisplayString(FullNameTypeDisplayFormat);
var assemblyName = type.ContainingAssembly.Identity.Name;
@ -157,6 +157,11 @@ namespace Microsoft.CodeAnalysis.Razor
pb.Metadata.Add(BlazorMetadata.Component.ChildContentKey, bool.TrueString);
}
if (kind == PropertyKind.EventCallback)
{
pb.Metadata.Add(BlazorMetadata.Component.EventCallbackKey, bool.TrueString);
}
if (kind == PropertyKind.Delegate)
{
pb.Metadata.Add(BlazorMetadata.Component.DelegateSignatureKey, bool.TrueString);
@ -230,7 +235,7 @@ namespace Microsoft.CodeAnalysis.Razor
});
}
private TagHelperDescriptor CreateChildContentDescriptor(BlazorSymbols symbols, TagHelperDescriptor component, BoundAttributeDescriptor attribute)
private TagHelperDescriptor CreateChildContentDescriptor(ComponentSymbols symbols, TagHelperDescriptor component, BoundAttributeDescriptor attribute)
{
var typeName = component.GetTypeName() + "." + attribute.Name;
var assemblyName = component.AssemblyName;
@ -297,7 +302,7 @@ namespace Microsoft.CodeAnalysis.Razor
// - have the [Parameter] attribute
// - have a setter, even if private
// - are not indexers
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(BlazorSymbols symbols, INamedTypeSymbol type)
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(ComponentSymbols symbols, INamedTypeSymbol type)
{
var properties = new Dictionary<string, (IPropertySymbol, PropertyKind)>(StringComparer.Ordinal);
do
@ -368,6 +373,19 @@ namespace Microsoft.CodeAnalysis.Razor
kind = PropertyKind.ChildContent;
}
if (kind == PropertyKind.Default && property.Type == symbols.EventCallback)
{
kind = PropertyKind.EventCallback;
}
if (kind == PropertyKind.Default &&
property.Type is INamedTypeSymbol namedType2 &&
namedType2.IsGenericType &&
namedType2.ConstructedFrom == symbols.EventCallbackOfT)
{
kind = PropertyKind.EventCallback;
}
if (kind == PropertyKind.Default && property.Type.TypeKind == TypeKind.Delegate)
{
kind = PropertyKind.Delegate;
@ -390,13 +408,18 @@ namespace Microsoft.CodeAnalysis.Razor
Enum,
ChildContent,
Delegate,
EventCallback,
}
private class BlazorSymbols
private class ComponentSymbols
{
public static BlazorSymbols Create(Compilation compilation)
public static ComponentSymbols Create(Compilation compilation)
{
var symbols = new BlazorSymbols();
// We find a bunch of important and fundamental types here that are needed to discover
// components. If one of these isn't defined then we just bail, because the results will
// be unpredictable.
var symbols = new ComponentSymbols();
symbols.ComponentBase = compilation.GetTypeByMetadataName(ComponentsApi.ComponentBase.MetadataName);
if (symbols.ComponentBase == null)
{
@ -422,18 +445,34 @@ namespace Microsoft.CodeAnalysis.Razor
if (symbols.RenderFragment == null)
{
// No definition for RenderFragment, nothing to do.
return null;
}
symbols.RenderFragmentOfT = compilation.GetTypeByMetadataName(ComponentsApi.RenderFragmentOfT.MetadataName);
if (symbols.RenderFragmentOfT == null)
{
// No definition for RenderFragment, nothing to do.
// No definition for RenderFragment<T>, nothing to do.
return null;
}
symbols.EventCallback = compilation.GetTypeByMetadataName(ComponentsApi.EventCallback.MetadataName);
if (symbols.EventCallback == null)
{
// No definition for EventCallback, nothing to do.
return null;
}
symbols.EventCallbackOfT = compilation.GetTypeByMetadataName(ComponentsApi.EventCallbackOfT.MetadataName);
if (symbols.EventCallbackOfT == null)
{
// No definition for EventCallback<T>, nothing to do.
return null;
}
return symbols;
}
private BlazorSymbols()
private ComponentSymbols()
{
}
@ -446,14 +485,18 @@ namespace Microsoft.CodeAnalysis.Razor
public INamedTypeSymbol RenderFragment { get; private set; }
public INamedTypeSymbol RenderFragmentOfT { get; private set; }
public INamedTypeSymbol EventCallback { get; private set; }
public INamedTypeSymbol EventCallbackOfT { get; private set; }
}
private class ComponentTypeVisitor : SymbolVisitor
{
private readonly BlazorSymbols _symbols;
private readonly ComponentSymbols _symbols;
private readonly List<INamedTypeSymbol> _results;
public ComponentTypeVisitor(BlazorSymbols symbols, List<INamedTypeSymbol> results)
public ComponentTypeVisitor(ComponentSymbols symbols, List<INamedTypeSymbol> results)
{
_symbols = symbols;
_results = results;

View File

@ -11,7 +11,7 @@ namespace Microsoft.CodeAnalysis.Razor
public class BindTagHelperDescriptorProviderTest : BaseTagHelperDescriptorProviderTest
{
[Fact]
public void Execute_FindsBindTagHelperOnComponentType_CreatesDescriptor()
public void Execute_FindsBindTagHelperOnComponentType_Delegate_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
@ -130,6 +130,120 @@ namespace Test
Assert.False(attribute.IsEnum);
}
[Fact]
public void Execute_FindsBindTagHelperOnComponentType_EventCallback_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
public class MyComponent : IComponent
{
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters) { }
[Parameter]
string MyProperty { get; set; }
[Parameter]
EventCallback<string> MyPropertyChanged { get; set; }
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
// We run after component discovery and depend on the results.
var componentProvider = new ComponentTagHelperDescriptorProvider();
componentProvider.Execute(context);
var provider = new BindTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var matches = GetBindTagHelpers(context);
var bind = Assert.Single(matches);
// These are features Bind Tags Helpers don't use. Verifying them once here and
// then ignoring them.
Assert.Empty(bind.AllowedChildTags);
Assert.Null(bind.TagOutputHint);
// These are features that are invariants of all Bind Tag Helpers. Verifying them once
// here and then ignoring them.
Assert.Empty(bind.Diagnostics);
Assert.False(bind.HasErrors);
Assert.Equal(BlazorMetadata.Bind.TagHelperKind, bind.Kind);
Assert.Equal(BlazorMetadata.Bind.RuntimeName, bind.Metadata[TagHelperMetadata.Runtime.Name]);
Assert.False(bind.IsDefaultKind());
Assert.False(bind.KindUsesDefaultTagHelperRuntime());
Assert.Equal("MyProperty", bind.Metadata[BlazorMetadata.Bind.ValueAttribute]);
Assert.Equal("MyPropertyChanged", bind.Metadata[BlazorMetadata.Bind.ChangeAttribute]);
Assert.Equal(
"Binds the provided expression to the 'MyProperty' property and a change event " +
"delegate to the 'MyPropertyChanged' property of the component.",
bind.Documentation);
// These are all trivially derived from the assembly/namespace/type name
Assert.Equal("TestAssembly", bind.AssemblyName);
Assert.Equal("Test.MyComponent", bind.Name);
Assert.Equal("Test.MyComponent", bind.DisplayName);
Assert.Equal("Test.MyComponent", bind.GetTypeName());
var rule = Assert.Single(bind.TagMatchingRules);
Assert.Empty(rule.Diagnostics);
Assert.False(rule.HasErrors);
Assert.Null(rule.ParentTag);
Assert.Equal("MyComponent", rule.TagName);
Assert.Equal(TagStructure.Unspecified, rule.TagStructure);
var requiredAttribute = Assert.Single(rule.Attributes);
Assert.Empty(requiredAttribute.Diagnostics);
Assert.Equal("bind-MyProperty", requiredAttribute.DisplayName);
Assert.Equal("bind-MyProperty", requiredAttribute.Name);
Assert.Equal(RequiredAttributeDescriptor.NameComparisonMode.FullMatch, requiredAttribute.NameComparison);
Assert.Null(requiredAttribute.Value);
Assert.Equal(RequiredAttributeDescriptor.ValueComparisonMode.None, requiredAttribute.ValueComparison);
var attribute = Assert.Single(bind.BoundAttributes);
// Invariants
Assert.Empty(attribute.Diagnostics);
Assert.False(attribute.HasErrors);
Assert.Equal(BlazorMetadata.Bind.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(
"Binds the provided expression to the 'MyProperty' property and a change event " +
"delegate to the 'MyPropertyChanged' property of the component.",
attribute.Documentation);
Assert.Equal("bind-MyProperty", attribute.Name);
Assert.Equal("MyProperty", attribute.GetPropertyName());
Assert.Equal("string Test.MyComponent.MyProperty", 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);
}
[Fact]
public void Execute_NoMatchedPropertiesOnComponent_IgnoresComponent()
{

View File

@ -577,6 +577,171 @@ namespace Test
});
}
[Fact]
public void Execute_EventCallbackProperty_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
public class MyComponent : ComponentBase
{
[Parameter]
EventCallback OnClick { get; set; }
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new ComponentTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var components = ExcludeBuiltInComponents(context);
var component = Assert.Single(components);
Assert.Equal("TestAssembly", component.AssemblyName);
Assert.Equal("Test.MyComponent", component.Name);
var attribute = Assert.Single(component.BoundAttributes);
Assert.Equal("OnClick", attribute.Name);
Assert.Equal("Microsoft.AspNetCore.Components.EventCallback", attribute.TypeName);
Assert.False(attribute.HasIndexer);
Assert.False(attribute.IsBooleanProperty);
Assert.False(attribute.IsEnum);
Assert.False(attribute.IsStringProperty);
Assert.True(attribute.IsEventCallbackProperty());
Assert.False(attribute.IsDelegateProperty());
Assert.False(attribute.IsChildContentProperty());
}
[Fact]
public void Execute_EventCallbackProperty_CreatesDescriptor_ClosedGeneric()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
public class MyComponent : ComponentBase
{
[Parameter]
EventCallback<UIMouseEventArgs> OnClick { get; set; }
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new ComponentTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var components = ExcludeBuiltInComponents(context);
var component = Assert.Single(components);
Assert.Equal("TestAssembly", component.AssemblyName);
Assert.Equal("Test.MyComponent", component.Name);
Assert.Collection(
component.BoundAttributes.OrderBy(a => a.Name),
a =>
{
Assert.Equal("OnClick", a.Name);
Assert.Equal("Microsoft.AspNetCore.Components.EventCallback<Microsoft.AspNetCore.Components.UIMouseEventArgs>", a.TypeName);
Assert.False(a.HasIndexer);
Assert.False(a.IsBooleanProperty);
Assert.False(a.IsEnum);
Assert.False(a.IsStringProperty);
Assert.True(a.IsEventCallbackProperty());
Assert.False(a.IsDelegateProperty());
Assert.False(a.IsChildContentProperty());
Assert.False(a.IsGenericTypedProperty());
});
}
[Fact]
public void Execute_EventCallbackProperty_CreatesDescriptor_OpenGeneric()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Components;
namespace Test
{
public class MyComponent<T> : ComponentBase
{
[Parameter]
EventCallback<T> OnClick { get; set; }
}
}
"));
Assert.Empty(compilation.GetDiagnostics());
var context = TagHelperDescriptorProviderContext.Create();
context.SetCompilation(compilation);
var provider = new ComponentTagHelperDescriptorProvider();
// Act
provider.Execute(context);
// Assert
var components = ExcludeBuiltInComponents(context);
var component = Assert.Single(components);
Assert.Equal("TestAssembly", component.AssemblyName);
Assert.Equal("Test.MyComponent<T>", component.Name);
Assert.Collection(
component.BoundAttributes.OrderBy(a => a.Name),
a =>
{
Assert.Equal("OnClick", a.Name);
Assert.Equal("Microsoft.AspNetCore.Components.EventCallback<T>", a.TypeName);
Assert.False(a.HasIndexer);
Assert.False(a.IsBooleanProperty);
Assert.False(a.IsEnum);
Assert.False(a.IsStringProperty);
Assert.True(a.IsEventCallbackProperty());
Assert.False(a.IsDelegateProperty());
Assert.False(a.IsChildContentProperty());
Assert.True(a.IsGenericTypedProperty());
},
a =>
{
Assert.Equal("T", a.Name);
Assert.Equal("T", a.GetPropertyName());
Assert.Equal("T", a.DisplayName);
Assert.Equal("System.Type", a.TypeName);
Assert.True(a.IsTypeParameterProperty());
});
}
[Fact]
public void Execute_RenderFragmentProperty_CreatesDescriptors()
{

View File

@ -69,6 +69,24 @@ namespace Microsoft.AspNetCore.Components
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static EventCallback GetEventHandlerValue<T>(EventCallback value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>
public static EventCallback<T> GetEventHandlerValue<T>(EventCallback<T> value)
where T : UIEventArgs
{
return value;
}
/// <summary>
/// Not intended to be used directly.
/// </summary>

View File

@ -0,0 +1,33 @@
// 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.Components
{
public readonly struct EventCallback
{
public static readonly EventCallbackFactory Factory = new EventCallbackFactory();
internal readonly MulticastDelegate Delegate;
internal readonly IHandleEvent Receiver;
public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate)
{
Receiver = receiver;
Delegate = @delegate;
}
}
public readonly struct EventCallback<T>
{
internal readonly MulticastDelegate Delegate;
internal readonly IHandleEvent Receiver;
public EventCallback(IHandleEvent receiver, MulticastDelegate @delegate)
{
Receiver = receiver;
Delegate = @delegate;
}
}
}

View File

@ -0,0 +1,43 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
public sealed class EventCallbackFactory
{
public EventCallback Create(object receiver, EventCallback callback) => default;
public EventCallback Create(object receiver, Action callback) => default;
public EventCallback Create(object receiver, Action<object> callback) => default;
public EventCallback Create(object receiver, Func<Task> callback) => default;
public EventCallback Create(object receiver, Func<object, Task> callback) => default;
public EventCallback<T> Create<T>(object receiver, string callback) => default;
public EventCallback<T> Create<T>(object receiver, EventCallback<T> callback) => default;
public EventCallback<T> Create<T>(object receiver, Action callback) => default;
public EventCallback<T> Create<T>(object receiver, Action<T> callback) => default;
public EventCallback<T> Create<T>(object receiver, Func<Task> callback) => default;
public EventCallback<T> Create<T>(object receiver, Func<T, Task> callback) => default;
public EventCallback<T> CreateInferred<T>(object receiver, Action<T> callback, T value)
{
return Create(receiver, callback);
}
public EventCallback<T> CreateInferred<T>(object receiver, Func<T, Task> callback, T value)
{
return Create(receiver, callback);
}
}
}

View File

@ -0,0 +1,107 @@
// 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.Components
{
public static class EventCallbackFactoryBinderExtensions
{
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<string> setter,
string existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<bool> setter,
bool existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<bool?> setter,
bool? existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<int> setter,
int existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<int?> setter,
int? existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<long> setter,
long existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<long?> setter,
long? existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<float> setter,
float existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<float?> setter,
float? existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<double> setter,
double existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<double?> setter,
double? existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<decimal> setter,
decimal existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<decimal?> setter,
decimal? existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<DateTime> setter,
DateTime existingValue) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder(
this EventCallbackFactory factory,
object receiver,
Action<DateTime> setter,
DateTime existingValue,
string format) => default;
public static EventCallback<UIChangeEventArgs> CreateBinder<T>(
this EventCallbackFactory factory,
object receiver,
Action<T> setter,
T existingValue) where T : Enum => default;
}
}

View File

@ -0,0 +1,59 @@
// 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.Threading.Tasks;
namespace Microsoft.AspNetCore.Components
{
public static class EventCallbackFactoryUIEventArgsExtensions
{
public static EventCallback<UIEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIEventArgs> callback) => default;
public static EventCallback<UIEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIEventArgs, Task> callback) => default;
public static EventCallback<UIChangeEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIChangeEventArgs> callback) => default;
public static EventCallback<UIChangeEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIChangeEventArgs, Task> callback) => default;
public static EventCallback<UIClipboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIClipboardEventArgs> callback) => default;
public static EventCallback<UIClipboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIClipboardEventArgs, Task> callback) => default;
public static EventCallback<UIDragEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIDragEventArgs> callback) => default;
public static EventCallback<UIDragEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIDragEventArgs, Task> callback) => default;
public static EventCallback<UIErrorEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIErrorEventArgs> callback) => default;
public static EventCallback<UIErrorEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIErrorEventArgs, Task> callback) => default;
public static EventCallback<UIFocusEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIFocusEventArgs> callback) => default;
public static EventCallback<UIFocusEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIFocusEventArgs, Task> callback) => default;
public static EventCallback<UIKeyboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIKeyboardEventArgs> callback) => default;
public static EventCallback<UIKeyboardEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIKeyboardEventArgs, Task> callback) => default;
public static EventCallback<UIMouseEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIMouseEventArgs> callback) => default;
public static EventCallback<UIMouseEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIMouseEventArgs, Task> callback) => default;
public static EventCallback<UIPointerEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIPointerEventArgs> callback) => default;
public static EventCallback<UIPointerEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIPointerEventArgs, Task> callback) => default;
public static EventCallback<UIProgressEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIProgressEventArgs> callback) => default;
public static EventCallback<UIProgressEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIProgressEventArgs, Task> callback) => default;
public static EventCallback<UITouchEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UITouchEventArgs> callback) => default;
public static EventCallback<UITouchEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UITouchEventArgs, Task> callback) => default;
public static EventCallback<UIWheelEventArgs> Create(this EventCallbackFactory factory, object receiver, Action<UIWheelEventArgs> callback) => default;
public static EventCallback<UIWheelEventArgs> Create(this EventCallbackFactory factory, object receiver, Func<UIWheelEventArgs, Task> callback) => default;
}
}

View File

@ -0,0 +1,9 @@
// 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.
namespace Microsoft.AspNetCore.Components
{
public interface IHandleEvent
{
}
}

View File

@ -72,6 +72,14 @@ namespace Microsoft.AspNetCore.Components.RenderTree
{
}
public void AddAttribute(int sequence, string name, EventCallback value)
{
}
public void AddAttribute<T>(int sequence, string name, EventCallback<T> value)
{
}
public void AddAttribute(int sequence, string name, object value)
{
}