diff --git a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs index 26e183912c10..722ad0f5903b 100644 --- a/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs +++ b/src/Controls/src/Build.Tasks/SetPropertiesVisitor.cs @@ -419,12 +419,17 @@ static IEnumerable 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) diff --git a/src/Controls/src/Build.Tasks/XmlTypeExtensions.cs b/src/Controls/src/Build.Tasks/XmlTypeExtensions.cs index 57dfee10f62c..de34074a2742 100644 --- a/src/Controls/src/Build.Tasks/XmlTypeExtensions.cs +++ b/src/Controls/src/Build.Tasks/XmlTypeExtensions.cs @@ -50,25 +50,17 @@ static IList 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) diff --git a/src/Controls/src/Xaml/TypeArgumentsParser.cs b/src/Controls/src/Xaml/TypeArgumentsParser.cs index 7e730ca6f33d..29859504fe2c 100644 --- a/src/Controls/src/Xaml/TypeArgumentsParser.cs +++ b/src/Controls/src/Xaml/TypeArgumentsParser.cs @@ -18,6 +18,18 @@ public static IList 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; diff --git a/src/Controls/src/Xaml/XamlServiceProvider.cs b/src/Controls/src/Xaml/XamlServiceProvider.cs index 23bf38738b32..aa2e0cb42629 100644 --- a/src/Controls/src/Xaml/XamlServiceProvider.cs +++ b/src/Controls/src/Xaml/XamlServiceProvider.cs @@ -199,23 +199,6 @@ 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) { @@ -223,14 +206,8 @@ Type Resolve(string qualifiedTypeName, IServiceProvider serviceProvider, out Xam 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); diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml b/src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml new file mode 100644 index 000000000000..117d7b2c5d60 --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml @@ -0,0 +1,13 @@ + + + + + + + + \ No newline at end of file diff --git a/src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml.cs b/src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml.cs new file mode 100644 index 000000000000..383b69c8e09b --- /dev/null +++ b/src/Controls/tests/Xaml.UnitTests/Issues/Maui20616.xaml.cs @@ -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 { 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 { 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, string>>()); + } + + page.LabelB.BindingContext = new ViewModel20616> { Value = new ViewModel20616 { 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>, bool>>()); + } + + Assert.AreEqual(typeof(ViewModel20616), page.Resources["ViewModelBool"]); + Assert.AreEqual(typeof(ViewModel20616>), page.Resources["NestedViewModel"]); + } + } +} + +public class ViewModel20616 +{ + public required T Value { get; init; } +} \ No newline at end of file