diff --git a/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json b/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json
index e4da60f7ca..ad2b1e30f6 100644
--- a/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json
+++ b/samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json
@@ -8,14 +8,6 @@
}
},
"profiles": {
- "ControlCatalog.Web - IIS Express": {
- "commandName": "IISExpress",
- "launchBrowser": true,
- "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}",
- "environmentVariables": {
- "ASPNETCORE_ENVIRONMENT": "Development"
- }
- },
"ControlCatalog.Web": {
"commandName": "Project",
"dotnetRunMessages": "true",
diff --git a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
index 6c17e9ac43..e4c83dca49 100644
--- a/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
+++ b/samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
@@ -5,7 +5,7 @@
net6.0
true
true
- 6.0.9
+ 6.0.8
diff --git a/samples/ControlCatalog/MainView.xaml b/samples/ControlCatalog/MainView.xaml
index ec198c6bba..0d7a1ea4d7 100644
--- a/samples/ControlCatalog/MainView.xaml
+++ b/samples/ControlCatalog/MainView.xaml
@@ -12,8 +12,8 @@
-
-
+
+
@@ -118,6 +118,9 @@
+
+
+
@@ -130,7 +133,7 @@
-
+
diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml
new file mode 100644
index 0000000000..a60f54d10e
--- /dev/null
+++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
new file mode 100644
index 0000000000..1f37451782
--- /dev/null
+++ b/samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
@@ -0,0 +1,20 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ControlCatalog.ViewModels;
+
+namespace ControlCatalog.Pages
+{
+ public class PlatformInfoPage : UserControl
+ {
+ public PlatformInfoPage()
+ {
+ this.InitializeComponent();
+ DataContext = new PlatformInformationViewModel();
+ }
+
+ private void InitializeComponent()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+ }
+}
diff --git a/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs b/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs
new file mode 100644
index 0000000000..e4f6c3ac73
--- /dev/null
+++ b/samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs
@@ -0,0 +1,54 @@
+using Avalonia;
+using Avalonia.Platform;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels;
+#nullable enable
+
+public class PlatformInformationViewModel : ViewModelBase
+{
+ public PlatformInformationViewModel()
+ {
+ var runtimeInfo = AvaloniaLocator.Current.GetService()?.GetRuntimeInfo();
+
+ if (runtimeInfo is { } info)
+ {
+ if (info.IsBrowser)
+ {
+ if (info.IsDesktop)
+ {
+ PlatformInfo = "Platform: Desktop (browser)";
+ }
+ else if (info.IsMobile)
+ {
+ PlatformInfo = "Platform: Mobile (browser)";
+ }
+ else
+ {
+ PlatformInfo = "Platform: Unknown (browser) - please report";
+ }
+ }
+ else
+ {
+ if (info.IsDesktop)
+ {
+ PlatformInfo = "Platform: Desktop (native)";
+ }
+ else if (info.IsMobile)
+ {
+ PlatformInfo = "Platform: Mobile (native)";
+ }
+ else
+ {
+ PlatformInfo = "Platform: Unknown (native) - please report";
+ }
+ }
+ }
+ else
+ {
+
+ }
+ }
+
+ public string PlatformInfo { get; }
+}
diff --git a/src/Avalonia.Base/Metadata/MarkupExtensionOption.cs b/src/Avalonia.Base/Metadata/MarkupExtensionOption.cs
new file mode 100644
index 0000000000..4009720bcb
--- /dev/null
+++ b/src/Avalonia.Base/Metadata/MarkupExtensionOption.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Avalonia.Metadata;
+
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+public sealed class MarkupExtensionOptionAttribute : Attribute
+{
+ public MarkupExtensionOptionAttribute(object value)
+ {
+ Value = value;
+ }
+
+ public object Value { get; }
+
+ public int Priority { get; set; } = 0;
+}
+
+[AttributeUsage(AttributeTargets.Property, AllowMultiple = false)]
+public sealed class MarkupExtensionDefaultOptionAttribute : Attribute
+{
+
+}
diff --git a/src/Avalonia.Base/Platform/IRuntimePlatform.cs b/src/Avalonia.Base/Platform/IRuntimePlatform.cs
index 8ab04f5995..3f8983479f 100644
--- a/src/Avalonia.Base/Platform/IRuntimePlatform.cs
+++ b/src/Avalonia.Base/Platform/IRuntimePlatform.cs
@@ -17,13 +17,16 @@ namespace Avalonia.Platform
IntPtr Address { get; }
int Size { get; }
bool IsDisposed { get; }
-
+
}
[Unstable]
public struct RuntimePlatformInfo
{
public OperatingSystemType OperatingSystem { get; set; }
+
+ public FormFactorType FormFactor => IsDesktop ? FormFactorType.Desktop :
+ IsMobile ? FormFactorType.Mobile : FormFactorType.Unknown;
public bool IsDesktop { get; set; }
public bool IsMobile { get; set; }
public bool IsBrowser { get; set; }
@@ -44,4 +47,12 @@ namespace Avalonia.Platform
iOS,
Browser
}
+
+ [Unstable]
+ public enum FormFactorType
+ {
+ Unknown,
+ Desktop,
+ Mobile
+ }
}
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
index 1e2a77c34d..1d4794f02a 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
@@ -198,6 +198,7 @@ namespace Avalonia.Markup.Xaml.XamlIl
compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType);
var created = tb.CreateTypeInfo();
clrPropertyBuilder.CreateTypeInfo();
+ indexerClosureType.CreateTypeInfo();
trampolineBuilder.CreateTypeInfo();
return LoadOrPopulate(created, rootInstance);
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index 4ece433530..1692238d06 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -22,20 +22,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings)
: base(configuration, emitMappings, true)
{
- void InsertAfter(params IXamlAstTransformer[] t)
+ void InsertAfter(params IXamlAstTransformer[] t)
=> Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t);
- void InsertBefore(params IXamlAstTransformer[] t)
+ void InsertBefore(params IXamlAstTransformer[] t)
=> Transformers.InsertRange(Transformers.FindIndex(x => x is T), t);
// Before everything else
-
+
Transformers.Insert(0, new XNameTransformer());
Transformers.Insert(1, new IgnoredDirectivesTransformer());
Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer());
Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer());
-
+
// Targeted
InsertBefore(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@@ -49,7 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
InsertBefore(
new AvaloniaXamlIlControlThemeTransformer(),
new AvaloniaXamlIlSelectorTransformer(),
- new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
+ new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlPropertyPathTransformer(),
new AvaloniaXamlIlSetterTargetTypeMetadataTransformer(),
@@ -58,6 +58,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),
new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
);
+ InsertBefore(
+ new AvaloniaXamlIlOptionMarkupExtensionTransformer());
InsertAfter(
new XDataTypeTransformer());
@@ -89,14 +91,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
_contextType = CreateContextType(contextTypeBuilder);
}
-
+
public AvaloniaXamlIlCompiler(TransformerConfiguration configuration,
XamlLanguageEmitMappings emitMappings,
IXamlType contextType) : this(configuration, emitMappings)
{
_contextType = contextType;
}
-
+
public const string PopulateName = "__AvaloniaXamlIlPopulate";
public const string BuildName = "__AvaloniaXamlIlBuild";
@@ -118,7 +120,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{
{XamlNamespaces.Blend2008, XamlNamespaces.Blend2008}
});
-
+
var rootObject = (XamlAstObjectNode)parsed.Root;
var classDirective = rootObject.Children
@@ -133,8 +135,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
false) :
TypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true),
(XamlAstXmlTypeReference)rootObject.Type, true);
-
-
+
+
if (overrideRootType != null)
{
if (!rootType.Type.IsAssignableFrom(overrideRootType))
@@ -147,7 +149,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
Transform(parsed);
Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource);
-
+
}
public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType)
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
index c8fced515d..b4999136a4 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
@@ -185,6 +185,15 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public static bool CustomValueConverter(AstTransformationContext context,
IXamlAstValueNode node, IXamlType type, out IXamlAstValueNode result)
{
+ if (node is AvaloniaXamlIlOptionMarkupExtensionTransformer.OptionsMarkupExtensionNode optionsNode)
+ {
+ if (optionsNode.ConvertToReturnType(context, type, out var newOptionsNode))
+ {
+ result = newOptionsNode;
+ return true;
+ }
+ }
+
if (!(node is XamlAstTextNode textNode))
{
result = null;
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
index d907bcbef9..023e978afa 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
@@ -48,10 +48,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var thickness = Thickness.Parse(text);
-
+
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Thickness, types.ThicknessFullConstructor,
new[] { thickness.Left, thickness.Top, thickness.Right, thickness.Bottom });
-
+
return true;
}
catch
@@ -65,10 +65,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var point = Point.Parse(text);
-
+
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Point, types.PointFullConstructor,
new[] { point.X, point.Y });
-
+
return true;
}
catch
@@ -76,16 +76,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a point", node);
}
}
-
+
if (type.Equals(types.Vector))
{
try
{
var vector = Vector.Parse(text);
-
+
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Vector, types.VectorFullConstructor,
new[] { vector.X, vector.Y });
-
+
return true;
}
catch
@@ -93,16 +93,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a vector", node);
}
}
-
+
if (type.Equals(types.Size))
{
try
{
var size = Size.Parse(text);
-
+
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Size, types.SizeFullConstructor,
new[] { size.Width, size.Height });
-
+
return true;
}
catch
@@ -110,16 +110,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a size", node);
}
}
-
+
if (type.Equals(types.Matrix))
{
try
{
var matrix = Matrix.Parse(text);
-
+
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.Matrix, types.MatrixFullConstructor,
new[] { matrix.M11, matrix.M12, matrix.M21, matrix.M22, matrix.M31, matrix.M32 });
-
+
return true;
}
catch
@@ -127,16 +127,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a matrix", node);
}
}
-
+
if (type.Equals(types.CornerRadius))
{
try
{
var cornerRadius = CornerRadius.Parse(text);
-
+
result = new AvaloniaXamlIlVectorLikeConstantAstNode(node, types, types.CornerRadius, types.CornerRadiusFullConstructor,
new[] { cornerRadius.TopLeft, cornerRadius.TopRight, cornerRadius.BottomRight, cornerRadius.BottomLeft });
-
+
return true;
}
catch
@@ -144,7 +144,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
throw new XamlX.XamlLoadException($"Unable to parse \"{text}\" as a corner radius", node);
}
}
-
+
if (type.Equals(types.Color))
{
if (!Color.TryParse(text, out Color color))
@@ -165,9 +165,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var relativePoint = RelativePoint.Parse(text);
-
+
var relativePointTypeRef = new XamlAstClrTypeReference(node, types.RelativePoint, false);
-
+
result = new XamlAstNewClrObjectNode(node, relativePointTypeRef, types.RelativePointFullConstructor, new List
{
new XamlConstantNode(node, types.XamlIlTypes.Double, relativePoint.Point.X),
@@ -188,9 +188,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
try
{
var gridLength = GridLength.Parse(text);
-
+
result = new AvaloniaXamlIlGridLengthAstNode(node, types, gridLength);
-
+
return true;
}
catch
@@ -201,12 +201,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
if (type.Equals(types.Cursor))
{
- if (TypeSystemHelpers.TryGetEnumValueNode(types.StandardCursorType, text, node, out var enumConstantNode))
+ if (TypeSystemHelpers.TryGetEnumValueNode(types.StandardCursorType, text, node, false, out var enumConstantNode))
{
var cursorTypeRef = new XamlAstClrTypeReference(node, types.Cursor, false);
result = new XamlAstNewClrObjectNode(node, cursorTypeRef, types.CursorTypeConstructor, new List { enumConstantNode });
-
+
return true;
}
}
@@ -275,7 +275,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
private static bool ConvertDefinitionList(
- IXamlAstValueNode node,
+ IXamlAstValueNode node,
string text,
AvaloniaXamlIlWellKnownTypes types,
IXamlType listType,
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs
new file mode 100644
index 0000000000..5004e594f7
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs
@@ -0,0 +1,403 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection.Emit;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Emit;
+using XamlX.IL;
+using XamlX.Transform;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
+
+internal class AvaloniaXamlIlOptionMarkupExtensionTransformer : IXamlAstTransformer
+{
+ public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
+ {
+ if (node is XamlMarkupExtensionNode
+ {
+ Value: XamlAstObjectNode { Type: XamlAstClrTypeReference { Type: { } type } } objectNode
+ } markupExtensionNode
+ && type.FindMethods(m => m.IsPublic && m.Parameters.Count is 1 or 2 && m.ReturnType == context.Configuration.WellKnownTypes.Boolean && m.Name == "ShouldProvideOption").ToArray() is { } methods
+ && methods.Any())
+ {
+ var optionAttribute = context.GetAvaloniaTypes().MarkupExtensionOptionAttribute;
+ var defaultOptionAttribute = context.GetAvaloniaTypes().MarkupExtensionDefaultOptionAttribute;
+
+ var typeArgument = type.GenericArguments?.FirstOrDefault();
+
+ IXamlAstValueNode defaultValue = null;
+ var values = new List();
+
+ if (objectNode.Arguments.FirstOrDefault() is { } argument)
+ {
+ var hasDefaultProp = objectNode.Type.GetClrType().GetAllProperties().Any(p =>
+ p.CustomAttributes.Any(a => a.Type == defaultOptionAttribute));
+ if (hasDefaultProp)
+ {
+ if (objectNode.Arguments.Count > 1)
+ {
+ throw new XamlParseException("Options MarkupExtensions allow only single argument", objectNode);
+ }
+
+ defaultValue = TransformNode(new[] { argument }, typeArgument, objectNode);
+ objectNode.Arguments.Remove(argument);
+ }
+ }
+
+ foreach (var extProp in objectNode.Children.OfType().ToArray())
+ {
+ if (!extProp.Values.Any())
+ {
+ continue;
+ }
+
+ var shouldRemoveProp = false;
+ var onObjs = extProp.Values.OfType()
+ .Where(o => o.Type.GetClrType() == context.GetAvaloniaTypes().OnExtensionType).ToArray();
+ if (onObjs.Any())
+ {
+ shouldRemoveProp = true;
+ foreach (var onObj in onObjs)
+ {
+ var optionsPropNode = onObj.Children.OfType()
+ .SingleOrDefault(v => v.Property.GetClrProperty().Name == "Options")
+ ?.Values.Single();
+ var options = (optionsPropNode as XamlAstTextNode)?.Text?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
+ ?? Array.Empty();
+ if (options.Length == 0)
+ {
+ throw new XamlParseException("On.Options string must be set", onObj);
+ }
+
+ var content = onObj.Children.OfType()
+ .SingleOrDefault(v => v.Property.GetClrProperty().Name == "Content");
+ if (content is null)
+ {
+ throw new XamlParseException("On content object must be set", onObj);
+ }
+
+ var propertiesSet = options
+ .Select(o => type.GetAllProperties()
+ .FirstOrDefault(p => o.Equals(p.Name, StringComparison.Ordinal))
+ ?? throw new XamlParseException($"Property \"{o}\" wasn't found on the \"{type.Name}\" type", onObj))
+ .ToArray();
+ foreach (var propertySet in propertiesSet)
+ {
+ AddBranchNode(content.Values, propertySet.CustomAttributes, content);
+ }
+ }
+ }
+ else
+ {
+ shouldRemoveProp = AddBranchNode(extProp.Values, extProp.Property.GetClrProperty().CustomAttributes, extProp);
+ }
+
+ if (shouldRemoveProp)
+ {
+ objectNode.Children.Remove(extProp);
+ }
+ }
+
+ if (defaultValue is null && !values.Any())
+ {
+ throw new XamlParseException("Options markup extension requires at least one option to be set", objectNode);
+ }
+
+ return new OptionsMarkupExtensionNode(
+ markupExtensionNode, values.ToArray(), defaultValue,
+ context.Configuration.TypeMappings.ServiceProvider);
+
+ bool AddBranchNode(
+ IReadOnlyCollection valueNodes,
+ IReadOnlyCollection propAttributes,
+ IXamlLineInfo li)
+ {
+ var transformed = TransformNode(valueNodes, typeArgument, li);
+ if (propAttributes.FirstOrDefault(a => a.Type == defaultOptionAttribute) is { } defAttr)
+ {
+ defaultValue = transformed;
+ return true;
+ }
+ else if (propAttributes.FirstOrDefault(a => a.Type == optionAttribute) is { } optAttr)
+ {
+ var option = optAttr.Parameters.Single();
+ if (option is null)
+ {
+ throw new XamlParseException("MarkupExtension option must not be null", li);
+ }
+
+ var optionAsString = option.ToString();
+ IXamlAstValueNode optionNode = null;
+ foreach (var method in methods)
+ {
+ try
+ {
+ var targetType = method.Parameters.Last();
+ if (targetType.FullName == "System.Type")
+ {
+ if (option is IXamlType typeOption)
+ {
+ optionNode = new XamlTypeExtensionNode(li,
+ new XamlAstClrTypeReference(li, typeOption, false), targetType);
+ }
+ }
+ else if (targetType == context.Configuration.WellKnownTypes.String)
+ {
+ optionNode = new XamlConstantNode(li, targetType, optionAsString);
+ }
+ else if (targetType.IsEnum)
+ {
+ if (TypeSystemHelpers.TryGetEnumValueNode(targetType, optionAsString, li, false,
+ out var enumConstantNode))
+ {
+ optionNode = enumConstantNode;
+ }
+ }
+ else if (TypeSystemHelpers.ParseConstantIfTypeAllows(optionAsString, targetType, li,
+ out var constantNode))
+ {
+ optionNode = constantNode;
+ }
+ }
+ catch (FormatException)
+ {
+ // try next method overload
+ }
+
+ if (optionNode is not null)
+ {
+ values.Add(new OptionsMarkupExtensionBranch(optionNode, transformed, method));
+ return true;
+ }
+ }
+
+ throw new XamlParseException($"Option value \"{optionAsString}\" is not assignable to any of existing ShouldProvideOption methods", li);
+ }
+
+ return false;
+ }
+ }
+
+ return node;
+
+ IXamlAstValueNode TransformNode(
+ IReadOnlyCollection values,
+ IXamlType suggestedType,
+ IXamlLineInfo line)
+ {
+ if (suggestedType is not null)
+ {
+ values = values
+ .Select(v => XamlTransformHelpers
+ .TryGetCorrectlyTypedValue(context, v, suggestedType, out var converted)
+ ? converted : v)
+ .ToArray();
+ }
+
+ if (values.Count > 1)
+ {
+ throw new XamlParseException("Options markup extension supports only a singular value", line);
+ }
+
+ return values.Single();
+ }
+ }
+
+ internal sealed class OptionsMarkupExtensionNode : XamlMarkupExtensionNode, IXamlAstValueNode
+ {
+ private readonly IXamlType _contextParameter;
+
+ public OptionsMarkupExtensionNode(
+ XamlMarkupExtensionNode original,
+ OptionsMarkupExtensionBranch[] branches,
+ IXamlAstValueNode defaultNode,
+ IXamlType contextParameter)
+ : base(
+ original.Value,
+ new OptionsMarkupExtensionMethod(new OptionsMarkupExtensionNodesContainer(branches, defaultNode), original.Value.Type.GetClrType(), contextParameter),
+ original.Value)
+ {
+ _contextParameter = contextParameter;
+ }
+
+ public new OptionsMarkupExtensionMethod ProvideValue => (OptionsMarkupExtensionMethod)base.ProvideValue;
+
+ IXamlAstTypeReference IXamlAstValueNode.Type => new XamlAstClrTypeReference(this, ProvideValue.ReturnType, false);
+
+ public override void VisitChildren(IXamlAstVisitor visitor)
+ {
+ ProvideValue.ExtensionNodeContainer.Visit(visitor);
+ base.VisitChildren(visitor);
+ }
+
+ public bool ConvertToReturnType(AstTransformationContext context, IXamlType type, out OptionsMarkupExtensionNode res)
+ {
+ IXamlAstValueNode convertedDefaultNode = null;
+
+ if (ProvideValue.ExtensionNodeContainer.DefaultNode is { } defaultNode)
+ {
+ if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, defaultNode, type, out convertedDefaultNode))
+ {
+ res = null;
+ return false;
+ }
+ }
+
+ var convertedBranches = ProvideValue.ExtensionNodeContainer.Branches.Select(b => XamlTransformHelpers
+ .TryGetCorrectlyTypedValue(context, b.Value, type, out var convertedValue) ?
+ new OptionsMarkupExtensionBranch(b.Option, convertedValue, b.ConditionMethod) :
+ null).ToArray();
+ if (convertedBranches.Any(b => b is null))
+ {
+ res = null;
+ return false;
+ }
+
+ res = new OptionsMarkupExtensionNode(this, convertedBranches, convertedDefaultNode, _contextParameter);
+ return true;
+ }
+ }
+
+ internal sealed class OptionsMarkupExtensionNodesContainer : XamlAstNode
+ {
+ public OptionsMarkupExtensionNodesContainer(
+ OptionsMarkupExtensionBranch[] branches,
+ IXamlAstValueNode defaultNode) : base(branches.FirstOrDefault()?.Value ?? defaultNode)
+ {
+ Branches = branches;
+ DefaultNode = defaultNode;
+ }
+
+ public OptionsMarkupExtensionBranch[] Branches { get; }
+ public IXamlAstValueNode DefaultNode { get; private set; }
+
+ public override void VisitChildren(IXamlAstVisitor visitor)
+ {
+ VisitList(Branches, visitor);
+ DefaultNode = (IXamlAstValueNode)DefaultNode?.Visit(visitor);
+ }
+
+ public IXamlType GetReturnType()
+ {
+ var types = Branches.Select(b => b.Value.Type);
+ if (DefaultNode?.Type is { } type)
+ {
+ types = types.Concat(new [] { type });
+ }
+ return types.Select(t => t.GetClrType()).ToArray().GetCommonBaseClass();
+ }
+ }
+
+ internal sealed class OptionsMarkupExtensionBranch : XamlAstNode
+ {
+ public OptionsMarkupExtensionBranch(IXamlAstValueNode option, IXamlAstValueNode value, IXamlMethod conditionMethod) : base(value)
+ {
+ Option = option;
+ Value = value;
+ ConditionMethod = conditionMethod;
+ }
+
+ public IXamlAstValueNode Option { get; set; }
+ public IXamlAstValueNode Value { get; set; }
+ public IXamlMethod ConditionMethod { get; }
+
+ public bool HasContext => ConditionMethod.Parameters.Count > 1;
+
+ public override void VisitChildren(IXamlAstVisitor visitor)
+ {
+ Option = (IXamlAstValueNode)Option.Visit(visitor);
+ Value = (IXamlAstValueNode)Value.Visit(visitor);
+ }
+ }
+
+ internal sealed class OptionsMarkupExtensionMethod : IXamlCustomEmitMethodWithContext
+ {
+ public OptionsMarkupExtensionMethod(
+ OptionsMarkupExtensionNodesContainer extensionNodeContainer,
+ IXamlType declaringType,
+ IXamlType contextParameter)
+ {
+ ExtensionNodeContainer = extensionNodeContainer;
+ DeclaringType = declaringType;
+ Parameters = extensionNodeContainer.Branches.Any(c => c.HasContext) ?
+ new[] { contextParameter } :
+ Array.Empty();
+ }
+
+ public OptionsMarkupExtensionNodesContainer ExtensionNodeContainer { get; }
+
+ public string Name => "ProvideValue";
+ public bool IsPublic => true;
+ public bool IsStatic => false;
+ public IXamlType ReturnType => ExtensionNodeContainer.GetReturnType();
+ public IReadOnlyList Parameters { get; }
+ public IXamlType DeclaringType { get; }
+ public IXamlMethod MakeGenericMethod(IReadOnlyList typeArguments) => throw new NotImplementedException();
+ public IReadOnlyList CustomAttributes => Array.Empty();
+
+ public void EmitCall(XamlEmitContext context, IXamlILEmitter codeGen)
+ {
+ // At this point this extension will be called from MarkupExtensionEmitter.
+ // Since it's a "fake" method, we share stack and locals with parent method.
+ // Real ProvideValue method would pop 2 parameters from the stack and return one. This method should do the same.
+ // At this point we will have on stack:
+ // - context (if parameters > 1)
+ // - markup ext "@this" instance (always)
+ // We always pop context from the stack, as this method decide by itself either context is needed.
+ // We store "@this" as a local variable. But only if any conditional method is an instance method.
+ IXamlLocal @this = null;
+ if (Parameters.Count > 0)
+ {
+ codeGen.Pop();
+ }
+ if (ExtensionNodeContainer.Branches.Any(b => !b.ConditionMethod.IsStatic))
+ {
+ codeGen.Stloc(@this = codeGen.DefineLocal(DeclaringType));
+ }
+ else
+ {
+ codeGen.Pop();
+ }
+
+ // Iterate over all branches and push prepared locals into the stack if needed.
+ var ret = codeGen.DefineLabel();
+ foreach (var branch in ExtensionNodeContainer.Branches)
+ {
+ var next = codeGen.DefineLabel();
+ if (branch.HasContext)
+ {
+ codeGen.Ldloc(context.ContextLocal);
+ }
+ if (!branch.ConditionMethod.IsStatic)
+ {
+ codeGen.Ldloc(@this);
+ }
+ context.Emit(branch.Option, codeGen, branch.Option.Type.GetClrType());
+ codeGen.EmitCall(branch.ConditionMethod);
+ codeGen.Brfalse(next);
+
+ context.Emit(branch.Value, codeGen, branch.Value.Type.GetClrType());
+ codeGen.Br(ret);
+ codeGen.MarkLabel(next);
+ }
+
+ if (ExtensionNodeContainer.DefaultNode is {} defaultNode)
+ {
+ // Nop is needed, otherwise Label wouldn't be set on nested CALL op (limitation of our IL validator).
+ codeGen.Emit(OpCodes.Nop);
+ context.Emit(defaultNode, codeGen, defaultNode.Type.GetClrType());
+ }
+ else
+ {
+ codeGen.EmitDefault(ReturnType);
+ }
+
+ codeGen.MarkLabel(ret);
+ }
+
+ public bool Equals(IXamlMethod other) => ReferenceEquals(this, other);
+ }
+}
+
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
index dbfbe0e070..c3e0223de4 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
@@ -9,6 +9,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{
class AvaloniaXamlIlWellKnownTypes
{
+ public IXamlType RuntimeHelpers { get; }
public IXamlType AvaloniaObject { get; }
public IXamlType IAvaloniaObject { get; }
public IXamlType BindingPriority { get; }
@@ -29,6 +30,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType AssignBindingAttribute { get; }
public IXamlType DependsOnAttribute { get; }
public IXamlType DataTypeAttribute { get; }
+ public IXamlType MarkupExtensionOptionAttribute { get; }
+ public IXamlType MarkupExtensionDefaultOptionAttribute { get; }
+ public IXamlType OnExtensionType { get; }
public IXamlType UnsetValueType { get; }
public IXamlType StyledElement { get; }
public IXamlType IStyledElement { get; }
@@ -104,6 +108,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg)
{
+ RuntimeHelpers = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers");
+
XamlIlTypes = cfg.WellKnownTypes;
AvaloniaObject = cfg.TypeSystem.GetType("Avalonia.AvaloniaObject");
IAvaloniaObject = cfg.TypeSystem.GetType("Avalonia.IAvaloniaObject");
@@ -125,6 +131,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute");
DependsOnAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DependsOnAttribute");
DataTypeAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.DataTypeAttribute");
+ MarkupExtensionOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionOptionAttribute");
+ MarkupExtensionDefaultOptionAttribute = cfg.TypeSystem.GetType("Avalonia.Metadata.MarkupExtensionDefaultOptionAttribute");
+ OnExtensionType = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.On");
AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject,
AvaloniaProperty,
IBinding, cfg.WellKnownTypes.Object);
@@ -239,7 +248,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration));
return rv;
}
-
+
public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlEmitContext ctx)
{
if (ctx.TryGetItem(out var rv))
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
index c1c0594ec2..880ba5742e 160000
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
@@ -1 +1 @@
-Subproject commit c1c0594ec2c35b08988183b1a5b3e34dfa19179d
+Subproject commit 880ba5742e52b67afda048c4023cf7e3c3c16a46
diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
index 75746273c2..f5bf14c2a4 100644
--- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
+++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
@@ -29,6 +29,9 @@
+
+
+
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs
new file mode 100644
index 0000000000..0752ab43b5
--- /dev/null
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs
@@ -0,0 +1,15 @@
+#nullable enable
+using System.Collections.Generic;
+using Avalonia.Metadata;
+
+namespace Avalonia.Markup.Xaml.MarkupExtensions;
+
+public class On
+{
+ public IReadOnlyList Options { get; } = new List();
+
+ [Content]
+ public TReturn? Content { get; set; }
+}
+
+public class On : On