Skip to content

Commit

Permalink
Merge pull request #19239 from unoplatform/dev/xygu/20250114/contentc…
Browse files Browse the repository at this point in the history
…ontrol-binding-cs1029

fix(codegen): ContentPresenter.Content binding compilation error
  • Loading branch information
Xiaoy312 authored Jan 20, 2025
2 parents 4c8b326 + f2adbf0 commit 7e20f1f
Show file tree
Hide file tree
Showing 10 changed files with 564 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ private bool IsUnoFluentAssembly

internal Lazy<INamedTypeSymbol?> AssemblyMetadataSymbol { get; }
internal Lazy<INamedTypeSymbol> ElementStubSymbol { get; }
internal Lazy<INamedTypeSymbol> ContentControlSymbol { get; }
internal Lazy<INamedTypeSymbol> ContentPresenterSymbol { get; }
internal Lazy<INamedTypeSymbol> StringSymbol { get; }
internal Lazy<INamedTypeSymbol> ObjectSymbol { get; }
Expand Down Expand Up @@ -267,6 +268,7 @@ public XamlCodeGeneration(GeneratorExecutionContext context)
AssemblyMetadataSymbol = GetOptionalSymbolAsLazy("System.Reflection.AssemblyMetadataAttribute");
ElementStubSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.ElementStub);
SetterSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.Setter);
ContentControlSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.ContentControl);
ContentPresenterSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.ContentPresenter);
FrameworkElementSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.FrameworkElement);
UIElementSymbol = GetMandatorySymbolAsLazy(XamlConstants.Types.UIElement);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,27 @@ internal static class XamlConstants

public const int MaxFluentResourcesVersion = 2;

public const string UnknownContent = "_UnknownContent";
public const string PositionalParameters = "_PositionalParameters";

public static class Xmlnses
{
/// <summary>
/// <code>xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"</code>
/// </summary>
public const string Default = PresentationXamlXmlNamespace;

/// <summary>
/// <code>xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"</code>
/// </summary>
public const string X = XamlXmlNamespace;

/// <summary>
/// <code>xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"</code>
/// </summary>
public const string MC = "http://schemas.openxmlformats.org/markup-compatibility/2006";
}

public static class Namespaces
{
public const string Base = BaseXamlNamespace;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -548,11 +548,16 @@ private static bool HasInitializer(XamlObjectDefinition objectDefinition)
return null;
}

private INamedTypeSymbol GetType(string name, XamlObjectDefinition? objectDefinition = null)
private INamedTypeSymbol? ResolveType(string? name, XamlObjectDefinition? xmlnsContextProvider = null)
{
while (objectDefinition is not null)
if (name == null)
{
var namespaces = objectDefinition.Namespaces;
return null;
}

while (xmlnsContextProvider is not null)
{
var namespaces = xmlnsContextProvider.Namespaces;
if (namespaces is { Count: > 0 } && name.IndexOf(':') is int indexOfColon && indexOfColon > 0)
{
var ns = name.AsSpan().Slice(0, indexOfColon);
Expand All @@ -570,18 +575,14 @@ private INamedTypeSymbol GetType(string name, XamlObjectDefinition? objectDefini
}
}

objectDefinition = objectDefinition.Owner;
}

var type = _findType!(name);

if (type == null)
{
throw new InvalidOperationException("The type {0} could not be found".InvariantCultureFormat(name));
xmlnsContextProvider = xmlnsContextProvider.Owner;
}

return type;
return _findType!(name);
}
private INamedTypeSymbol GetType(string name, XamlObjectDefinition? objectDefinition = null) =>
ResolveType(name, objectDefinition) ??
throw new InvalidOperationException("The type {0} could not be found".InvariantCultureFormat(name));

private INamedTypeSymbol GetType(XamlType type)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3850,31 +3850,67 @@ private void BuildStatementLocalizedProperties(IIndentedStringBuilder writer, Xa
private void TryValidateContentPresenterBinding(IIndentedStringBuilder writer, XamlObjectDefinition objectDefinition, XamlMemberDefinition member)
{
TryAnnotateWithGeneratorSource(writer);
if (
SymbolEqualityComparer.Default.Equals(FindType(objectDefinition.Type), Generation.ContentPresenterSymbol.Value)
&& member.Member.Name == "Content"
)

// detect content binding in: <ContentPresenter Content="{Binding}" />
if (FindType(objectDefinition.Type)?.Is(Generation.ContentPresenterSymbol.Value) == true &&
member.Member.Name == "Content" &&
member.Objects.FirstOrDefault(x => x.Type.Name == "Binding") is { } binding)
{
var binding = member.Objects.FirstOrDefault(o => o.Type.Name == "Binding");
// In Uno, ContentPresenter.Content overrides the local value of ContentPresenter.DataContext,
// which in turn breaks the binding source for the Content binding. However, there are certain exceptions
// where it would be fine.

if (binding != null)
// It can either be TemplatedParent or Self. In either cases, it does not use the inherited
// DataContext, which falls outside of the scenario we want to avoid.
if (binding.Members.Any(x => x.Member.Name == "RelativeSource"))
{
var hasRelativeSource = binding.Members
.Any(m =>
m.Member.Name == "RelativeSource"
// It can either be TemplatedParent or Self. In either cases, it does not use the inherited
// DataContext, which falls outside of the scenario we want to avoid.
);
return;
}

if (!hasRelativeSource)
{
writer.AppendLine();
writer.AppendIndented("#error Using a non-template binding expression on Content " +
"will likely result in an undefined runtime behavior, as ContentPresenter.Content overrides " +
"the local value of ContentPresenter.DataContent. " +
"Use ContentControl instead if you really need a normal binding on Content.\n");
}
// {Binding} is fine, because it doesnt alter the data-context.
if (binding.Members.Count == 0)
{
return;
}

// {Binding .} or {Binding Path=.} are fine too, because it doesnt alter the data-context.
if ((FindImplicitContentMember(binding, XamlConstants.PositionalParameters) ?? FindImplicitContentMember(binding, "Path")) is { } path &&
path.Value?.ToString() == ".")
{
return;
}

// Not inside any template.
var template = FindAncestor(objectDefinition, x => FrameworkTemplateTypes.Contains(x.Type.Name));
if (template == null)
{
return;
}

// If it is within a <ControlTemplate>, regardless if it descends from ContentControl or not.
if (template?.Type.Name is "ControlTemplate")
{
return;
}

// note: <DataTemplate> with ContentPresenter.Content bound to arbitrary path should work.
// but is failing to resolve somehow. We will not allow this case to compile:
// <ContentControl Content="new TestData { Text='lalala' }">
// <ContentControl.ContentTemplate>
// <DataTemplate>
// <ContentPresenter Content="{Binding Text}" />
// ContentControl // DC=TestData, Content=TestData, Content.Binding=[Path=, RelativeSource=]
// ContentPresenter // DC=TestData, Content=TestData, Content.Binding=[Path=Content, RelativeSource=TemplatedParent]
// ContentPresenter // DC=TestData, Content=null, Content.Binding=[Path=Text, RelativeSource=]
// ^ the binding didnt produces the expected value
// ImplicitTextBlock // DC=TestData, Text=''

writer.AppendLine();
writer.AppendIndented(
"#error Using a non-template binding expression on Content " +
"will likely result in an undefined runtime behavior, as ContentPresenter.Content overrides " +
"the local value of ContentPresenter.DataContent. " +
"Use ContentControl instead if you really need a normal binding on Content.\n");
}
}

Expand Down Expand Up @@ -5101,6 +5137,21 @@ string RewriteUri(string? rawValue)
return null;
}

private XamlObjectDefinition? FindAncestor(XamlObjectDefinition? objectDefinition, Func<XamlObjectDefinition, bool> predicate)
{
while (objectDefinition is not null)
{
if (predicate(objectDefinition))
{
return objectDefinition;
}

objectDefinition = objectDefinition.Owner;
}

return null;
}

private string? BuildLocalizedResourceValue(INamedTypeSymbol? owner, string memberName, string objectUid)
{
//windows 10 localization concat the xUid Value with the member value (Text, Content, Header etc...)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<Page x:Class="XamlGenerationTests.ContentPresenter_ContentBinding"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:XamlGenerationTests"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:void="There is no mistake so great that it cannot be undone."
mc:Ignorable="d void">
<Page.Resources>

<!-- OK: should work; unaffected in uno -->
<DataTemplate x:Key="DTCPCB_DataTemplate">
<ContentPresenter Content="{Binding}" />
</DataTemplate>

<!-- OK: should work; unaffected in uno -->
<ControlTemplate x:Key="DTCPCB_UntypedCtrlTemplate_BindingBlank">
<ContentPresenter Content="{Binding}" />
</ControlTemplate>
<ControlTemplate x:Key="DTCPCB_UntypedCtrlTemplate_BindingDot">
<ContentPresenter Content="{Binding .}" />
</ControlTemplate>
<ControlTemplate x:Key="DTCPCB_ContentCtrlTemplate" TargetType="ContentControl">
<ContentPresenter Content="{Binding}" />
</ControlTemplate>
<ControlTemplate x:Key="DTCPCB_DerivedContentCtrlTemplate" TargetType="Button">
<ContentPresenter Content="{Binding}" />
</ControlTemplate>
<ControlTemplate x:Key="DTCPCB_NonContentCtrlTemplate" TargetType="TextBox">
<ContentPresenter Content="{Binding}" />
</ControlTemplate>

<!-- OK: ControlTemplate\ContentPresenter.Content with Path works -->
<ControlTemplate x:Key="DTCPCB_DerivedContentCtrlTemplate_SomePath_1" TargetType="Button">
<ContentPresenter Content="{Binding SomePath}" />
</ControlTemplate>
<ControlTemplate x:Key="DTCPCB_DerivedContentCtrlTemplate_SomePath_2" TargetType="Button">
<ContentPresenter Content="{Binding Mode=OneTime, Path=SomePath}" />
</ControlTemplate>

<!-- FAIL: DataTemplate\ContentPresenter.Content with Path should work, but doesn't -->
<!-- for now, it should not compile-->
<void:DataTemplate x:Key="DTCPCB_ContentDataTemplate_SomePath">
<ContentPresenter Content="{Binding SomePath}" />
</void:DataTemplate>

</Page.Resources>

<StackPanel>
<!-- OK: should work; unaffected in uno -->
<ContentPresenter Content="{Binding}" />
<ContentPresenter Content="{Binding SomePath}" />
</StackPanel>

</Page>
64 changes: 64 additions & 0 deletions src/Uno.UI.RuntimeTests/Extensions/StringExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace Uno.UI.RuntimeTests.Extensions;

public static class StringExtensions
{
/// <summary>
/// Split a string by a separator, while ignoring certain regex pattern to be splitted.
/// </summary>
/// <param name="input"></param>
/// <param name="separator"></param>
/// <param name="ignoredPattern"></param>
/// <param name="options"></param>
/// <remarks>This is typically used to split separator-seperator string that contains brackets.</remarks>
/// <returns></returns>
public static string[] SplitWithIgnore(this string input, char separator, string ignoredPattern, StringSplitOptions options = StringSplitOptions.None)
{
var ignores = Regex.Matches(input, ignoredPattern);

var shards = new List<string>();
for (int i = 0; i < input.Length; i++)
{
var nextSeparator = input.IndexOf(separator, i);

// find the next separator, if we are within the ignored pattern
while (nextSeparator != -1 && ignores.FirstOrDefault(x => InRange(x, nextSeparator)) is { } enclosingIgnore)
{
nextSeparator = enclosingIgnore.Index + enclosingIgnore.Length is { } afterIgnore && afterIgnore < input.Length
? input.IndexOf(separator, afterIgnore)
: -1;
}

if (nextSeparator != -1)
{
shards.Add(input.Substring(i, nextSeparator - i));
i = nextSeparator;

// skip multiple continuous spaces
while (options.HasFlag(StringSplitOptions.RemoveEmptyEntries) && i + 1 < input.Length && input[i + 1] == separator) i++;
}
else
{
shards.Add(input.Substring(i));
break;
}
}

if (options.HasFlag(StringSplitOptions.TrimEntries))
{
return shards.Select(x => x.Trim()).ToArray();
}
else
{
return shards.ToArray();
}

bool InRange(Match x, int index) => x.Index <= index && index < (x.Index + x.Length);
}
}
Loading

0 comments on commit 7e20f1f

Please sign in to comment.