Browse Source
* Update ncrunch config. * Fix test naming. * Add BindingExpressionGrammar tests. We only had tests for the error states, and that was named incorrectly. * Parse null-conditionals in bindings. * Initial impl of null conditionals in binding path. Fixes #17029. * Ensure that nothing is logged. * Make "?." work when binding to methods. * Don't add a new public API. And add a comment reminding us to make this class internal for 12.0. * Use existing method.pull/18450/head
committed by
GitHub
15 changed files with 680 additions and 45 deletions
@ -0,0 +1,5 @@ |
|||
<ProjectConfiguration> |
|||
<Settings> |
|||
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
|||
</Settings> |
|||
</ProjectConfiguration> |
|||
@ -0,0 +1,5 @@ |
|||
<ProjectConfiguration> |
|||
<Settings> |
|||
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely> |
|||
</Settings> |
|||
</ProjectConfiguration> |
|||
@ -0,0 +1,27 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Base.UnitTests.Data.Core; |
|||
|
|||
/// <summary>
|
|||
/// A <see cref="TextBox"/> which stores the latest binding error state.
|
|||
/// </summary>
|
|||
public class ErrorCollectingTextBox : TextBox |
|||
{ |
|||
public Exception? Error { get; private set; } |
|||
public BindingValueType ErrorState { get; private set; } |
|||
|
|||
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValueType state, Exception? error) |
|||
{ |
|||
if (property == TextProperty) |
|||
{ |
|||
Error = error; |
|||
ErrorState = state; |
|||
} |
|||
|
|||
base.UpdateDataValidation(property, state, error); |
|||
} |
|||
} |
|||
@ -0,0 +1,184 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Data; |
|||
using Avalonia.Logging; |
|||
using Avalonia.Markup.Xaml; |
|||
using Avalonia.UnitTests; |
|||
using Xunit; |
|||
|
|||
#nullable enable |
|||
|
|||
namespace Avalonia.Base.UnitTests.Data.Core; |
|||
|
|||
/// <summary>
|
|||
/// Tests for null-conditional operator in binding paths.
|
|||
/// </summary>
|
|||
/// <remarks>
|
|||
/// Ideally these would be part of the <see cref="BindingExpressionTests"/> suite but that uses
|
|||
/// C# expression trees as an abstraction to represent both reflection and compiled binding paths.
|
|||
/// This is a problem because expression trees don't support the C# null-conditional operator
|
|||
/// and I have no desire to refactor all of those tests right now.
|
|||
/// </remarks>
|
|||
public class NullConditionalBindingTests |
|||
{ |
|||
[Theory] |
|||
[InlineData(false)] |
|||
[InlineData(true)] |
|||
public void Should_Report_Error_Without_Null_Conditional_Operator(bool compileBindings) |
|||
{ |
|||
// Testing the baseline: should report a null error without null conditionals.
|
|||
using var app = Start(); |
|||
using var log = TestLogger.Create(); |
|||
|
|||
var xaml = $$$"""
|
|||
<Window xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
xmlns:local='using:Avalonia.Base.UnitTests.Data.Core' |
|||
x:DataType='local:NullConditionalBindingTests+First' |
|||
x:CompileBindings='{{{compileBindings}}}'> |
|||
<local:ErrorCollectingTextBox Text='{Binding Second.Third.Final}'/> |
|||
</Window> |
|||
""";
|
|||
var data = new First(new Second(null)); |
|||
var window = CreateTarget(xaml, data); |
|||
var textBox = Assert.IsType<ErrorCollectingTextBox>(window.Content); |
|||
var error = Assert.IsType<BindingChainException>(textBox.Error); |
|||
var message = Assert.Single(log.Messages); |
|||
|
|||
Assert.Null(textBox.Text); |
|||
Assert.Equal("Second.Third.Final", error.Expression); |
|||
Assert.Equal("Third", error.ExpressionErrorPoint); |
|||
Assert.Equal(BindingValueType.BindingError, textBox.ErrorState); |
|||
Assert.Equal("An error occurred binding {Property} to {Expression} at {ExpressionErrorPoint}: {Message}", message); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(false)] |
|||
[InlineData(true)] |
|||
public void Should_Not_Report_Error_With_Null_Conditional_Operator(bool compileBindings) |
|||
{ |
|||
using var app = Start(); |
|||
using var log = TestLogger.Create(); |
|||
var xaml = $$$"""
|
|||
<Window xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
xmlns:local='using:Avalonia.Base.UnitTests.Data.Core' |
|||
x:DataType='local:NullConditionalBindingTests+First' |
|||
x:CompileBindings='{{{compileBindings}}}'> |
|||
<local:ErrorCollectingTextBox Text='{Binding Second.Third?.Final}'/> |
|||
</Window> |
|||
""";
|
|||
var data = new First(new Second(null)); |
|||
var window = CreateTarget(xaml, data); |
|||
var textBox = Assert.IsType<ErrorCollectingTextBox>(window.Content); |
|||
|
|||
Assert.Null(textBox.Text); |
|||
Assert.Null(textBox.Error); |
|||
Assert.Equal(BindingValueType.Value, textBox.ErrorState); |
|||
Assert.Empty(log.Messages); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(false)] |
|||
[InlineData(true)] |
|||
public void Should_Not_Report_Error_With_Null_Conditional_Operator_Before_Method(bool compileBindings) |
|||
{ |
|||
using var app = Start(); |
|||
using var log = TestLogger.Create(); |
|||
var xaml = $$$"""
|
|||
<Window xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
xmlns:local='using:Avalonia.Base.UnitTests.Data.Core' |
|||
x:DataType='local:NullConditionalBindingTests+First' |
|||
x:CompileBindings='{{{compileBindings}}}'> |
|||
<local:ErrorCollectingTextBox Text='{Binding Second.Third?.Greeting}'/> |
|||
</Window> |
|||
""";
|
|||
var data = new First(new Second(null)); |
|||
var window = CreateTarget(xaml, data); |
|||
var textBox = Assert.IsType<ErrorCollectingTextBox>(window.Content); |
|||
|
|||
Assert.Null(textBox.Text); |
|||
Assert.Null(textBox.Error); |
|||
Assert.Equal(BindingValueType.Value, textBox.ErrorState); |
|||
Assert.Empty(log.Messages); |
|||
} |
|||
|
|||
[Theory] |
|||
[InlineData(false)] |
|||
[InlineData(true)] |
|||
public void Should_Use_TargetNullValue_With_Null_Conditional_Operator(bool compileBindings) |
|||
{ |
|||
using var app = Start(); |
|||
using var log = TestLogger.Create(); |
|||
var xaml = $$$"""
|
|||
<Window xmlns='https://github.com/avaloniaui'
|
|||
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
|
|||
xmlns:local='using:Avalonia.Base.UnitTests.Data.Core' |
|||
x:DataType='local:NullConditionalBindingTests+First' |
|||
x:CompileBindings='{{{compileBindings}}}'> |
|||
<local:ErrorCollectingTextBox Text='{Binding Second.Third?.Final, TargetNullValue=ItsNull}'/> |
|||
</Window> |
|||
""";
|
|||
var data = new First(new Second(null)); |
|||
var window = CreateTarget(xaml, data); |
|||
var textBox = Assert.IsType<ErrorCollectingTextBox>(window.Content); |
|||
|
|||
Assert.Equal("ItsNull", textBox.Text); |
|||
Assert.Null(textBox.Error); |
|||
Assert.Equal(BindingValueType.Value, textBox.ErrorState); |
|||
Assert.Empty(log.Messages); |
|||
} |
|||
|
|||
private Window CreateTarget(string xaml, object? data) |
|||
{ |
|||
var result = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); |
|||
result.DataContext = data; |
|||
result.Show(); |
|||
return result; |
|||
} |
|||
|
|||
private static IDisposable Start() |
|||
{ |
|||
return UnitTestApplication.Start(TestServices.StyledWindow); |
|||
} |
|||
|
|||
public record First(Second? Second); |
|||
public record Second(Third? Third); |
|||
public record Third(string Final) |
|||
{ |
|||
public string Greeting() => "Hello!"; |
|||
} |
|||
|
|||
private class TestLogger : ILogSink, IDisposable |
|||
{ |
|||
private TestLogger() { } |
|||
|
|||
public IList<string> Messages { get; } = []; |
|||
|
|||
public static TestLogger Create() |
|||
{ |
|||
var result = new TestLogger(); |
|||
Logger.Sink = result; |
|||
return result; |
|||
} |
|||
|
|||
public void Dispose() => Logger.Sink = null; |
|||
|
|||
public bool IsEnabled(LogEventLevel level, string area) |
|||
{ |
|||
return level >= LogEventLevel.Warning && area == LogArea.Binding; |
|||
} |
|||
|
|||
public void Log(LogEventLevel level, string area, object? source, string messageTemplate) |
|||
{ |
|||
Messages.Add(messageTemplate); |
|||
} |
|||
|
|||
public void Log(LogEventLevel level, string area, object? source, string messageTemplate, params object?[] propertyValues) |
|||
{ |
|||
Messages.Add(messageTemplate); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,260 @@ |
|||
using System.Collections.Generic; |
|||
using Avalonia.Markup.Parsers; |
|||
using Avalonia.Utilities; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Markup.UnitTests.Parsers |
|||
{ |
|||
public partial class BindingExpressionGrammarTests |
|||
{ |
|||
[Fact] |
|||
public void Should_Parse_Single_Property() |
|||
{ |
|||
var result = Parse("Foo"); |
|||
var node = Assert.Single(result); |
|||
|
|||
AssertIsProperty(node, "Foo"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Underscored_Property() |
|||
{ |
|||
var result = Parse("_Foo"); |
|||
var node = Assert.Single(result); |
|||
|
|||
AssertIsProperty(node, "_Foo"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Property_With_Digits() |
|||
{ |
|||
var result = Parse("F0o"); |
|||
var node = Assert.Single(result); |
|||
|
|||
AssertIsProperty(node, "F0o"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Dot() |
|||
{ |
|||
var result = Parse("."); |
|||
var node = Assert.Single(result); |
|||
|
|||
Assert.IsType<BindingExpressionGrammar.EmptyExpressionNode>(node); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Single_Attached_Property() |
|||
{ |
|||
var result = Parse("(Foo.Bar)"); |
|||
var node = Assert.Single(result); |
|||
|
|||
AssertIsAttachedProperty(node, "Foo", "Bar"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Property_Chain() |
|||
{ |
|||
var result = Parse("Foo.Bar.Baz"); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar"); |
|||
AssertIsProperty(result[2], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Property_Chain_With_Attached_Property_1() |
|||
{ |
|||
var result = Parse("(Foo.Bar).Baz"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsAttachedProperty(result[0], "Foo", "Bar"); |
|||
AssertIsProperty(result[1], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Property_Chain_With_Attached_Property_2() |
|||
{ |
|||
var result = Parse("Foo.(Bar.Baz)"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsAttachedProperty(result[1], "Bar", "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Property_Chain_With_Attached_Property_3() |
|||
{ |
|||
var result = Parse("Foo.(Bar.Baz).Last"); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsAttachedProperty(result[1], "Bar", "Baz"); |
|||
AssertIsProperty(result[2], "Last"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Null_Conditional_In_Property_Chain_1() |
|||
{ |
|||
var result = Parse("Foo?.Bar.Baz"); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar", acceptsNull: true); |
|||
AssertIsProperty(result[2], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Null_Conditional_In_Property_Chain_2() |
|||
{ |
|||
var result = Parse("Foo.Bar?.Baz"); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar"); |
|||
AssertIsProperty(result[2], "Baz", acceptsNull: true); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Null_Conditional_In_Property_Chain_3() |
|||
{ |
|||
var result = Parse("Foo?.(Bar.Baz)"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsAttachedProperty(result[1], "Bar", "Baz", acceptsNull: true); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Negated_Property_Chain() |
|||
{ |
|||
var result = Parse("!Foo.Bar.Baz"); |
|||
|
|||
Assert.Equal(4, result.Count); |
|||
Assert.IsType<BindingExpressionGrammar.NotNode>(result[0]); |
|||
AssertIsProperty(result[1], "Foo"); |
|||
AssertIsProperty(result[2], "Bar"); |
|||
AssertIsProperty(result[3], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Double_Negated_Property_Chain() |
|||
{ |
|||
var result = Parse("!!Foo.Bar.Baz"); |
|||
|
|||
Assert.Equal(5, result.Count); |
|||
Assert.IsType<BindingExpressionGrammar.NotNode>(result[0]); |
|||
Assert.IsType<BindingExpressionGrammar.NotNode>(result[1]); |
|||
AssertIsProperty(result[2], "Foo"); |
|||
AssertIsProperty(result[3], "Bar"); |
|||
AssertIsProperty(result[4], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Indexed_Property() |
|||
{ |
|||
var result = Parse("Foo[15]"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], "15"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Indexed_Property_StringIndex() |
|||
{ |
|||
var result = Parse("Foo[Key]"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], "Key"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Multiple_Indexed_Property() |
|||
{ |
|||
var result = Parse("Foo[15,6]"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], "15", "6"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Multiple_Indexed_Property_With_Space() |
|||
{ |
|||
var result = Parse("Foo[5, 16]"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], "5", "16"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Consecutive_Indexers() |
|||
{ |
|||
var result = Parse("Foo[15][16]"); |
|||
|
|||
Assert.Equal(3, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsIndexer(result[1], "15"); |
|||
AssertIsIndexer(result[2], "16"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Indexed_Property_In_Chain() |
|||
{ |
|||
var result = Parse("Foo.Bar[5, 6].Baz"); |
|||
|
|||
Assert.Equal(4, result.Count); |
|||
AssertIsProperty(result[0], "Foo"); |
|||
AssertIsProperty(result[1], "Bar"); |
|||
AssertIsIndexer(result[2], "5", "6"); |
|||
AssertIsProperty(result[3], "Baz"); |
|||
} |
|||
|
|||
[Fact] |
|||
public void Should_Parse_Stream_Node() |
|||
{ |
|||
var result = Parse("Foo^"); |
|||
|
|||
Assert.Equal(2, result.Count); |
|||
Assert.IsType<BindingExpressionGrammar.StreamNode>(result[1]); |
|||
} |
|||
|
|||
private static void AssertIsProperty( |
|||
BindingExpressionGrammar.INode node, |
|||
string name, |
|||
bool acceptsNull = false) |
|||
{ |
|||
var p = Assert.IsType<BindingExpressionGrammar.PropertyNameNode>(node); |
|||
Assert.Equal(name, p.PropertyName); |
|||
Assert.Equal(acceptsNull, p.AcceptsNull); |
|||
} |
|||
|
|||
private static void AssertIsAttachedProperty( |
|||
BindingExpressionGrammar.INode node, |
|||
string typeName, |
|||
string name, |
|||
bool acceptsNull = false) |
|||
{ |
|||
var p = Assert.IsType<BindingExpressionGrammar.AttachedPropertyNameNode>(node); |
|||
Assert.Equal(typeName, p.TypeName); |
|||
Assert.Equal(name, p.PropertyName); |
|||
Assert.Equal(acceptsNull, p.AcceptsNull); |
|||
} |
|||
|
|||
private static void AssertIsIndexer(BindingExpressionGrammar.INode node, params string[] args) |
|||
{ |
|||
var e = Assert.IsType<BindingExpressionGrammar.IndexerNode>(node); |
|||
Assert.Equal(e.Arguments, args); |
|||
} |
|||
|
|||
private static List<BindingExpressionGrammar.INode> Parse(string s) |
|||
{ |
|||
var r = new CharacterReader(s); |
|||
return BindingExpressionGrammar.Parse(ref r).Nodes; |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue