Browse Source
Allow casting to a nested type in a binding path (#22173 )
The binding path parser built type names with ParseIdentifier, which doesn't
accept '+', so a cast like ((local:Outer+Nested)DataContext) stopped parsing at
the '+' and then failed expecting a ')'. Parse type names with a variant that
accepts '+'. Ordinary identifiers are unchanged.
pull/22201/head
Evan
2 weeks ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with
98 additions and
2 deletions
src/Avalonia.Base/Data/Core/Parsers/BindingExpressionGrammar.cs
src/Avalonia.Base/Utilities/IdentifierParser.cs
tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionGrammarTests.cs
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
@ -377,12 +377,12 @@ namespace Avalonia.Data.Core.Parsers
{
ReadOnlySpan < char > ns , typeName ;
ns = ReadOnlySpan < char > . Empty ;
var typeNameOrNamespace = r . ParseIdentifier ( ) ;
var typeNameOrNamespace = r . ParseType Identifier ( ) ;
if ( ! r . End & & r . TakeIf ( ':' ) )
{
ns = typeNameOrNamespace ;
typeName = r . ParseIdentifier ( ) ;
typeName = r . ParseType Identifier ( ) ;
}
else
{
@ -21,6 +21,25 @@ namespace Avalonia.Utilities
}
}
/// <summary>
/// Parses an identifier that may name a nested type, e.g. "Outer+Inner".
/// </summary>
public static ReadOnlySpan < char > ParseTypeIdentifier ( this
#if NET7SDK
scoped
#endif
ref CharacterReader r )
{
if ( IsValidIdentifierStart ( r . Peek ) )
{
return r . TakeWhile ( c = > IsValidIdentifierChar ( c ) | | c = = '+' ) ;
}
else
{
return ReadOnlySpan < char > . Empty ;
}
}
private static bool IsValidIdentifierStart ( char c )
{
return char . IsLetter ( c ) | | c = = '_' ;
@ -244,6 +244,26 @@ namespace Avalonia.Base.UnitTests.Data.Core.Parsers
Assert . IsType < BindingExpressionGrammar . StreamNode > ( result [ 1 ] ) ;
}
[Fact]
public void Should_Parse_Cast_To_Nested_Type ( )
{
var result = Parse ( "((ns:Outer+Inner)Foo).Bar" ) ;
var cast = Assert . IsType < BindingExpressionGrammar . TypeCastNode > ( result [ 1 ] ) ;
Assert . Equal ( "ns" , cast . Namespace ) ;
Assert . Equal ( "Outer+Inner" , cast . TypeName ) ;
}
[Fact]
public void Should_Parse_Cast_To_Non_Nested_Type ( )
{
var result = Parse ( "((ns:Outer)Foo).Bar" ) ;
var cast = Assert . IsType < BindingExpressionGrammar . TypeCastNode > ( result [ 1 ] ) ;
Assert . Equal ( "ns" , cast . Namespace ) ;
Assert . Equal ( "Outer" , cast . TypeName ) ;
}
private static void AssertIsProperty (
BindingExpressionGrammar . INode node ,
string name ,
@ -11,6 +11,27 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
{
public class BindingExtensionTests : XamlTestBase
{
[Fact]
public void SupportCastToNestedTypeInExpression ( )
{
using ( UnitTestApplication . Start ( TestServices . StyledWindow ) )
{
var xaml = @ "
< Window xmlns = ' https : //github.com/avaloniaui'
xmlns : x = ' http : //schemas.microsoft.com/winfx/2006/xaml'
xmlns : local = ' clr - namespace : Avalonia . Markup . Xaml . UnitTests . MarkupExtensions ; assembly = Avalonia . Markup . Xaml . UnitTests '
>
< ContentControl Content = ' { Binding $ parent . ( ( local : ReflectionOuter + Nested ) DataContext ) . NestedProperty } ' Name = ' contentControl ' / >
< / Window > ";
var window = ( Window ) AvaloniaRuntimeXamlLoader . Load ( xaml ) ;
var contentControl = window . GetControl < ContentControl > ( "contentControl" ) ;
window . DataContext = new ReflectionOuter . Nested { NestedProperty = "hello" } ;
Assert . Equal ( "hello" , contentControl . Content ) ;
}
}
[Fact]
public void BindingExtension_Binds_To_Source ( )
{
@ -167,4 +188,12 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} ;
}
}
public class ReflectionOuter
{
public class Nested
{
public string NestedProperty { get ; set ; } = "nested" ;
}
}
}
@ -1456,6 +1456,26 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
[Fact]
public void SupportCastToNestedTypeInExpression ( )
{
using ( UnitTestApplication . Start ( TestServices . StyledWindow ) )
{
var xaml = @ "
< Window xmlns = ' https : //github.com/avaloniaui'
xmlns : x = ' http : //schemas.microsoft.com/winfx/2006/xaml'
xmlns : local = ' using : Avalonia . Markup . Xaml . UnitTests . MarkupExtensions ' >
< ContentControl Content = ' { CompiledBinding $ parent . ( ( local : OuterClass + NestedClass ) DataContext ) . NestedProperty } ' Name = ' contentControl ' / >
< / Window > ";
var window = ( Window ) AvaloniaRuntimeXamlLoader . Load ( xaml ) ;
var contentControl = window . GetControl < ContentControl > ( "contentControl" ) ;
window . DataContext = new OuterClass . NestedClass { NestedProperty = "hello" } ;
Assert . Equal ( "hello" , contentControl . Content ) ;
}
}
[Fact]
public void SupportCastToTypeInExpression_DifferentTypeEvaluatesToNull ( )
{
@ -2825,6 +2845,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public string? StringProperty { get ; set ; }
}
public class OuterClass
{
public class NestedClass
{
public string NestedProperty { get ; set ; } = "nested value" ;
}
}
public class TestDataContextBaseClass { }
public class TestItemsCollectionDataContext : TestDataContextBaseClass