Skip to content
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
70 changes: 64 additions & 6 deletions src/Controls/src/SourceGen/Visitors/CreateValuesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public void Visit(ElementNode node, INode parentNode)
//x:Array
if (type.Equals(Context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Controls.Xaml.ArrayExtension"), SymbolEqualityComparer.Default))
{
//we might want to mive this to a separate method
//we might want to move this to a separate method
Comment thread
StephaneDelcroix marked this conversation as resolved.
var visitor = new SetPropertiesVisitor(Context);
// var children = node.Properties.Values.ToList();
// children.AddRange(node.CollectionItems);
Expand Down Expand Up @@ -150,7 +150,6 @@ public void Visit(ElementNode node, INode parentNode)

if (ctor is null && factoryMethod is null)
{

//TODO we might need an extension method for that and cache the result. it happens eveytime we have a Style
ctor = type.InstanceConstructors.FirstOrDefault(c
=> c.Parameters.Length > 0
Expand Down Expand Up @@ -189,9 +188,59 @@ public void Visit(ElementNode node, INode parentNode)

bool isColor = type.Equals(Context.Compilation.GetTypeByMetadataName("Microsoft.Maui.Graphics.Color"), SymbolEqualityComparer.Default);

//are there any `required` properties ?
var requiredPropAndFields = type.GetMembers()
.Where(p => p is IPropertySymbol prop && prop.IsRequired || p is IFieldSymbol field && field.IsRequired)
.ToList();
List<(string name, ISymbol propOrField, ITypeSymbol propType, string propValue)> requiredPropertiesAndFields = [];
if (requiredPropAndFields.Count > 0)
{
var contentPropertyName = type.GetContentPropertyName(Context);
foreach (var req in requiredPropAndFields)
{
XmlName propXmlName ;
INode propNode;
if (req.Name == contentPropertyName && node.CollectionItems.Count == 1)
{
propNode = node.CollectionItems[0];
propXmlName = new XmlName("", contentPropertyName);
}
else if (!node.Properties.TryGetValue(req.Name, out propNode, out propXmlName))
Context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, LocationCreate(Context.FilePath!, node, req.Name), $"Required field '{req.Name}' not found in {type}"));
Comment on lines +208 to +209

Copilot AI Jul 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The diagnostic error is reported but execution continues, potentially causing null reference exceptions when propNode is used later. Consider adding a continue statement or early return after reporting the diagnostic.

Suggested change
else if (!node.Properties.TryGetValue(req.Name, out propNode, out propXmlName))
Context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, LocationCreate(Context.FilePath!, node, req.Name), $"Required field '{req.Name}' not found in {type}"));
else if (!node.Properties.TryGetValue(req.Name, out propNode, out propXmlName))
{
Context.ReportDiagnostic(Diagnostic.Create(Descriptors.XamlParserError, LocationCreate(Context.FilePath!, node, req.Name), $"Required field '{req.Name}' not found in {type}"));
continue;
}

Copilot uses AI. Check for mistakes.

string propValue = "null";
var pType = req is IPropertySymbol prop ? prop.Type : ((IFieldSymbol)req).Type;
var pConverter = req.GetAttributes().FirstOrDefault(a => a.AttributeClass!.Equals(Context.Compilation.GetTypeByMetadataName("System.ComponentModel.TypeConverterAttribute")!, SymbolEqualityComparer.Default))?.ConstructorArguments[0].Value as ITypeSymbol;

var visitor = new SetPropertiesVisitor(Context);
var children = node.Properties.Values.ToList();
children.AddRange(node.CollectionItems);
foreach (var cn in children)
{
if (cn is not ElementNode en)
continue;
foreach (var n in en.Properties.Values.ToList())
n.Accept(visitor, cn);
foreach (var n in en.CollectionItems)
n.Accept(visitor, cn);
}
Comment on lines +215 to +226

Copilot AI Jul 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A new SetPropertiesVisitor is created inside the loop for each required property, but the visitor traversal logic appears to be the same for all properties. Consider moving the visitor creation and traversal outside the loop to avoid redundant work.

Suggested change
var visitor = new SetPropertiesVisitor(Context);
var children = node.Properties.Values.ToList();
children.AddRange(node.CollectionItems);
foreach (var cn in children)
{
if (cn is not ElementNode en)
continue;
foreach (var n in en.Properties.Values.ToList())
n.Accept(visitor, cn);
foreach (var n in en.CollectionItems)
n.Accept(visitor, cn);
}

Copilot uses AI. Check for mistakes.
if (propNode is ValueNode vn)
propValue = vn.ConvertTo(pType, pConverter, Context);
else if (propNode is ElementNode en)
{
en.TryProvideValue(Context);
propValue = Context.Variables[en].Name;
}

requiredPropertiesAndFields.Add((req.Name, req, pType, propValue));
if (!node.SkipProperties.Contains(new XmlName("", req.Name)))
node.SkipProperties.Add(new XmlName("", req.Name));
}
}

if (node.CollectionItems.Count == 1
&& node.CollectionItems[0] is ValueNode valueNode
&& (isColor || type.IsValueType))
&& node.CollectionItems[0] is ValueNode valueNode
&& (isColor || type.IsValueType))
{ //<Color>HotPink</Color>
var variableName = NamingHelpers.CreateUniqueVariableName(Context, type);
var valueString = valueNode.ConvertTo(type, Context);
Expand All @@ -202,7 +251,6 @@ public void Visit(ElementNode node, INode parentNode)
return;
}
//TODO we could also check if there's a TypeConverter, but we have no test (so no need?) for it

else if (node.CollectionItems.Count == 1
&& node.CollectionItems[0] is ValueNode valueNode1
&& implicitOperator != null)
Expand All @@ -226,7 +274,17 @@ public void Visit(ElementNode node, INode parentNode)
{
var variableName = NamingHelpers.CreateUniqueVariableName(Context, type);

Writer.WriteLine($"var {variableName} = new {type.ToFQDisplayString()}({string.Join(", ", parameters?.ToMethodParameters(Context) ?? [])});");
if (requiredPropAndFields.Any())
{
Writer.WriteLine($"var {variableName} = new {type.ToFQDisplayString()}({string.Join(", ", parameters?.ToMethodParameters(Context) ?? [])})");
using (PrePost.NewBlock(Writer, begin: "{", end: "};"))
{
foreach (var (name, propOrField, propType, propValue) in requiredPropertiesAndFields)
Writer.WriteLine($"{name} = ({propType.ToFQDisplayString()}){propValue},");
}
}

Copilot AI Jul 30, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The indentation and spacing around the closing brace appears inconsistent with the surrounding code. The else statement on line 286 should be properly aligned.

Suggested change
}
}

Copilot uses AI. Check for mistakes.
else
Writer.WriteLine($"var {variableName} = new {type.ToFQDisplayString()}({string.Join(", ", parameters?.ToMethodParameters(Context) ?? [])});");
Context.Variables[node] = new LocalVariable(type, variableName);
node.RegisterSourceInfo(Context, Writer);
return;
Expand Down
23 changes: 23 additions & 0 deletions src/Controls/src/Xaml/XamlNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,29 @@ public override void Accept(IXamlNodeVisitor visitor, INode parentNode)
};
}

static class XmlNameExtensions
{
public static bool TryGetValue(this Dictionary<XmlName, INode> properties, string name, out INode node, out XmlName xmlName)
{
xmlName = new XmlName("", name);
if (properties.TryGetValue(xmlName, out node))
return true;

#if NETSTANDARD2_0
var kvp = properties.FirstOrDefault(kvp => kvp.Key.LocalName == name);
if (kvp.Key != null)
{
xmlName = kvp.Key;
node = kvp.Value;
}
#else
(xmlName, node) = properties.FirstOrDefault(kvp => kvp.Key.LocalName == name);
#endif
return node != null;
}

public static bool TryGetValue(this Dictionary<XmlName, INode> properties, string name, out INode node) => properties.TryGetValue(name, out node, out _);
}

[DebuggerDisplay("{XmlType.Name}")]
class ElementNode : BaseNode, IValueNode, IElementNode
Expand Down
22 changes: 22 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Required.xaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?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.Required"
Title="Required">
<ContentPage.Resources>
<DataTemplate x:Key="template1"><Label/></DataTemplate>
</ContentPage.Resources>
<local:Required.Selector>
<local:RequiredRandomSelector Template1="{StaticResource template1}">
<local:RequiredRandomSelector.Template2>
<DataTemplate>
<Label Text="Template 2"/>
</DataTemplate>
</local:RequiredRandomSelector.Template2>
</local:RequiredRandomSelector>
</local:Required.Selector>
<local:Required.Person>
<local:RequiredPerson Name="John Doe" />
</local:Required.Person>
</ContentPage>
83 changes: 83 additions & 0 deletions src/Controls/tests/Xaml.UnitTests/Required.xaml.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using System;
using NUnit.Framework;

using static Microsoft.Maui.Controls.Xaml.UnitTests.MockSourceGenerator;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

public partial class Required : ContentPage
{
public RequiredRandomSelector Selector { get; set; }
public RequiredPerson Person { get; set; }
public Required() => InitializeComponent();

class Tests
{
[Test]
public void RequiredFieldsAndPropertiesAreSet([Values] XamlInflator inflator)
{
if (inflator == XamlInflator.SourceGen)
{
var result = CreateMauiCompilation()
.WithAdditionalSource(
"""
using System;
using NUnit.Framework;

namespace Microsoft.Maui.Controls.Xaml.UnitTests;

[XamlProcessing(XamlInflator.SourceGen)]
public partial class Required : ContentPage
{
public RequiredRandomSelector Selector { get; set; }
public RequiredPerson Person { get; set; }
public Required() => InitializeComponent();
}

public class RequiredRandomSelector : DataTemplateSelector
{
public /*required*/ Controls.DataTemplate Template1 { get; set; }
public required Controls.DataTemplate Template2 { get; set; }

protected override Controls.DataTemplate OnSelectTemplate(object item, BindableObject container) => new Random().Next(2) == 0 ? Template1 : Template2;
}

public class RequiredPerson
{
public required string Name { get; set; }

public override string ToString()
=> Name;
}
""")
Comment on lines +22 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be aligned to the right and it will behave the same way as now:

Suggested change
.WithAdditionalSource(
"""
using System;
using NUnit.Framework;
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
[XamlProcessing(XamlInflator.SourceGen)]
public partial class Required : ContentPage
{
public RequiredRandomSelector Selector { get; set; }
public RequiredPerson Person { get; set; }
public Required() => InitializeComponent();
}
public class RequiredRandomSelector : DataTemplateSelector
{
public /*required*/ Controls.DataTemplate Template1 { get; set; }
public required Controls.DataTemplate Template2 { get; set; }
protected override Controls.DataTemplate OnSelectTemplate(object item, BindableObject container) => new Random().Next(2) == 0 ? Template1 : Template2;
}
public class RequiredPerson
{
public required string Name { get; set; }
public override string ToString()
=> Name;
}
""")
.WithAdditionalSource(
"""
using System;
using NUnit.Framework;
namespace Microsoft.Maui.Controls.Xaml.UnitTests;
[XamlProcessing(XamlInflator.SourceGen)]
public partial class Required : ContentPage
{
public RequiredRandomSelector Selector { get; set; }
public RequiredPerson Person { get; set; }
public Required() => InitializeComponent();
}
public class RequiredRandomSelector : DataTemplateSelector
{
public /*required*/ Controls.DataTemplate Template1 { get; set; }
public required Controls.DataTemplate Template2 { get; set; }
protected override Controls.DataTemplate OnSelectTemplate(object item, BindableObject container) => new Random().Next(2) == 0 ? Template1 : Template2;
}
public class RequiredPerson
{
public required string Name { get; set; }
public override string ToString()
=> Name;
}
""")

.RunMauiSourceGenerator(typeof(Required));

Assert.That(result.Diagnostics, Is.Empty, "No diagnostics expected");
}

var layout = new Required(inflator);
Assert.IsNotNull(layout.Selector);
Assert.IsNotNull(layout.Selector.Template1);
Assert.IsNotNull(layout.Selector.Template2);
Assert.IsNotNull(layout.Person);
Assert.IsNotNull(layout.Person.Name);
}
}
}

public class RequiredRandomSelector : DataTemplateSelector
{
public /*required*/ Controls.DataTemplate Template1 { get; set; }
public required Controls.DataTemplate Template2 { get; set; }

protected override Controls.DataTemplate OnSelectTemplate(object item, BindableObject container) => new Random().Next(2) == 0 ? Template1 : Template2;
}

public class RequiredPerson
{
public required string Name { get; set; }


public override string ToString()
=> Name;
}
Loading