Add [Parameter] for component parameters

This change introduces ParameterAttribute to specify a bindable
component parameter. As of the 0.3 release of Blazor we plan to make
[Parameter] required to make a property bindable by callers.

This also applies to parameters when their value is set by the
infrastructure, such as `Body` for layouts, and route paramters.

The rationale behind this change is that we think there is a need to
separate the definition of properties from their suitability for a
caller to set them through markup. We plan to introduce more features in
this area in the future such as marking parameters as required. This is
first step, and we think that this approach will scale nicely as we add
more functionaly.

The 0.3 release seems like the right time to change this behavior since
we're also introducing `ref` for captures in this release.
This commit is contained in:
Ryan Nowak 2018-04-29 22:14:58 -07:00
Родитель 5a50ce2e8f
Коммит 0f821286c3
24 изменённых файлов: 235 добавлений и 21 удалений

Просмотреть файл

@ -15,5 +15,6 @@
</div>
@functions {
[Parameter]
public RenderFragment Body { get; set; }
}

Просмотреть файл

@ -16,6 +16,11 @@ namespace Microsoft.AspNetCore.Blazor.Razor
public static readonly string BuildRenderTree = nameof(BuildRenderTree);
}
public static class ParameterAttribute
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Components.ParameterAttribute";
}
public static class LayoutAttribute
{
public static readonly string FullTypeName = "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";

Просмотреть файл

@ -42,6 +42,13 @@ namespace Microsoft.AspNetCore.Blazor.Razor
return;
}
var parameterSymbol = compilation.GetTypeByMetadataName(BlazorApi.ParameterAttribute.FullTypeName);
if (parameterSymbol == null)
{
// No definition for [Parameter], nothing to do.
return;
}
var types = new List<INamedTypeSymbol>();
var visitor = new ComponentTypeVisitor(componentSymbol, types);
@ -60,17 +67,22 @@ namespace Microsoft.AspNetCore.Blazor.Razor
for (var i = 0; i < types.Count; i++)
{
var type = types[i];
context.Results.Add(CreateDescriptor(type));
context.Results.Add(CreateDescriptor(type, parameterSymbol));
}
}
private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type)
private TagHelperDescriptor CreateDescriptor(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol)
{
if (type == null)
{
throw new ArgumentNullException(nameof(type));
}
if (parameterSymbol == null)
{
throw new ArgumentNullException(nameof(parameterSymbol));
}
var typeName = type.ToDisplayString(FullNameTypeDisplayFormat);
var assemblyName = type.ContainingAssembly.Identity.Name;
@ -90,7 +102,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// Components have very simple matching rules. The type name (short) matches the tag name.
builder.TagMatchingRule(r => r.TagName = type.Name);
foreach (var property in GetVisibleProperties(type))
foreach (var property in GetProperties(type, parameterSymbol))
{
if (property.kind == PropertyKind.Ignored)
{
@ -134,7 +146,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
// - has public getter
// - has public setter
// - is not an indexer
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetVisibleProperties(INamedTypeSymbol type)
private IEnumerable<(IPropertySymbol property, PropertyKind kind)> GetProperties(INamedTypeSymbol type, INamedTypeSymbol parameterSymbol)
{
var properties = new Dictionary<string, (IPropertySymbol, PropertyKind)>(StringComparer.Ordinal);
do
@ -174,6 +186,12 @@ namespace Microsoft.AspNetCore.Blazor.Razor
kind = PropertyKind.Ignored;
}
if (!property.GetAttributes().Any(a => a.AttributeClass == parameterSymbol))
{
// Does not have [Parameter]
kind = PropertyKind.Ignored;
}
if (kind == PropertyKind.Default && property.Type.TypeKind == TypeKind.Enum)
{
kind = PropertyKind.Enum;

Просмотреть файл

@ -14,5 +14,6 @@
@functions
{
// This is to demonstrate how a parent component can supply parameters
[Parameter]
public string Title { get; set; }
}

Просмотреть файл

@ -14,5 +14,6 @@
@functions
{
// This is to demonstrate how a parent component can supply parameters
[Parameter]
public string Title { get; set; }
}

Просмотреть файл

@ -0,0 +1,15 @@
// 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
{
/// <summary>
/// Denotes the target member as a component parameter.
/// </summary>
[AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = false)]
public sealed class ParameterAttribute : Attribute
{
}
}

Просмотреть файл

@ -66,6 +66,13 @@ namespace Microsoft.AspNetCore.Blazor.Components
$"matching the name '{propertyName}'.");
}
if (!property.IsDefined(typeof(ParameterAttribute)))
{
throw new InvalidOperationException(
$"Object of type '{targetType.FullName}' has a property matching the name '{propertyName}', " +
$"but it does not have [{nameof(ParameterAttribute)}] applied.");
}
return property;
}
}

Просмотреть файл

@ -21,11 +21,13 @@ namespace Microsoft.AspNetCore.Blazor.Layouts
/// Gets or sets the type of the page component to display.
/// The type must implement <see cref="IComponent"/>.
/// </summary>
[Parameter]
public Type Page { get; set; }
/// <summary>
/// Gets or sets the parameters to pass to the page.
/// </summary>
[Parameter]
public IDictionary<string, object> PageParameters { get; set; }
/// <inheritdoc />

Просмотреть файл

@ -38,11 +38,13 @@ namespace Microsoft.AspNetCore.Blazor.Routing
/// Gets or sets the CSS class name applied to the NavLink when the
/// current route matches the NavLink href.
/// </summary>
[Parameter]
public string ActiveClass { get; set; }
/// <summary>
/// Gets or sets a value representing the URL matching behavior.
/// </summary>
[Parameter]
public NavLinkMatch Match { get; set; }
[Inject] private IUriHelper UriHelper { get; set; }

Просмотреть файл

@ -29,6 +29,7 @@ namespace Microsoft.AspNetCore.Blazor.Routing
/// Gets or sets the assembly that should be searched, along with its referenced
/// assemblies, for components matching the URI.
/// </summary>
[Parameter]
public Assembly AppAssembly { get; set; }
private RouteTable Routes { get; set; }

Просмотреть файл

@ -24,8 +24,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public int Value { get; set; }
[Parameter]
public Action<int> ValueChanged { get; set; }
}
}"));
@ -96,8 +98,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public int Value { get; set; }
[Parameter]
public Action<int> OnChanged { get; set; }
}
}"));

Просмотреть файл

@ -57,10 +57,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
public int IntProperty { get; set; }
public bool BoolProperty { get; set; }
public string StringProperty { get; set; }
public SomeType ObjectProperty { get; set; }
[Parameter] public int IntProperty { get; set; }
[Parameter] public bool BoolProperty { get; set; }
[Parameter] public string StringProperty { get; set; }
[Parameter] public SomeType ObjectProperty { get; set; }
}
}
"));
@ -90,6 +90,36 @@ namespace Test
});
}
[Fact]
public void Render_ChildComponent_TriesToSetNonParamter()
{
// Arrange
AdditionalSyntaxTrees.Add(Parse(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public class MyComponent : BlazorComponent
{
public int IntProperty { get; set; }
}
}
"));
var component = CompileToComponent(@"
@addTagHelper *, TestAssembly
<MyComponent IntProperty=""123"" />");
// Act
var ex = Assert.Throws<InvalidOperationException>(() => GetRenderTree(component));
// Assert
Assert.Equal(
"Object of type 'Test.MyComponent' has a property matching the name 'IntProperty', " +
"but it does not have [ParameterAttribute] applied.",
ex.Message);
}
[Fact]
public void Render_ChildComponent_WithExplicitStringParameter()
{
@ -101,6 +131,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public string StringProperty { get; set; }
}
}
@ -174,6 +205,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public Action<UIMouseEventArgs> OnClick { get; set; }
}
}
@ -221,6 +253,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public Action<UIEventArgs> OnClick { get; set; }
}
}
@ -267,6 +300,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public bool BoolProperty { get; set; }
}
}"));
@ -296,7 +330,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public string MyAttr { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}
}
@ -338,6 +375,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public RenderFragment ChildContent { get; set; }
}
}

Просмотреть файл

@ -1,7 +1,6 @@
// 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 Microsoft.CodeAnalysis.CSharp;
using Xunit;
namespace Microsoft.AspNetCore.Blazor.Build.Test
@ -27,10 +26,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
public int IntProperty { get; set; }
public bool BoolProperty { get; set; }
public string StringProperty { get; set; }
public SomeType ObjectProperty { get; set; }
[Parameter] public int IntProperty { get; set; }
[Parameter] public bool BoolProperty { get; set; }
[Parameter] public string StringProperty { get; set; }
[Parameter] public SomeType ObjectProperty { get; set; }
}
}
"));
@ -61,6 +60,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public string StringProperty { get; set; }
}
}
@ -116,6 +116,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public Action<UIEventArgs> OnClick { get; set; }
}
}
@ -152,6 +153,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public Action<UIEventArgs> OnClick { get; set; }
}
}
@ -188,8 +190,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public string MyAttr { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}
}
@ -382,8 +386,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public int Value { get; set; }
[Parameter]
public Action<int> ValueChanged { get; set; }
}
}"));
@ -446,8 +452,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public int Value { get; set; }
[Parameter]
public Action<int> OnChanged { get; set; }
}
}"));

Просмотреть файл

@ -121,6 +121,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public class TestLayout : ILayoutComponent
{
[Parameter]
public RenderFragment Body { get; set; }
public void Init(RenderHandle renderHandle)

Просмотреть файл

@ -51,10 +51,10 @@ namespace Test
public class MyComponent : BlazorComponent
{
public int IntProperty { get; set; }
public bool BoolProperty { get; set; }
public string StringProperty { get; set; }
public SomeType ObjectProperty { get; set; }
[Parameter] public int IntProperty { get; set; }
[Parameter] public bool BoolProperty { get; set; }
[Parameter] public string StringProperty { get; set; }
[Parameter] public SomeType ObjectProperty { get; set; }
}
}
"));
@ -85,6 +85,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public string StringProperty { get; set; }
}
}
@ -141,6 +142,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public Action<UIEventArgs> OnClick { get; set; }
}
}
@ -177,6 +179,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public Action<UIEventArgs> OnClick { get; set; }
}
}
@ -213,8 +216,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public string MyAttr { get; set; }
[Parameter]
public RenderFragment ChildContent { get; set; }
}
}
@ -635,8 +640,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public int Value { get; set; }
[Parameter]
public Action<int> ValueChanged { get; set; }
}
}"));
@ -699,8 +706,10 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public int Value { get; set; }
[Parameter]
public Action<int> OnChanged { get; set; }
}
}"));

Просмотреть файл

@ -27,8 +27,10 @@ namespace Test
public void SetParameters(ParameterCollection parameters) { }
[Parameter]
public string MyProperty { get; set; }
[Parameter]
public Action<string> MyPropertyChanged { get; set; }
}
}

Просмотреть файл

@ -27,6 +27,7 @@ namespace Test
public void SetParameters(ParameterCollection parameters) { }
[Parameter]
public string MyProperty { get; set; }
}
}
@ -136,6 +137,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public string MyProperty { get; set; }
}
}
@ -176,6 +178,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public bool MyProperty { get; set; }
}
}
@ -227,6 +230,7 @@ namespace Test
public class MyComponent : BlazorComponent
{
[Parameter]
public MyEnum MyProperty { get; set; }
}
}
@ -274,6 +278,7 @@ namespace Test
{
public class MyComponent : BlazorComponent
{
[Parameter]
public Action<UIMouseEventArgs> OnClick { get; set; }
}
}
@ -307,5 +312,61 @@ namespace Test
Assert.False(attribute.IsStringProperty);
Assert.True(attribute.IsDelegateProperty());
}
[Fact] // This component has lots of properties that don't become components.
public void Execute_IgnoredProperties_CreatesDescriptor()
{
// Arrange
var compilation = BaseCompilation.AddSyntaxTrees(Parse(@"
using Microsoft.AspNetCore.Blazor.Components;
namespace Test
{
public abstract class MyBase : BlazorComponent
{
[Parameter]
public string Hidden { get; set; }
}
public class MyComponent : MyBase
{
[Parameter]
public string NoPublicGetter { private get; set; }
public string NoParameterAttribute { get; set; }
// No attribute here, hides base-class property of the same name.
public new int Hidden { get; set; }
public string this[int i]
{
get { throw null; }
set { throw null; }
}
}
}
"));
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.Empty(component.BoundAttributes);
}
}
}

Просмотреть файл

@ -1,6 +1,7 @@
// 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 Microsoft.AspNetCore.Blazor.Components;
using Microsoft.AspNetCore.Blazor.Layouts;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Helpers;
@ -219,6 +220,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class RootLayout : AutoRenderComponent, ILayoutComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
@ -232,6 +234,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
[Layout(typeof(RootLayout))]
private class NestedLayout : AutoRenderComponent, ILayoutComponent
{
[Parameter]
public RenderFragment Body { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)

Просмотреть файл

@ -1503,12 +1503,23 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class FakeComponent : IComponent
{
[Parameter]
public int IntProperty { get; set; }
[Parameter]
public string StringProperty { get; set; }
[Parameter]
public object ObjectProperty { get; set; }
[Parameter]
public string ReadonlyProperty { get; private set; }
[Parameter]
private string PrivateProperty { get; set; }
public string NonParameterProperty { get; set; }
public void Init(RenderHandle renderHandle) { }
public void SetParameters(ParameterCollection parameters)
{

Просмотреть файл

@ -1109,6 +1109,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class MessageComponent : AutoRenderComponent
{
[Parameter]
public string Message { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
@ -1119,9 +1120,15 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class FakeComponent : IComponent
{
[Parameter]
public int IntProperty { get; set; }
[Parameter]
public string StringProperty { get; set; }
[Parameter]
public object ObjectProperty { get; set; }
public RenderHandle RenderHandle { get; private set; }
public void Init(RenderHandle renderHandle)
@ -1133,8 +1140,13 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class EventComponent : AutoRenderComponent, IComponent, IHandleEvent
{
[Parameter]
public Action<UIEventArgs> OnTest { get; set; }
[Parameter]
public Action<UIMouseEventArgs> OnClick { get; set; }
[Parameter]
public Action OnClickAction { get; set; }
public bool SkipElement { get; set; }
@ -1174,7 +1186,10 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class ConditionalParentComponent<T> : AutoRenderComponent where T : IComponent
{
[Parameter]
public bool IncludeChild { get; set; }
[Parameter]
public IDictionary<string, object> ChildParameters { get; set; }
protected override void BuildRenderTree(RenderTreeBuilder builder)
@ -1199,6 +1214,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class ReRendersParentComponent : AutoRenderComponent
{
[Parameter]
public TestComponent Parent { get; set; }
private bool _isFirstTime = true;
@ -1216,6 +1232,7 @@ namespace Microsoft.AspNetCore.Blazor.Test
private class RendersSelfAfterEventComponent : IComponent, IHandleEvent
{
[Parameter]
public Action<object> OnClick { get; set; }
private RenderHandle _renderHandle;

Просмотреть файл

@ -1,4 +1,5 @@
<span style="color: red" class="message">@Message</span>
@using Microsoft.AspNetCore.Blazor.Components
<span style="color: red" class="message">@Message</span>
@functions {
public string Message { get; set; }
[Parameter] public string Message { get; set; }
}

Просмотреть файл

@ -1,8 +1,9 @@
@using Microsoft.AspNetCore.Blazor
@using Microsoft.AspNetCore.Blazor.Components
@ChildContent
@functions {
// Note: The lack of any whitespace or other output besides @ChildContent is important for
// what scenarios this component is used for in E2E tests.
public RenderFragment ChildContent { get; set; }
[Parameter] public RenderFragment ChildContent { get; set; }
}

Просмотреть файл

@ -1,7 +1,9 @@
<div class="supplied">You supplied: @SuppliedValue</div>
@using Microsoft.AspNetCore.Blazor.Components
<div class="supplied">You supplied: @SuppliedValue</div>
<div class="computed">I computed: @computedValue</div>
@functions {
[Parameter]
public int SuppliedValue { get; set; }
private int computedValue;

Просмотреть файл

@ -1,11 +1,14 @@
@page "/RouterTest/WithParameters/Name/{firstName}/LastName/{lastName}"
@using BasicTestApp.RouterTest
@using Microsoft.AspNetCore.Blazor.Components
<div id="test-info">Your full name is @FirstName @LastName.</div>
<Links />
@functions
{
[Parameter]
public string FirstName { get; set; }
[Parameter]
public string LastName { get ; set; }
}