Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[XC] Allow generic types in x:DataType and x:Type #20625

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 10 additions & 5 deletions src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -419,12 +419,17 @@ static IEnumerable<Instruction> CompileBindingPath(ElementNode node, ILContext c
if (dataType is null)
throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null);

var prefix = dataType.Contains(":") ? dataType.Substring(0, dataType.IndexOf(":", StringComparison.Ordinal)) : "";
var namespaceuri = node.NamespaceResolver.LookupNamespace(prefix) ?? "";
if (!string.IsNullOrEmpty(prefix) && string.IsNullOrEmpty(namespaceuri))
XmlType dtXType = null;
try
{
dtXType = TypeArgumentsParser.ParseSingle(dataType, node.NamespaceResolver, dataTypeNode as IXmlLineInfo)
?? throw new BuildException(XDataTypeSyntax, dataTypeNode as IXmlLineInfo, null);
}
catch (XamlParseException)
{
var prefix = dataType.Contains(":") ? dataType.Substring(0, dataType.IndexOf(":", StringComparison.Ordinal)) : "";
throw new BuildException(XmlnsUndeclared, dataTypeNode as IXmlLineInfo, null, prefix);

var dtXType = new XmlType(namespaceuri, dataType, null);
}

var tSourceRef = dtXType.GetTypeReference(context.Cache, module, (IXmlLineInfo)node);
if (tSourceRef == null)
Expand Down
20 changes: 6 additions & 14 deletions src/Controls/src/Build.Tasks/XmlTypeExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,25 +50,17 @@ static IList<XmlnsDefinitionAttribute> GatherXmlnsDefinitionAttributes(ModuleDef
return xmlnsDefinitions;
}

public static TypeReference GetTypeReference(XamlCache cache, string xmlType, ModuleDefinition module, BaseNode node)
public static TypeReference GetTypeReference(XamlCache cache, string typeName, ModuleDefinition module, BaseNode node)
{
var split = xmlType.Split(':');
if (split.Length > 2)
throw new BuildException(BuildExceptionCode.InvalidXaml, node as IXmlLineInfo, null, xmlType);

string prefix, name;
if (split.Length == 2)
try
{
prefix = split[0];
name = split[1];
XmlType xmlType = TypeArgumentsParser.ParseSingle(typeName, node.NamespaceResolver, (IXmlLineInfo)node);
return GetTypeReference(xmlType, cache, module, node as IXmlLineInfo);
}
else
catch (XamlParseException)
{
prefix = "";
name = split[0];
throw new BuildException(BuildExceptionCode.InvalidXaml, node as IXmlLineInfo, null, typeName);
}
var namespaceuri = node.NamespaceResolver.LookupNamespace(prefix) ?? "";
return GetTypeReference(new XmlType(namespaceuri, name, null), cache, module, node as IXmlLineInfo);
}

public static TypeReference GetTypeReference(XamlCache cache, string namespaceURI, string typename, ModuleDefinition module, IXmlLineInfo xmlInfo)
Expand Down
12 changes: 12 additions & 0 deletions src/Controls/src/Xaml/TypeArgumentsParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ public static IList<XmlType> ParseExpression(string expression, IXmlNamespaceRes
return typeList;
}

public static XmlType ParseSingle(string expression, IXmlNamespaceResolver resolver, IXmlLineInfo lineInfo)
{
string remaining = null;
XmlType type = Parse(expression, ref remaining, resolver, lineInfo);
if (type is null || !string.IsNullOrWhiteSpace(remaining))
{
throw new XamlParseException($"Invalid type expression or more than one type declared in '{expression}'", lineInfo, null);
}

return type;
}

static XmlType Parse(string match, ref string remaining, IXmlNamespaceResolver resolver, IXmlLineInfo lineinfo)
{
remaining = null;
Expand Down
27 changes: 2 additions & 25 deletions src/Controls/src/Xaml/XamlServiceProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,38 +199,15 @@ internal bool TryResolve(XmlType xmlType, out Type type)

Type Resolve(string qualifiedTypeName, IServiceProvider serviceProvider, out XamlParseException exception)
{
exception = null;
var split = qualifiedTypeName.Split(':');
if (split.Length > 2)
return null;

string prefix, name;
if (split.Length == 2)
{
prefix = split[0];
name = split[1];
}
else
{
prefix = "";
name = split[0];
}

IXmlLineInfo xmlLineInfo = null;
if (serviceProvider != null)
{
if (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) is IXmlLineInfoProvider lineInfoProvider)
xmlLineInfo = lineInfoProvider.XmlLineInfo;
}

var namespaceuri = namespaceResolver.LookupNamespace(prefix);
if (namespaceuri == null)
{
exception = new XamlParseException($"No xmlns declaration for prefix \"{prefix}\"", xmlLineInfo);
return null;
}

return getTypeFromXmlName(new XmlType(namespaceuri, name, null), xmlLineInfo, currentAssembly, out exception);
var xmlType = TypeArgumentsParser.ParseSingle(qualifiedTypeName, namespaceResolver, xmlLineInfo);
return getTypeFromXmlName(xmlType, xmlLineInfo, currentAssembly, out exception);
}

internal delegate Type GetTypeFromXmlName(XmlType xmlType, IXmlLineInfo xmlInfo, Assembly currentAssembly, out XamlParseException exception);
Expand Down
13 changes: 13 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
xmlns:local="clr-namespace:Microsoft.Maui.Controls.Xaml.UnitTests"
x:Class="Microsoft.Maui.Controls.Xaml.UnitTests.Maui20616">
<ContentPage.Resources>
<x:Type x:Key="ViewModelBool" TypeName="local:ViewModel20616(x:Boolean)" />
<x:Type x:Key="NestedViewModel" TypeName="local:ViewModel20616(local:ViewModel20616(x:String))" />
</ContentPage.Resources>

<Label Text="{Binding Value}" x:DataType="local:ViewModel20616(x:String)" x:Name="LabelA" />
<Label Text="{Binding Value.Value}" x:DataType="{x:Type local:ViewModel20616(local:ViewModel20616(x:Boolean))}" x:Name="LabelB" />
</ContentPage>
76 changes: 76 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using Microsoft.Maui.ApplicationModel;
using Microsoft.Maui.Controls.Core.UnitTests;
using Microsoft.Maui.Controls.Shapes;
using Microsoft.Maui.Controls.Internals;
using Microsoft.Maui.Devices;
using Microsoft.Maui.Dispatching;

using Microsoft.Maui.Graphics;
using Microsoft.Maui.UnitTests;
using NUnit.Framework;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Maui20616
{
public Maui20616()
{
InitializeComponent();
BindingContext = new ViewModel20616<string> { Value = "Foo" };
}

public Maui20616(bool useCompiledXaml)
{
//this stub will be replaced at compile time
}

[TestFixture]
class Test
{
[SetUp]
public void Setup()
{
Application.SetCurrentApplication(new MockApplication());
DispatcherProvider.SetCurrent(new DispatcherProviderStub());
}

[TearDown] public void TearDown() => AppInfo.SetCurrent(null);

[Test]
public void XDataTypeCanBeGeneric([Values(false, true)] bool useCompiledXaml)
{
var page = new Maui20616(useCompiledXaml);

page.LabelA.BindingContext = new ViewModel20616<string> { Value = "ABC" };
Assert.AreEqual("ABC", page.LabelA.Text);

if (useCompiledXaml)
{
var binding = page.LabelA.GetContext(Label.TextProperty).Bindings.Values.Single();
Assert.That(binding, Is.TypeOf<TypedBinding<ViewModel20616<string>, string>>());
}

page.LabelB.BindingContext = new ViewModel20616<ViewModel20616<bool>> { Value = new ViewModel20616<bool> { Value = true } };
Assert.AreEqual("True", page.LabelB.Text);

if (useCompiledXaml)
{
var binding = page.LabelB.GetContext(Label.TextProperty).Bindings.Values.Single();
Assert.That(binding, Is.TypeOf<TypedBinding<ViewModel20616<ViewModel20616<bool>>, bool>>());
}
simonrozsival marked this conversation as resolved.
Show resolved Hide resolved

Assert.AreEqual(typeof(ViewModel20616<bool>), page.Resources["ViewModelBool"]);
Assert.AreEqual(typeof(ViewModel20616<ViewModel20616<string>>), page.Resources["NestedViewModel"]);
}
}
}

public class ViewModel20616<T>
{
public required T Value { get; init; }
}
Loading