Browse Source

Merge branch 'master' into notification

pull/9277/head
Max Katz 3 years ago
committed by GitHub
parent
commit
2372e238cf
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      samples/ControlCatalog.Blazor.Web/Properties/launchSettings.json
  2. 2
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  3. 9
      samples/ControlCatalog/MainView.xaml
  4. 51
      samples/ControlCatalog/Pages/PlatformInfoPage.xaml
  5. 20
      samples/ControlCatalog/Pages/PlatformInfoPage.xaml.cs
  6. 54
      samples/ControlCatalog/ViewModels/PlatformInformationViewModel.cs
  7. 22
      src/Avalonia.Base/Metadata/MarkupExtensionOption.cs
  8. 13
      src/Avalonia.Base/Platform/IRuntimePlatform.cs
  9. 6
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  10. 5
      src/Avalonia.Diagnostics/Diagnostics/Conventions.cs
  11. 7
      src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs
  12. 5
      src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs
  13. 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  14. 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  15. 9
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  16. 14
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs
  17. 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs
  18. 10
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  19. 4
      src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml
  20. 1
      src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
  21. 24
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  22. 9
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  23. 69
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  24. 403
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlOptionMarkupExtensionTransformer.cs
  25. 15
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  26. 2
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  27. 3
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  28. 15
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/On.cs
  29. 60
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs
  30. 73
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs
  31. 22
      src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
  32. 26
      src/Web/Avalonia.Web/BrowserRuntimePlatform.cs
  33. 2
      src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs
  34. 5
      src/Web/Avalonia.Web/Interop/AvaloniaModule.cs
  35. 8
      src/Web/Avalonia.Web/WindowingPlatform.cs
  36. 18
      src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts
  37. 25
      tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs
  38. 5
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  39. 71
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs
  40. 75
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs
  41. 605
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs

8
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",

2
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -5,7 +5,7 @@
<TargetFramework>net6.0</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
<RuntimeFrameworkVersion>6.0.9</RuntimeFrameworkVersion>
<RuntimeFrameworkVersion>6.0.8</RuntimeFrameworkVersion>
</PropertyGroup>
<PropertyGroup Condition="'$(RunNativeAotCompilation)' == 'true'">

9
samples/ControlCatalog/MainView.xaml

@ -12,8 +12,8 @@
<Setter Property="HorizontalAlignment" Value="Left" />
</Style>
</Grid.Styles>
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<controls:HamburgerMenu Name="Sidebar">
<TabItem Header="Composition">
<pages:CompositionPage/>
</TabItem>
<TabItem Header="Acrylic">
@ -118,6 +118,9 @@
<TabItem Header="OpenGL">
<pages:OpenGlPage />
</TabItem>
<TabItem Header="Platform Information">
<pages:PlatformInfoPage />
</TabItem>
<TabItem Header="Pointers">
<pages:PointersPage />
</TabItem>
@ -130,7 +133,7 @@
<TabItem Header="RelativePanel">
<pages:RelativePanelPage />
</TabItem>
<TabItem Header="ScrollViewer">
<TabItem Header="ScrollViewer">
<pages:ScrollViewerPage />
</TabItem>
<TabItem Header="Slider">

51
samples/ControlCatalog/Pages/PlatformInfoPage.xaml

@ -0,0 +1,51 @@
<UserControl x:Class="ControlCatalog.Pages.PlatformInfoPage"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
d:DesignHeight="800"
d:DesignWidth="400"
mc:Ignorable="d">
<StackPanel Spacing="20">
<TextBlock Text="{Binding PlatformInfo}" />
<StackPanel TextElement.Foreground="White">
<StackPanel Orientation="Horizontal">
<Border Height="100" Width="100" Background="{OnFormFactor Gray, Desktop=Green}">
<TextBlock Text="Desktop" />
</Border>
<Border Height="100" Width="100" Background="{OnFormFactor Gray, Mobile=Green}">
<TextBlock Text="Mobile" />
</Border>
</StackPanel>
<WrapPanel>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Windows=Green}">
<TextBlock Text="Windows" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, macOS=Green}">
<TextBlock Text="macOS" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Linux=Green}">
<TextBlock Text="Linux" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Browser=Green}">
<TextBlock Text="Browser" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, iOS=Green}">
<TextBlock Text="iOS" />
</Border>
<Border Height="100" Width="100" Background="{OnPlatform Gray, Android=Green}">
<TextBlock Text="Android" />
</Border>
<Border Height="100" Width="100">
<Border.Background>
<OnPlatform Default="Gray" >
<On Options="macOS, Linux, Windows" Content="Green" />
</OnPlatform>
</Border.Background>
<TextBlock Text="Win, Lin or Mac" />
</Border>
</WrapPanel>
</StackPanel>
</StackPanel>
</UserControl>

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

54
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<IRuntimePlatform>()?.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; }
}

22
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
{
}

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

6
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -178,10 +178,11 @@ namespace Avalonia.Build.Tasks
var stringEquals = asm.MainModule.ImportReference(asm.MainModule.TypeSystem.String.Resolve().Methods.First(
m =>
m.IsStatic && m.Name == "Equals" && m.Parameters.Count == 2 &&
m.IsStatic && m.Name == "Equals" && m.Parameters.Count == 3 &&
m.ReturnType.FullName == "System.Boolean"
&& m.Parameters[0].ParameterType.FullName == "System.String"
&& m.Parameters[1].ParameterType.FullName == "System.String"));
&& m.Parameters[1].ParameterType.FullName == "System.String"
&& m.Parameters[2].ParameterType.FullName == "System.StringComparison"));
bool CompileGroup(IResourceGroup group)
{
@ -384,6 +385,7 @@ namespace Avalonia.Build.Tasks
var nop = Instruction.Create(OpCodes.Nop);
i.Add(Instruction.Create(OpCodes.Ldarg_0));
i.Add(Instruction.Create(OpCodes.Ldstr, res.Uri));
i.Add(Instruction.Create(OpCodes.Ldc_I4, (int)StringComparison.OrdinalIgnoreCase));
i.Add(Instruction.Create(OpCodes.Call, stringEquals));
i.Add(Instruction.Create(OpCodes.Brfalse, nop));
if (parameterlessConstructor != null)

5
src/Avalonia.Diagnostics/Diagnostics/Convetions.cs → src/Avalonia.Diagnostics/Diagnostics/Conventions.cs

@ -1,11 +1,8 @@
using System;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics
{
static class Convetions
internal static class Conventions
{
public static string DefaultScreenshotsRoot =>
System.IO.Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.MyPictures, Environment.SpecialFolderOption.Create),

7
src/Avalonia.Diagnostics/Diagnostics/DevToolsOptions.cs

@ -1,5 +1,4 @@
using System;
using Avalonia.Input;
using Avalonia.Input;
namespace Avalonia.Diagnostics
{
@ -36,11 +35,11 @@ namespace Avalonia.Diagnostics
public bool ShowImplementedInterfaces { get; set; } = true;
/// <summary>
/// Allow to customizze SreenshotHandler
/// Allow to customize SreenshotHandler
/// </summary>
/// <remarks>Default handler is <see cref="Screenshots.FilePickerHandler"/></remarks>
public IScreenshotHandler ScreenshotHandler { get; set; }
= Convetions.DefaultScreenshotHandler;
= Conventions.DefaultScreenshotHandler;
/// <summary>
/// Gets or sets whether DevTools should use the dark mode theme

5
src/Avalonia.Diagnostics/Diagnostics/Screenshots/FilePickerHandler.cs

@ -1,5 +1,4 @@
using System;
using System.IO;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
@ -40,7 +39,7 @@ namespace Avalonia.Diagnostics.Screenshots
/// The default root folder is [Environment.SpecialFolder.MyPictures]/Screenshots.
/// </summary>
public string ScreenshotsRoot { get; }
= Convetions.DefaultScreenshotsRoot;
= Conventions.DefaultScreenshotsRoot;
/// <summary>
/// SaveFilePicker Title

1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@ -1,6 +1,5 @@
using System;
using Avalonia.Data;
using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{

1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@ -1,6 +1,5 @@
using System;
using System.Reflection;
using Avalonia.Media;
namespace Avalonia.Diagnostics.ViewModels
{

9
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
@ -15,7 +14,7 @@ using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{
internal class ControlDetailsViewModel : ViewModelBase, IDisposable
internal class ControlDetailsViewModel : ViewModelBase, IDisposable, IClassesChangedListener
{
private readonly IAvaloniaObject _avaloniaObject;
private IDictionary<object, PropertyViewModel[]>? _propertyIndex;
@ -46,7 +45,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (avaloniaObject is StyledElement styledElement)
{
styledElement.Classes.CollectionChanged += OnClassesChanged;
styledElement.Classes.AddListener(this);
var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes<PseudoClassesAttribute>(true);
@ -250,7 +249,7 @@ namespace Avalonia.Diagnostics.ViewModels
if (_avaloniaObject is StyledElement se)
{
se.Classes.CollectionChanged -= OnClassesChanged;
se.Classes.RemoveListener(this);
}
}
@ -325,7 +324,7 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e)
void IClassesChangedListener.Changed()
{
if (!SnapshotStyles)
{

14
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@ -1,13 +1,11 @@
using System;
using System.ComponentModel;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Diagnostics.Models;
using Avalonia.Input;
using Avalonia.Metadata;
using Avalonia.Threading;
using System.Linq;
namespace Avalonia.Diagnostics.ViewModels
{
@ -96,7 +94,9 @@ namespace Avalonia.Diagnostics.ViewModels
changed = false;
}
if (changed)
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
{
RaiseAndSetIfChanged(ref _shouldVisualizeDirtyRects, value);
}
}
}
@ -331,7 +331,7 @@ namespace Avalonia.Diagnostics.ViewModels
private set => RaiseAndSetIfChanged(ref _showImplementedInterfaces , value);
}
public void ToggleShowImplementedInterfaces(object parametr)
public void ToggleShowImplementedInterfaces(object parameter)
{
ShowImplementedInterfaces = !ShowImplementedInterfaces;
if (Content is TreePageViewModel viewModel)
@ -340,15 +340,15 @@ namespace Avalonia.Diagnostics.ViewModels
}
}
public bool ShowDettailsPropertyType
public bool ShowDetailsPropertyType
{
get => _showPropertyType;
private set => RaiseAndSetIfChanged(ref _showPropertyType , value);
}
public void ToggleShowDettailsPropertyType(object paramter)
public void ToggleShowDetailsPropertyType(object parameter)
{
ShowDettailsPropertyType = !ShowDettailsPropertyType;
ShowDetailsPropertyType = !ShowDetailsPropertyType;
}
}
}

1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/TreeNode.cs

@ -6,7 +6,6 @@ using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace Avalonia.Diagnostics.ViewModels
{

10
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@ -19,7 +19,7 @@
<ColumnDefinition Width="Auto"/>
<!--
When selecting the Application node, we need this trick to hide Layout Visualizer
because when using the GridSplitter it sets the Witdth property of ColumnDefinition
because when using the GridSplitter it sets the Width property of ColumnDefinition
(see https://github.com/AvaloniaUI/Avalonia/blob/master/src/Avalonia.Controls/GridSplitter.cs#L528)
and if we hide the contents of the column, the space is not reclaimed
(see discussion https://github.com/AvaloniaUI/Avalonia/discussions/6773).
@ -62,15 +62,15 @@
<DataGridTextColumn Header="Value" Binding="{Binding Value}" />
<DataGridTextColumn Header="Type" Binding="{Binding Type}"
IsReadOnly="True"
IsVisible="{Binding !$parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
IsVisible="{Binding !$parent[UserControl;2].DataContext.ShowDetailsPropertyType}"
/>
<DataGridTextColumn Header="Assinged Type" Binding="{Binding AssignedType, Converter={StaticResource GetTypeName}}"
<DataGridTextColumn Header="Assigned Type" Binding="{Binding AssignedType, Converter={StaticResource GetTypeName}}"
IsReadOnly="True"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDetailsPropertyType}"
/>
<DataGridTextColumn Header="Property Type" Binding="{Binding PropertyType, Converter={StaticResource GetTypeName}}"
IsReadOnly="True"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDettailsPropertyType}"
IsVisible="{Binding $parent[UserControl;2].DataContext.ShowDetailsPropertyType}"
/>
<DataGridTextColumn Header="Priority" Binding="{Binding Priority}" IsReadOnly="True" />
</DataGrid.Columns>

4
src/Avalonia.Diagnostics/Diagnostics/Views/MainView.xaml

@ -53,10 +53,10 @@
IsEnabled="False" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Split Property Type" Command="{Binding ToggleShowDettailsPropertyType}">
<MenuItem Header="Split Property Type" Command="{Binding ToggleShowDetailsPropertyType}">
<MenuItem.Icon>
<CheckBox BorderThickness="0"
IsChecked="{Binding ShowDettailsPropertyType}"
IsChecked="{Binding ShowDetailsPropertyType}"
IsEnabled="False"/>
</MenuItem.Icon>

1
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);

24
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -22,20 +22,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings)
: base(configuration, emitMappings, true)
{
void InsertAfter<T>(params IXamlAstTransformer[] t)
void InsertAfter<T>(params IXamlAstTransformer[] t)
=> Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t);
void InsertBefore<T>(params IXamlAstTransformer[] t)
void InsertBefore<T>(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<PropertyReferenceResolver>(
new AvaloniaXamlIlResolveClassesPropertiesTransformer(),
@ -49,7 +49,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
InsertBefore<ContentConvertTransformer>(
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<ConvertPropertyValuesToAssignmentsTransformer>(
new AvaloniaXamlIlOptionMarkupExtensionTransformer());
InsertAfter<TypeReferenceResolver>(
new XDataTypeTransformer());
@ -89,14 +91,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
_contextType = CreateContextType(contextTypeBuilder);
}
public AvaloniaXamlIlCompiler(TransformerConfiguration configuration,
XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> 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)

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

69
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<IXamlAstValueNode>
{
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<IXamlAstValueNode> { enumConstantNode });
return true;
}
}
@ -270,12 +270,33 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
}
if (type.Equals(types.Uri))
{
var uriText = text.Trim();
var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.Absolute : UriKind.Relative);
if (string.IsNullOrWhiteSpace(uriText) || !Uri.TryCreate(uriText, kind, out var _))
{
throw new XamlX.XamlLoadException($"Unable to parse text {uriText} as a {kind} uri.", node);
}
result = new XamlAstNewClrObjectNode(node
, new(node, types.Uri, false)
, types.UriConstructor
, new List<IXamlAstValueNode>()
{
new XamlConstantNode(node, context.Configuration.WellKnownTypes.String, uriText),
new XamlConstantNode(node, types.UriKind, (int)kind),
});
return true;
}
result = null;
return false;
}
private static bool ConvertDefinitionList(
IXamlAstValueNode node,
IXamlAstValueNode node,
string text,
AvaloniaXamlIlWellKnownTypes types,
IXamlType listType,

403
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<OptionsMarkupExtensionBranch>();
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<XamlAstXamlPropertyValueNode>().ToArray())
{
if (!extProp.Values.Any())
{
continue;
}
var shouldRemoveProp = false;
var onObjs = extProp.Values.OfType<XamlAstObjectNode>()
.Where(o => o.Type.GetClrType() == context.GetAvaloniaTypes().OnExtensionType).ToArray();
if (onObjs.Any())
{
shouldRemoveProp = true;
foreach (var onObj in onObjs)
{
var optionsPropNode = onObj.Children.OfType<XamlAstXamlPropertyValueNode>()
.SingleOrDefault(v => v.Property.GetClrProperty().Name == "Options")
?.Values.Single();
var options = (optionsPropNode as XamlAstTextNode)?.Text?.Split(new[] { ',', ' ' }, StringSplitOptions.RemoveEmptyEntries)
?? Array.Empty<string>();
if (options.Length == 0)
{
throw new XamlParseException("On.Options string must be set", onObj);
}
var content = onObj.Children.OfType<XamlAstXamlPropertyValueNode>()
.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<IXamlAstValueNode> valueNodes,
IReadOnlyCollection<IXamlCustomAttribute> 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<IXamlAstValueNode> 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<IXamlILEmitter, XamlILNodeEmitResult>
{
public OptionsMarkupExtensionMethod(
OptionsMarkupExtensionNodesContainer extensionNodeContainer,
IXamlType declaringType,
IXamlType contextParameter)
{
ExtensionNodeContainer = extensionNodeContainer;
DeclaringType = declaringType;
Parameters = extensionNodeContainer.Branches.Any(c => c.HasContext) ?
new[] { contextParameter } :
Array.Empty<IXamlType>();
}
public OptionsMarkupExtensionNodesContainer ExtensionNodeContainer { get; }
public string Name => "ProvideValue";
public bool IsPublic => true;
public bool IsStatic => false;
public IXamlType ReturnType => ExtensionNodeContainer.GetReturnType();
public IReadOnlyList<IXamlType> Parameters { get; }
public IXamlType DeclaringType { get; }
public IXamlMethod MakeGenericMethod(IReadOnlyList<IXamlType> typeArguments) => throw new NotImplementedException();
public IReadOnlyList<IXamlCustomAttribute> CustomAttributes => Array.Empty<IXamlCustomAttribute>();
public void EmitCall(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> 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);
}
}

15
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; }
@ -101,9 +105,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
public IXamlType IResourceDictionary { get; }
public IXamlType ResourceDictionary { get; }
public IXamlMethod ResourceDictionaryDeferredAdd { get; }
public IXamlType UriKind { get; }
public IXamlConstructor UriConstructor { get; }
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 +133,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);
@ -227,6 +238,8 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
cfg.TypeSystem.GetType("System.Func`2").MakeGenericType(
cfg.TypeSystem.GetType("System.IServiceProvider"),
XamlIlTypes.Object));
UriKind = cfg.TypeSystem.GetType("System.UriKind");
UriConstructor = Uri.GetConstructor(new List<IXamlType>() { cfg.WellKnownTypes.String, UriKind });
}
}
@ -239,7 +252,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration));
return rv;
}
public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> ctx)
{
if (ctx.TryGetItem<AvaloniaXamlIlWellKnownTypes>(out var rv))

2
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@ -1 +1 @@
Subproject commit c1c0594ec2c35b08988183b1a5b3e34dfa19179d
Subproject commit 880ba5742e52b67afda048c4023cf7e3c3c16a46

3
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -29,6 +29,9 @@
<Compile Include="MarkupExtensions\CompiledBindings\StrongTypeCastNode.cs" />
<Compile Include="MarkupExtensions\CompiledBindings\TaskStreamPlugin.cs" />
<Compile Include="MarkupExtensions\DynamicResourceExtension.cs" />
<Compile Include="MarkupExtensions\On.cs" />
<Compile Include="MarkupExtensions\OnFormFactorExtension.cs" />
<Compile Include="MarkupExtensions\OnPlatformExtension.cs" />
<Compile Include="MarkupExtensions\ResolveByNameExtension.cs" />
<Compile Include="MarkupExtensions\ResourceInclude.cs" />
<Compile Include="MarkupExtensions\StaticResourceExtension.cs" />

15
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<TReturn>
{
public IReadOnlyList<string> Options { get; } = new List<string>();
[Content]
public TReturn? Content { get; set; }
}
public class On : On<object> {}

60
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnFormFactorExtension.cs

@ -0,0 +1,60 @@
#nullable enable
using System;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class OnFormFactorExtension : OnFormFactorExtensionBase<object, On>
{
public OnFormFactorExtension()
{
}
public OnFormFactorExtension(object defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().FormFactor == option;
}
}
public class OnFormFactorExtension<TReturn> : OnFormFactorExtensionBase<TReturn, On<TReturn>>
{
public OnFormFactorExtension()
{
}
public OnFormFactorExtension(TReturn defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, FormFactorType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().FormFactor == option;
}
}
public abstract class OnFormFactorExtensionBase<TReturn, TOn> : IAddChild<TOn>
where TOn : On<TReturn>
{
[MarkupExtensionDefaultOption]
public TReturn? Default { get; set; }
[MarkupExtensionOption(FormFactorType.Desktop)]
public TReturn? Desktop { get; set; }
[MarkupExtensionOption(FormFactorType.Mobile)]
public TReturn? Mobile { get; set; }
// Required for the compiler, will be replaced with actual method compile time.
public object ProvideValue() { return this; }
void IAddChild<TOn>.AddChild(TOn child) {}
}

73
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/OnPlatformExtension.cs

@ -0,0 +1,73 @@
#nullable enable
using System;
using Avalonia.Metadata;
using Avalonia.Platform;
namespace Avalonia.Markup.Xaml.MarkupExtensions;
public class OnPlatformExtension : OnPlatformExtensionBase<object, On>
{
public OnPlatformExtension()
{
}
public OnPlatformExtension(object defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option;
}
}
public class OnPlatformExtension<TReturn> : OnPlatformExtensionBase<TReturn, On<TReturn>>
{
public OnPlatformExtension()
{
}
public OnPlatformExtension(TReturn defaultValue)
{
Default = defaultValue;
}
public static bool ShouldProvideOption(IServiceProvider serviceProvider, OperatingSystemType option)
{
return serviceProvider.GetService<IRuntimePlatform>().GetRuntimeInfo().OperatingSystem == option;
}
}
public abstract class OnPlatformExtensionBase<TReturn, TOn> : IAddChild<TOn>
where TOn : On<TReturn>
{
[MarkupExtensionDefaultOption]
public TReturn? Default { get; set; }
[MarkupExtensionOption(OperatingSystemType.WinNT)]
public TReturn? Windows { get; set; }
[MarkupExtensionOption(OperatingSystemType.OSX)]
// ReSharper disable once InconsistentNaming
public TReturn? macOS { get; set; }
[MarkupExtensionOption(OperatingSystemType.Linux)]
public TReturn? Linux { get; set; }
[MarkupExtensionOption(OperatingSystemType.Android)]
public TReturn? Android { get; set; }
[MarkupExtensionOption(OperatingSystemType.iOS)]
// ReSharper disable once InconsistentNaming
public TReturn? iOS { get; set; }
[MarkupExtensionOption(OperatingSystemType.Browser)]
public TReturn? Browser { get; set; }
// Required for the compiler, will be replaced with actual method compile time.
public object ProvideValue() { return this; }
void IAddChild<TOn>.AddChild(TOn child) {}
}

22
src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs

@ -1,11 +1,12 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Platform;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedParameter.Global
@ -18,7 +19,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
{
return DeferredTransformationFactoryV2<IControl>(builder, provider);
}
public static Func<IServiceProvider, object> DeferredTransformationFactoryV2<T>(Func<IServiceProvider, object> builder,
IServiceProvider provider)
{
@ -31,7 +32,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope();
var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope));
scope.Complete();
if(typeof(T) == typeof(IControl))
return new ControlTemplateResult((IControl)obj, scope);
@ -107,9 +108,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
throw new ArgumentException("Don't know what to do with " + value.GetType());
}
public static IServiceProvider CreateInnerServiceProviderV1(IServiceProvider compiled)
public static IServiceProvider CreateInnerServiceProviderV1(IServiceProvider compiled)
=> new InnerServiceProvider(compiled);
class InnerServiceProvider : IServiceProvider
{
private readonly IServiceProvider _compiledProvider;
@ -136,7 +137,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
{
_nsInfo = nsInfo;
}
public Type Resolve(string qualifiedTypeName)
{
var sp = qualifiedTypeName.Split(new[] {':'}, 2);
@ -166,22 +167,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
return new RootServiceProvider(new NameScope());
}
#line default
class RootServiceProvider : IServiceProvider, IAvaloniaXamlIlParentStackProvider
{
private readonly INameScope _nameScope;
private readonly IRuntimePlatform _runtimePlatform;
public RootServiceProvider(INameScope nameScope)
{
_nameScope = nameScope;
_runtimePlatform = AvaloniaLocator.Current.GetService<IRuntimePlatform>();
}
public object GetService(Type serviceType)
{
if (serviceType == typeof(INameScope))
return _nameScope;
if (serviceType == typeof(IAvaloniaXamlIlParentStackProvider))
return this;
if (serviceType == typeof(IRuntimePlatform))
return _runtimePlatform ?? throw new KeyNotFoundException($"{nameof(IRuntimePlatform)} was not registered");
return null;
}

26
src/Web/Avalonia.Web/BrowserRuntimePlatform.cs

@ -0,0 +1,26 @@
using System;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.JavaScript;
using System.Text.RegularExpressions;
using Avalonia.Platform;
using Avalonia.Web.Interop;
namespace Avalonia.Web;
internal class BrowserRuntimePlatform : StandardRuntimePlatform
{
private static readonly Lazy<RuntimePlatformInfo> Info = new(() =>
{
var result = new RuntimePlatformInfo
{
IsCoreClr = true, // WASM browser is always CoreCLR
IsBrowser = true, // BrowserRuntimePlatform only runs on Browser.
OperatingSystem = OperatingSystemType.Browser,
IsMobile = AvaloniaModule.IsMobile()
};
return result;
});
public override RuntimePlatformInfo GetRuntimeInfo() => Info.Value;
}

2
src/Web/Avalonia.Web/BrowserSingleViewLifetime.cs

@ -3,6 +3,7 @@ using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Web.Skia;
using System.Runtime.Versioning;
using Avalonia.Platform;
namespace Avalonia.Web;
@ -23,7 +24,6 @@ public class BrowserPlatformOptions
public Func<string, string> FrameworkAssetPathResolver { get; set; } = new(fileName => $"./{fileName}");
}
[SupportedOSPlatform("browser")]
public static class WebAppBuilder
{

5
src/Web/Avalonia.Web/Interop/AvaloniaModule.cs

@ -3,7 +3,7 @@ using System.Threading.Tasks;
namespace Avalonia.Web.Interop;
internal static class AvaloniaModule
internal static partial class AvaloniaModule
{
public const string MainModuleName = "avalonia";
public const string StorageModuleName = "storage";
@ -19,4 +19,7 @@ internal static class AvaloniaModule
var options = AvaloniaLocator.Current.GetService<BrowserPlatformOptions>() ?? new BrowserPlatformOptions();
return JSHost.ImportAsync(StorageModuleName, options.FrameworkAssetPathResolver("storage.js"));
}
[JSImport("Caniuse.isMobile", AvaloniaModule.MainModuleName)]
public static partial bool IsMobile();
}

8
src/Web/Avalonia.Web/WindowingPlatform.cs

@ -31,8 +31,10 @@ namespace Avalonia.Web
public static void Register()
{
var instance = new BrowserWindowingPlatform();
s_keyboard = new KeyboardDevice();
AvaloniaLocator.CurrentMutable
.Bind<IRuntimePlatform>().ToSingleton<BrowserRuntimePlatform>()
.Bind<IClipboard>().ToSingleton<ClipboardImpl>()
.Bind<ICursorFactory>().ToSingleton<CssCursorFactory>()
.Bind<IKeyboardDevice>().ToConstant(s_keyboard)
@ -64,11 +66,11 @@ namespace Avalonia.Web
{
if (_signaled)
return;
_signaled = true;
IDisposable? disp = null;
disp = GetRuntimePlatform()
.StartSystemTimer(TimeSpan.FromMilliseconds(1),
() =>

18
src/Web/Avalonia.Web/webapp/modules/avalonia/caniuse.ts

@ -1,13 +1,25 @@
export class Caniuse {
public static canShowOpenFilePicker(): boolean {
return typeof window.showOpenFilePicker !== "undefined";
return typeof globalThis.showOpenFilePicker !== "undefined";
}
public static canShowSaveFilePicker(): boolean {
return typeof window.showSaveFilePicker !== "undefined";
return typeof globalThis.showSaveFilePicker !== "undefined";
}
public static canShowDirectoryPicker(): boolean {
return typeof window.showDirectoryPicker !== "undefined";
return typeof globalThis.showDirectoryPicker !== "undefined";
}
public static isMobile(): boolean {
const userAgentData = (globalThis.navigator as any)?.userAgentData;
if (userAgentData) {
return userAgentData.mobile;
}
const userAgent = navigator.userAgent;
const regex1 = /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i;
const regex2 = /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw(n|u)|c55\/|capi|ccwa|cdm|cell|chtm|cldc|cmd|co(mp|nd)|craw|da(it|ll|ng)|dbte|dcs|devi|dica|dmob|do(c|p)o|ds(12|d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(|_)|g1 u|g560|gene|gf5|gmo|go(\.w|od)|gr(ad|un)|haie|hcit|hd(m|p|t)|hei|hi(pt|ta)|hp( i|ip)|hsc|ht(c(| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i(20|go|ma)|i230|iac( ||\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|[a-w])|libw|lynx|m1w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|mcr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|([1-8]|c))|phil|pire|pl(ay|uc)|pn2|po(ck|rt|se)|prox|psio|ptg|qaa|qc(07|12|21|32|60|[2-7]|i)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h|oo|p)|sdk\/|se(c(|0|1)|47|mc|nd|ri)|sgh|shar|sie(|m)|sk0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h|v|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl|tdg|tel(i|m)|tim|tmo|to(pl|sh)|ts(70|m|m3|m5)|tx9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas|your|zeto|zte/i;
return regex1.test(userAgent) || regex2.test(userAgent.substr(0, 4));
}
}

25
tests/Avalonia.IntegrationTests.Appium/PlatformFactAttribute.cs

@ -1,27 +1,34 @@
#nullable enable
using System;
using System.Linq;
using System.Runtime.InteropServices;
using Xunit;
namespace Avalonia.IntegrationTests.Appium
namespace Avalonia
{
[Flags]
internal enum TestPlatforms
{
Windows = 0x01,
MacOS = 0x02,
All = Windows | MacOS,
Linux = 0x04,
All = Windows | MacOS | Linux,
}
internal class PlatformFactAttribute : FactAttribute
{
public PlatformFactAttribute(TestPlatforms platforms = TestPlatforms.All) => Platforms = platforms;
private readonly string? _reason;
public PlatformFactAttribute(TestPlatforms platforms, string? reason = null)
{
_reason = reason;
Platforms = platforms;
}
public TestPlatforms Platforms { get; }
public override string? Skip
{
get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}";
get => IsSupported() ? null : $"Ignored on {RuntimeInformation.OSDescription}" + (_reason is not null ? $" reason: \"{_reason}\"" : "");
set => throw new NotSupportedException();
}
@ -31,6 +38,8 @@ namespace Avalonia.IntegrationTests.Appium
return Platforms.HasAnyFlag(TestPlatforms.Windows);
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
return Platforms.HasAnyFlag(TestPlatforms.MacOS);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
return Platforms.HasAnyFlag(TestPlatforms.Linux);
return false;
}
}

5
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -31,5 +31,10 @@
<AvaloniaResource Include="Xaml\XamlIlClassWithPrecompiledXaml.xaml" />
<AvaloniaResource Include="Xaml\XamlIlClassWithCustomProperty.xaml" />
</ItemGroup>
<ItemGroup>
<Compile Include="..\Avalonia.IntegrationTests.Appium\PlatformFactAttribute.cs">
<Link>PlatformFactAttribute.cs</Link>
</Compile>
</ItemGroup>
<Import Project="..\..\build\BuildTargets.targets" />
</Project>

71
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnFormFactorExtensionTests.cs

@ -0,0 +1,71 @@
using Avalonia.Controls;
using Avalonia.Platform;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
public class OnFormFactorExtensionTests : XamlTestBase
{
[Fact]
public void Should_Resolve_Default_Value()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(false, false));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnFormFactor Default=""Hello World""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal("Hello World", textBlock.Text);
}
}
[Theory]
[InlineData(false, true, "Im Mobile")]
[InlineData(true, false, "Im Desktop")]
[InlineData(false, false, "Default value")]
public void Should_Resolve_Expected_Value_Per_Platform(bool isDesktop, bool isMobile, string expectedResult)
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(isDesktop, isMobile));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnFormFactor ""Default value"",
Mobile=""Im Mobile"", Desktop=""Im Desktop""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal(expectedResult, textBlock.Text);
}
}
private class TestRuntimePlatform : StandardRuntimePlatform
{
private readonly bool _isDesktop;
private readonly bool _isMobile;
public TestRuntimePlatform(bool isDesktop, bool isMobile)
{
_isDesktop = isDesktop;
_isMobile = isMobile;
}
public override RuntimePlatformInfo GetRuntimeInfo()
{
return new RuntimePlatformInfo() { IsDesktop = _isDesktop, IsMobile = _isMobile };
}
}
}

75
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OnPlatformExtensionTests.cs

@ -0,0 +1,75 @@
using Avalonia.Controls;
using Avalonia.Platform;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
public class OnPlatformExtensionTests : XamlTestBase
{
[Fact]
public void Should_Resolve_Default_Value()
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(OperatingSystemType.Unknown));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnPlatform Default=""Hello World""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal("Hello World", textBlock.Text);
}
}
[Theory]
[InlineData(OperatingSystemType.WinNT, "Im Windows")]
[InlineData(OperatingSystemType.OSX, "Im macOS")]
[InlineData(OperatingSystemType.Linux, "Im Linux")]
[InlineData(OperatingSystemType.Android, "Im Android")]
[InlineData(OperatingSystemType.iOS, "Im iOS")]
[InlineData(OperatingSystemType.Browser, "Im Browser")]
[InlineData(OperatingSystemType.Unknown, "Default value")]
public void Should_Resolve_Expected_Value_Per_Platform(OperatingSystemType currentPlatform, string expectedResult)
{
using (AvaloniaLocator.EnterScope())
{
AvaloniaLocator.CurrentMutable.Bind<IRuntimePlatform>()
.ToConstant(new TestRuntimePlatform(currentPlatform));
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock Text='{OnPlatform ""Default value"",
Windows=""Im Windows"", macOS=""Im macOS"",
Linux=""Im Linux"", Android=""Im Android"",
iOS=""Im iOS"", Browser=""Im Browser""}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = (TextBlock)userControl.Content!;
Assert.Equal(expectedResult, textBlock.Text);
}
}
private class TestRuntimePlatform : StandardRuntimePlatform
{
private readonly OperatingSystemType _operatingSystemType;
public TestRuntimePlatform(OperatingSystemType operatingSystemType)
{
_operatingSystemType = operatingSystemType;
}
public override RuntimePlatformInfo GetRuntimeInfo()
{
return new RuntimePlatformInfo() { OperatingSystem = _operatingSystemType };
}
}
}

605
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/OptionsMarkupExtensionTests.cs

@ -0,0 +1,605 @@
using System;
using System.Reactive.Disposables;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Media;
using Avalonia.Metadata;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;
public class OptionsMarkupExtensionTests : XamlTestBase
{
public static Func<object, bool> RaisedOption;
public static int? ObjectsCreated;
[Fact]
public void Resolve_Default_Value()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtension Default=""Hello World""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal("Hello World", textBlock.Text);
}
[Fact]
public void Resolve_Default_Value_From_Ctor()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtension ""Hello World"", OptionB=""Im Android""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal("Hello World", textBlock.Text);
}
[Fact]
public void Resolve_Implicit_Default_Value_Ref_Type()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Tag='{local:OptionsMarkupExtension OptionA=""Hello World"", x:DataType=x:String}' />";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(null, userControl.Tag);
}
[Fact]
public void Resolve_Implicit_Default_Value_Val_Type()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Height='{local:OptionsMarkupExtension OptionA=10}' />";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(0d, userControl.Height);
}
[Fact]
public void Resolve_Implicit_Default_Value_Avalonia_Val_Type()
{
using var _ = SetupTestGlobals("default");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Margin='{local:OptionsMarkupExtension OptionA=10}' />";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(0), userControl.Margin);
}
[Theory]
[InlineData("option 1", "Im Option 1")]
[InlineData("option 2", "Im Option 2")]
[InlineData("3", "Im Option 3")]
[InlineData("unknown", "Default value")]
public void Resolve_Expected_Value_Per_Option(object option, string expectedResult)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtension ""Default value"",
OptionA=""Im Option 1"", OptionB=""Im Option 2"",
OptionNumber=""Im Option 3""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(expectedResult, textBlock.Text);
}
[Theory]
[InlineData("option 1", "Im Option 1")]
[InlineData("option 2", "Im Option 2")]
[InlineData("3", "Im Option 3")]
[InlineData("unknown", "Default value")]
public void Resolve_Expected_Value_Per_Option_Create_Single_Object(object option, string expectedResult)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<ContentControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<ContentControl.Content>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.Default>
<local:ChildObject Name=""Default value"" />
</local:OptionsMarkupExtension.Default>
<local:OptionsMarkupExtension.OptionA>
<local:ChildObject Name=""Im Option 1"" />
</local:OptionsMarkupExtension.OptionA>
<local:OptionsMarkupExtension.OptionB>
<local:ChildObject Name=""Im Option 2"" />
</local:OptionsMarkupExtension.OptionB>
<local:OptionsMarkupExtension.OptionNumber>
<local:ChildObject Name=""Im Option 3"" />
</local:OptionsMarkupExtension.OptionNumber>
</local:OptionsMarkupExtension>
</ContentControl.Content>
</ContentControl>";
var contentControl = (ContentControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var obj = Assert.IsType<ChildObject>(contentControl.Content);
Assert.Equal(expectedResult, obj.Name);
Assert.Equal(1, ObjectsCreated);
}
[Fact]
public void Convert_Bcl_Type()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Height='{local:OptionsMarkupExtension OptionA=50.1}' />";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(50.1, border.Height);
}
[Fact]
public void Convert_Avalonia_Type()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Padding='{local:OptionsMarkupExtension OptionA=""10, 8, 10, 8""}' />";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10, 8, 10, 8), border.Padding);
}
[PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux, "TypeArguments test is failing on macOS from SRE emit")]
public void Respect_Custom_TypeArgument()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Tag='{local:OptionsMarkupExtensionWithGeneric Default=20, OptionA=""10, 10, 10, 10"", x:TypeArguments=Thickness}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10, 10, 10, 10), textBlock.Tag);
}
[Fact]
public void Allow_Nester_Markup_Extensions()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
</UserControl.Resources>
<Border Background='{local:OptionsMarkupExtension OptionA={StaticResource brush}}'/>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var border = (Border)userControl.Content!;
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color);
}
[Fact]
public void Allow_Nester_On_Platform_Markup_Extensions()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Margin='{local:OptionsMarkupExtension OptionA={local:OptionsMarkupExtensionMinimal OptionA=""10,10,10,10""}}' />";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10), border.Margin);
}
[Fact]
public void Support_Xml_Syntax()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border.Background>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.OptionA>
<SolidColorBrush Color='#ff506070' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
</Border.Background>
</Border>";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(Color.Parse("#ff506070"), ((ISolidColorBrush)border.Background!).Color);
}
[PlatformFact(TestPlatforms.Windows | TestPlatforms.Linux, "TypeArguments test is failing on macOS from SRE emit")]
public void Support_Xml_Syntax_With_Custom_TypeArguments()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border.Tag>
<local:OptionsMarkupExtensionWithGeneric x:TypeArguments='Thickness' OptionA='10, 10, 10, 10' Default='20' />
</Border.Tag>
</Border>";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(new Thickness(10, 10, 10, 10), border.Tag);
}
[Theory]
[InlineData("option 1", "#ff506070")]
[InlineData("3", "#000")]
public void Support_Special_On_Syntax(object option, string color)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<Border xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Border.Background>
<local:OptionsMarkupExtension>
<On Options='OptionA, OptionB'>
<SolidColorBrush Color='#ff506070' />
</On>
<On Options=' OptionNumber '>
<SolidColorBrush Color='#000' />
</On>
</local:OptionsMarkupExtension>
</Border.Background>
</Border>";
var border = (Border)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(Color.Parse(color), ((ISolidColorBrush)border.Background!).Color);
}
[Fact]
public void Support_Control_Inside_Xml_Syntax()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.OptionA>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = (Button)userControl.Content!;
Assert.Equal("Hello World", button.Content);
}
[Fact]
public void Support_Default_Control_Inside_Xml_Syntax()
{
using var _ = SetupTestGlobals("unknown");
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.Default>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.Default>
</local:OptionsMarkupExtension>
</UserControl>";
var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = (Button)userControl.Content!;
Assert.Equal("Hello World", button.Content);
}
[Fact]
public void Support_Complex_Property_Setters_Dictionary()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<ResourceDictionary xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<Color x:Key='Color1'>Black</Color>
<local:OptionsMarkupExtension x:Key='MyKey'>
<local:OptionsMarkupExtension.OptionA>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
<Color x:Key='Color2'>White</Color>
</ResourceDictionary>";
var resourceDictionary = (ResourceDictionary)AvaloniaRuntimeXamlLoader.Load(xaml);
var button = Assert.IsType<Button>(resourceDictionary["MyKey"]);
Assert.Equal("Hello World", button.Content);
Assert.Equal(Colors.Black, resourceDictionary["Color1"]);
Assert.Equal(Colors.White, resourceDictionary["Color2"]);
}
[Fact]
public void Support_Complex_Property_Setters_List()
{
using var _ = SetupTestGlobals("option 1");
var xaml = @"
<Panel xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<TextBlock />
<local:OptionsMarkupExtension>
<local:OptionsMarkupExtension.OptionA>
<Button Content='Hello World' />
</local:OptionsMarkupExtension.OptionA>
</local:OptionsMarkupExtension>
<TextBox />
</Panel>";
var panel = (Panel)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal(3, panel.Children.Count);
Assert.IsType<Button>(panel.Children[1]);
}
[Theory]
[InlineData("option 1", "foo")]
[InlineData("option 2", "bar")]
public void BindingExtension_Works_Inside_Of_OptionsMarkupExtension(string option, string expected)
{
using var _ = SetupTestGlobals(option);
var xaml = @"
<UserControl xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
<UserControl.Resources>
<x:String x:Key='text'>foo</x:String>
</UserControl.Resources>
<TextBlock Name='textBlock' Text='{local:OptionsMarkupExtension OptionA={CompiledBinding Source={StaticResource text}}, OptionB=bar}'/>
</UserControl>";
var window = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
Assert.Equal(expected, textBlock.Text);
}
[Fact]
public void Resolve_Expected_Value_With_Method_Without_ServiceProvider()
{
using var _ = SetupTestGlobals(2);
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
Text='{local:OptionsMarkupExtensionNoServiceProvider OptionB=""Im Option 2""}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.Equal("Im Option 2", textBlock.Text);
}
[Fact]
public void Resolve_Expected_Value_Minimal_Extension()
{
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
IsVisible='{local:OptionsMarkupExtensionMinimal OptionA=True}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.True(textBlock.IsSet(Visual.IsVisibleProperty));
Assert.True(textBlock.IsVisible);
}
[Fact]
public void Resolve_Expected_Value_Extension_With_Property()
{
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
IsVisible='{local:OptionsMarkupExtensionWithProperty OptionA=True, Property=5}' />";
var textBlock = (TextBlock)AvaloniaRuntimeXamlLoader.Load(xaml);
Assert.True(textBlock.IsSet(Visual.IsVisibleProperty));
Assert.True(textBlock.IsVisible);
}
private static IDisposable SetupTestGlobals(object acceptedOption)
{
RaisedOption = o => o.Equals(acceptedOption);
ObjectsCreated = 0;
return Disposable.Create(() =>
{
RaisedOption = null;
ObjectsCreated = null;
});
}
}
public class OptionsMarkupExtension : OptionsMarkupExtensionBase<object, On>
{
public OptionsMarkupExtension()
{
}
public OptionsMarkupExtension(object defaultValue)
{
Default = defaultValue;
}
}
public class OptionsMarkupExtension<TReturn> : OptionsMarkupExtensionBase<TReturn, On<TReturn>>
{
public OptionsMarkupExtension()
{
}
public OptionsMarkupExtension(TReturn defaultValue)
{
Default = defaultValue;
}
}
public class OptionsMarkupExtensionBase<TReturn, TOn> : IAddChild<TOn>
where TOn : On<TReturn>
{
[MarkupExtensionOption("option 1")]
public TReturn OptionA { get; set; }
[MarkupExtensionOption("option 2")]
public TReturn OptionB { get; set; }
[MarkupExtensionOption(3)]
public TReturn OptionNumber { get; set; }
[Content]
[MarkupExtensionDefaultOption]
public TReturn Default { get; set; }
public bool ShouldProvideOption(IServiceProvider serviceProvider, string option)
{
return OptionsMarkupExtensionTests.RaisedOption(option);
}
public TReturn ProvideValue(IServiceProvider serviceProvider) { throw null; }
public void AddChild(TOn child) {}
}
public class OptionsMarkupExtensionNoServiceProvider
{
[MarkupExtensionOption(1)]
public object OptionA { get; set; }
[MarkupExtensionOption(2)]
public object OptionB { get; set; }
[Content]
[MarkupExtensionDefaultOption]
public object Default { get; set; }
public static bool ShouldProvideOption(int option)
{
return OptionsMarkupExtensionTests.RaisedOption(option);
}
public object ProvideValue(IServiceProvider serviceProvider) { throw null; }
}
public class OptionsMarkupExtensionMinimal
{
[MarkupExtensionOption(11.0)]
public bool OptionA { get; set; }
public static bool ShouldProvideOption(double option) => option > 0;
public object ProvideValue() { throw null; }
}
public class OptionsMarkupExtensionWithProperty
{
[MarkupExtensionOption(5)]
public bool OptionA { get; set; }
public int Property { get; set; }
public bool ShouldProvideOption(int option) => option == Property;
public object ProvideValue() { throw null; }
}
public class OptionsMarkupExtensionWithGeneric<TResult>
{
[MarkupExtensionOption("option 1")]
public TResult OptionA { get; set; }
[Content]
[MarkupExtensionDefaultOption]
public TResult Default { get; set; }
public bool ShouldProvideOption(string option)
{
return OptionsMarkupExtensionTests.RaisedOption(option);
}
public TResult ProvideValue() { throw null; }
}
public class ChildObject
{
public string Name { get; set; }
public ChildObject()
{
OptionsMarkupExtensionTests.ObjectsCreated++;
}
}
Loading…
Cancel
Save