Skip to content

Commit

Permalink
[XC] Allow generic types in x:DataType and x:Type (#20625)
Browse files Browse the repository at this point in the history
* Add parser for single type expression

* Allow generic types in x:DataType

* Allow generic types in x:Type

* Add test

* Improve test
  • Loading branch information
simonrozsival authored and rmarinho committed Feb 22, 2024
1 parent 6307828 commit 7c5568f
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 44 deletions.
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 @@ -435,12 +435,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 @@ -217,38 +217,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>>());
}

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

0 comments on commit 7c5568f

Please sign in to comment.