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
parent
commit
8057428233
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      src/Avalonia.Base/Data/Core/Parsers/BindingExpressionGrammar.cs
  2. 19
      src/Avalonia.Base/Utilities/IdentifierParser.cs
  3. 20
      tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionGrammarTests.cs
  4. 29
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs
  5. 28
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

4
src/Avalonia.Base/Data/Core/Parsers/BindingExpressionGrammar.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.ParseTypeIdentifier();
if (!r.End && r.TakeIf(':'))
{
ns = typeNameOrNamespace;
typeName = r.ParseIdentifier();
typeName = r.ParseTypeIdentifier();
}
else
{

19
src/Avalonia.Base/Utilities/IdentifierParser.cs

@ -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 == '_';

20
tests/Avalonia.Base.UnitTests/Data/Core/Parsers/BindingExpressionGrammarTests.cs

@ -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,

29
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/BindingExtensionTests.cs

@ -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";
}
}
}

28
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -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

Loading…
Cancel
Save