Skip to content
This repository has been archived by the owner on Feb 25, 2021. It is now read-only.

Commit

Permalink
Remove old workaround @OnClick and @Bind
Browse files Browse the repository at this point in the history
This change removes support for the old syntax used for event handlers
and two-way binding.

See the relevant issues for details on the new features and
improvements:

bind https://github.com/aspnet/Blazor/issues/409
event handlers https://github.com/aspnet/Blazor/issues/503

Along with this change we've removed a few additional things Blazor
could do that aren't part of Razor's usual syntax.

----

The features that was used to make something like:
```
<button @OnClick(...) />
```

is an expression that's embedded in a an element's attribute. This
feature might be useful in the future if we want to support 'splatting'
arbitrary attributes into a tag, but the runtime support for this isn't
accessible outside the Blazor core.

----

The features that implement:
```
<button onclick=@{ } />
```

have been removed in favor of a better design for lambdas, method group
conversions and other things for event handler attributes.

use `<button onclick=@(x => ...} />` instead.

We think is a better approach in general, because we want the app
developer to write and see the parameter list.

----

Both syntactic features that have been removed have dedicated error
messages in the compiler. If you're porting old code it should help you
figure out what to do.
  • Loading branch information
rynowak authored and Ryan Nowak committed Apr 10, 2018
1 parent 32ad47b commit cda57e7
Show file tree
Hide file tree
Showing 15 changed files with 162 additions and 222 deletions.
5 changes: 3 additions & 2 deletions samples/StandaloneApp/Pages/Counter.cshtml
@@ -1,14 +1,15 @@
@page "/counter"
@using Microsoft.AspNetCore.Blazor
<h1>Counter</h1>

<p>Current count: @currentCount</p>

<button @onclick(IncrementCount)>Click me</button>
<button onclick="@IncrementCount">Click me</button>

@functions {
int currentCount = 0;

void IncrementCount()
void IncrementCount(UIMouseEventArgs e)
{
currentCount++;
}
Expand Down
Expand Up @@ -5,14 +5,50 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AngleSharp;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.Intermediate;

namespace Microsoft.AspNetCore.Blazor.Razor
{
internal static class BlazorDiagnosticFactory
{
public static readonly RazorDiagnosticDescriptor CodeBlockInAttribute =
new RazorDiagnosticDescriptor(
"BL9979",
() =>
"Code blocks delimited by '@{...}' like '@{{ {0} }}' for attributes are no longer supported " +
"These features have been changed to use attribute syntax. " +
"Use 'attr=\"@(x => {... }\"'.",
RazorDiagnosticSeverity.Error);

public static RazorDiagnostic Create_CodeBlockInAttribute(SourceSpan? source, string expression)
{
var diagnostic = RazorDiagnostic.Create(
CodeBlockInAttribute,
source ?? SourceSpan.Undefined,
expression);
return diagnostic;
}

public static readonly RazorDiagnosticDescriptor ExpressionInAttributeList =
new RazorDiagnosticDescriptor(
"BL9980",
() =>
"Expressions like '{0}' inside of a tag must be part of an attribute. " +
"Previous releases of Blazor supported constructs like '@onclick(...)' or '@bind(...)'." +
"These features have been changed to use attribute syntax. " +
"Use 'onclick=\"@...\"' or 'bind=\"...\" respectively.",
RazorDiagnosticSeverity.Error);

public static RazorDiagnostic Create_ExpressionInAttributeList(SourceSpan? source, string expression)
{
var diagnostic = RazorDiagnostic.Create(
ExpressionInAttributeList,
source ?? SourceSpan.Undefined,
expression);
return diagnostic;
}

public static readonly RazorDiagnosticDescriptor UnexpectedClosingTag = new RazorDiagnosticDescriptor(
"BL9981",
() => "Unexpected closing tag '{0}' with no matching start tag.",
Expand Down
Expand Up @@ -5,14 +5,12 @@
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using AngleSharp;
using AngleSharp.Html;
using AngleSharp.Parser.Html;
using Microsoft.AspNetCore.Razor.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using Microsoft.CodeAnalysis.CSharp;

namespace Microsoft.AspNetCore.Blazor.Razor
{
Expand All @@ -27,27 +25,18 @@ internal class BlazorRuntimeNodeWriter : BlazorNodeWriter
= new HashSet<string>(
new[] { "area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr" },
StringComparer.OrdinalIgnoreCase);
private readonly static Regex bindExpressionRegex = new Regex(@"^bind\((.+)\)$");
private readonly static CSharpParseOptions bindArgsParseOptions
= CSharpParseOptions.Default.WithKind(CodeAnalysis.SourceCodeKind.Script);

private readonly ScopeStack _scopeStack = new ScopeStack();
private string _unconsumedHtml;
private List<IntermediateToken> _currentAttributeValues;
private IDictionary<string, PendingAttribute> _currentElementAttributes = new Dictionary<string, PendingAttribute>();
private IList<PendingAttributeToken> _currentElementAttributeTokens = new List<PendingAttributeToken>();
private int _sourceSequence = 0;

private struct PendingAttribute
{
public List<IntermediateToken> Values { get; set; }
}

private struct PendingAttributeToken
{
public IntermediateToken AttributeValue;
}

public override void WriteCSharpCode(CodeRenderingContext context, CSharpCodeIntermediateNode node)
{
if (context == null)
Expand Down Expand Up @@ -118,27 +107,26 @@ public override void WriteCSharpCodeAttributeValue(CodeRenderingContext context,
throw new InvalidOperationException($"Invoked {nameof(WriteCSharpCodeAttributeValue)} while {nameof(_currentAttributeValues)} was null.");
}

// For attributes like "onsomeevent=@{ /* some C# code */ }", we treat it as if you
// wrote "onsomeevent=@(_ => { /* some C# code */ })" because then it works as an
// event handler and is a reasonable syntax for that.
var innerCSharp = (IntermediateToken)node.Children.Single();
innerCSharp.Content = $"_ => {{ {innerCSharp.Content} }}";
_currentAttributeValues.Add(innerCSharp);
// We used to support syntaxes like <elem onsomeevent=@{ /* some C# code */ } /> but this is no longer the
// case.
//
// We provide an error for this case just to be friendly.
var content = string.Join("", node.Children.OfType<IntermediateToken>().Select(t => t.Content));
context.Diagnostics.Add(BlazorDiagnosticFactory.Create_CodeBlockInAttribute(node.Source, content));
return;
}

public override void WriteCSharpExpression(CodeRenderingContext context, CSharpExpressionIntermediateNode node)
{
// To support syntax like <elem @completeAttributePair /> (which in turn supports syntax
// like <elem @OnSomeEvent(Handler) />), check whether we are currently in the middle of
// writing an element. If so, treat this C# expression as something that should evaluate
// as a RenderTreeFrame of type Attribute.
// We used to support syntaxes like <elem @completeAttributePair /> but this is no longer the case.
// The APIs that a user would need to do this correctly aren't accessible outside of Blazor's core
// anyway.
//
// We provide an error for this case just to be friendly.
if (_unconsumedHtml != null)
{
var token = (IntermediateToken)node.Children.Single();
_currentElementAttributeTokens.Add(new PendingAttributeToken
{
AttributeValue = token
});
var content = string.Join("", node.Children.OfType<IntermediateToken>().Select(t => t.Content));
context.Diagnostics.Add(BlazorDiagnosticFactory.Create_ExpressionInAttributeList(node.Source, content));
return;
}

Expand Down Expand Up @@ -279,15 +267,6 @@ public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentI
_currentElementAttributes.Clear();
}

if (_currentElementAttributeTokens.Count > 0)
{
foreach (var token in _currentElementAttributeTokens)
{
WriteElementAttributeToken(context, nextTag, token);
}
_currentElementAttributeTokens.Clear();
}

_scopeStack.OpenScope( tagName: nextTag.Data, isComponent: false);
}

Expand Down Expand Up @@ -325,56 +304,6 @@ public override void WriteHtmlContent(CodeRenderingContext context, HtmlContentI
}
}

private void WriteElementAttributeToken(CodeRenderingContext context, HtmlTagToken tag, PendingAttributeToken token)
{
var bindMatch = bindExpressionRegex.Match(token.AttributeValue.Content);
if (bindMatch.Success)
{
// TODO: Consider alternatives to the @bind syntax. The following is very strange.

// The @bind(X, Y, Z, ...) syntax is special. We convert it to a pair of attributes:
// [1] value=@BindMethods.GetValue(X, Y, Z, ...)
var valueParams = bindMatch.Groups[1].Value;
WriteAttribute(context.CodeWriter, "value", new[]
{
new IntermediateToken
{
Kind = TokenKind.CSharp,
Content = $"{BlazorApi.BindMethods.GetValue}({valueParams})"
}
});

// [2] @onchange(BindSetValue(parsed => { X = parsed; }, X, Y, Z, ...))
var parsedArgs = CSharpSyntaxTree.ParseText(valueParams, bindArgsParseOptions);
var parsedArgsSplit = parsedArgs.GetRoot().ChildNodes().Select(x => x.ToString()).ToList();
if (parsedArgsSplit.Count > 0)
{
parsedArgsSplit.Insert(0, $"_parsedValue_ => {{ {parsedArgsSplit[0]} = _parsedValue_; }}");
}
var parsedArgsJoined = string.Join(", ", parsedArgsSplit);
var onChangeAttributeToken = new PendingAttributeToken
{
AttributeValue = new IntermediateToken
{
Kind = TokenKind.CSharp,
Content = $"onchange({BlazorApi.BindMethods.SetValue}({parsedArgsJoined}))"
}
};
WriteElementAttributeToken(context, tag, onChangeAttributeToken);
}
else
{
// For any other attribute token (e.g., @onclick(...)), treat it as an expression
// that will evaluate as an attribute frame
context.CodeWriter
.WriteStartMethodInvocation($"{_scopeStack.BuilderVarName}.{nameof(BlazorApi.RenderTreeBuilder.AddAttribute)}")
.Write((_sourceSequence++).ToString())
.WriteParameterSeparator()
.Write(token.AttributeValue.Content)
.WriteEndMethodInvocation();
}
}

public override void WriteUsingDirective(CodeRenderingContext context, UsingDirectiveIntermediateNode node)
{
context.CodeWriter.WriteUsing(node.Content, endLine: true);
Expand Down
Expand Up @@ -20,6 +20,7 @@ namespace Microsoft.AspNetCore.Blazor.Components
[BindInputElement("text", null, "value", "onchange")]

[BindElement("select", null, "value", "onchange")]
[BindElement("textarea", null, "value", "onchange")]
public static class BindAttributes
{
}
Expand Down
45 changes: 0 additions & 45 deletions src/Microsoft.AspNetCore.Blazor/Components/BlazorComponent.cs
Expand Up @@ -178,50 +178,5 @@ void IHandleEvent.HandleEvent(UIEventHandler handler, UIEventArgs args)
// at the end of every event callback.
StateHasChanged();
}

// At present, if you have a .cshtml file in a project with <Project Sdk="Microsoft.NET.Sdk.Web">,
// Visual Studio will run design-time builds for it, codegenning a class that attempts to override
// this method. Therefore the virtual method must be defined, even though it won't be used at runtime,
// because otherwise VS will display a design-time error in its 'Error List' pane.
// TODO: Track down what triggers the design-time build for .cshtml files and how to stop it, then
// this method can be removed.
/// <summary>
/// Not used. Do not invoke this method.
/// </summary>
/// <returns>Always throws an exception.</returns>
public virtual Task ExecuteAsync()
=> throw new NotImplementedException($"Blazor components do not implement {nameof(ExecuteAsync)}.");

/// <summary>
/// Applies two-way data binding between the element and the property.
/// </summary>
/// <param name="value">The model property to be bound to the element.</param>
protected RenderTreeFrame bind(object value)
=> throw new NotImplementedException($"{nameof(bind)} is a compile-time symbol only and should not be invoked.");

/// <summary>
/// Applies two-way data binding between the element and the property.
/// </summary>
/// <param name="value">The model property to be bound to the element.</param>
protected RenderTreeFrame bind(DateTime value, string format)
=> throw new NotImplementedException($"{nameof(bind)} is a compile-time symbol only and should not be invoked.");

/// <summary>
/// Handles click events by invoking <paramref name="handler"/>.
/// </summary>
/// <param name="handler">The handler to be invoked when the event occurs.</param>
/// <returns>A <see cref="RenderTreeFrame"/> that represents the event handler.</returns>
protected RenderTreeFrame onclick(Action handler)
// Note that the 'sequence' value is updated later when inserted into the tree
=> RenderTreeFrame.Attribute(0, "onclick", handler != null ? (_ => handler()) : (UIEventHandler)null);

/// <summary>
/// Handles change events by invoking <paramref name="handler"/>.
/// </summary>
/// <param name="handler">The handler to be invoked when the event occurs. The handler will receive the new value as a parameter.</param>
/// <returns>A <see cref="RenderTreeFrame"/> that represents the event handler.</returns>
protected RenderTreeFrame onchange(Action<object> handler)
// Note that the 'sequence' value is updated later when inserted into the tree
=> RenderTreeFrame.Attribute(0, "onchange", args => handler(((UIChangeEventArgs)args).Value));
}
}
Expand Up @@ -47,5 +47,45 @@ public void RejectsEndTagWithDifferentNameToStartTag()
Assert.Equal(20, item.Span.CharacterIndex);
});
}

// This is the old syntax used by @bind and @onclick, it's explicitly unsupported
// and has its own diagnostic.
[Fact]
public void OldEventHandlerSyntax_ReportsError()
{
// Arrange/Act
var generated = CompileToCSharp(@"
<elem @foo(MyHandler) />
@functions {
void MyHandler()
{
}
string foo(Action action)
{
return action.ToString();
}
}");

// Assert
var diagnostic = Assert.Single(generated.Diagnostics);
Assert.Equal("BL9980", diagnostic.Id);
}

// This used to be a sugar syntax for lambdas, but we don't support that anymore
[Fact]
public void OldCodeBlockAttributeSyntax_ReportsError()
{
// Arrange/Act
var generated = CompileToCSharp(@"
<elem attr=@{ DidInvokeCode = true; } />
@functions {
public bool DidInvokeCode { get; set; } = false;
}");

// Assert
var diagnostic = Assert.Single(generated.Diagnostics);
Assert.Equal("BL9979", diagnostic.Id);
}
}
}

0 comments on commit cda57e7

Please sign in to comment.