Procházet zdrojové kódy

Merge branch 'master' into fixes/composition-keyframe-tests

Max Katz před 2 roky
rodič
revize
f157ebb5b0
48 změnil soubory, kde provedl 1375 přidání a 622 odebrání
  1. 2 2
      packages/Avalonia/AvaloniaBuildTasks.targets
  2. 10 4
      samples/BindingDemo/App.xaml
  3. 18 0
      samples/ControlCatalog/Pages/ExpanderPage.xaml
  4. 6 0
      samples/ControlCatalog/Pages/ExpanderPage.xaml.cs
  5. 9 2
      src/Avalonia.Base/Platform/AssetLoader.cs
  6. 1 1
      src/Avalonia.Base/Rect.cs
  7. 1 0
      src/Avalonia.Base/Utilities/StringBuilderCache.cs
  8. 3 0
      src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj
  9. 2 0
      src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs
  10. 18 4
      src/Avalonia.Build.Tasks/Extensions.cs
  11. 71 158
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  12. 230 21
      src/Avalonia.Controls/Expander.cs
  13. 1 1
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  14. 1 2
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  15. 8 7
      src/Avalonia.Themes.Fluent/Controls/Expander.xaml
  16. 22 28
      src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs
  17. 101 32
      src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs
  18. 46 5
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  19. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs
  20. 78 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs
  21. 173 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs
  22. 17 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/IXamlDocumentResource.cs
  23. 0 93
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs
  24. 9 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs
  25. 21 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs
  26. 40 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentResource.cs
  27. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  28. 1 0
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  29. 12 11
      src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs
  30. 1 1
      src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs
  31. 1 12
      src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs
  32. 70 0
      src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs
  33. 2 1
      src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs
  34. 2 1
      src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs
  35. 0 10
      src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
  36. 2 4
      src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs
  37. 2 4
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
  38. 58 50
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs
  39. 24 20
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs
  40. 25 19
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs
  41. 0 51
      tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs
  42. 1 21
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  43. 12 10
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs
  44. 1 2
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml
  45. 1 2
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style2.xaml
  46. 268 0
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs
  47. 0 39
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  48. 2 2
      tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs

+ 2 - 2
packages/Avalonia/AvaloniaBuildTasks.targets

@@ -74,7 +74,7 @@
       ReportImportance="$(AvaloniaXamlReportImportance)"/>
     <Exec 
       Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
-      Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
+      Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/>
 
   </Target>
 
@@ -112,7 +112,7 @@
     />
     <Exec
       Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
-      Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileAvaloniaXaml /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
+      Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:CompileAvaloniaXaml /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:RuntimeIdentifier=$(RuntimeIdentifier) /p:BuildProjectReferences=false"/>
   </Target>
 
   

+ 10 - 4
samples/BindingDemo/App.xaml

@@ -2,8 +2,14 @@
     xmlns="https://github.com/avaloniaui" 
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
     x:Class="BindingDemo.App">
-    <Application.Styles>
-        <FluentTheme />
-        <StyleInclude Source="avares://Avalonia.Themes.Simple/Accents/BaseLight.xaml"/>
-    </Application.Styles>
+  <Application.Resources>
+    <ResourceDictionary>
+      <ResourceDictionary.MergedDictionaries>
+        <ResourceInclude Source="avares://Avalonia.Themes.Simple/Accents/BaseLight.xaml"/>
+      </ResourceDictionary.MergedDictionaries>
+    </ResourceDictionary>
+  </Application.Resources>
+  <Application.Styles>
+    <FluentTheme />
+   </Application.Styles>
 </Application>

+ 18 - 0
samples/ControlCatalog/Pages/ExpanderPage.xaml

@@ -52,6 +52,24 @@
         </StackPanel>
       </Expander>
       <CheckBox IsChecked="{Binding Rounded}">Rounded</CheckBox>
+      <Expander x:Name="CollapsingDisabledExpander"
+                Header="Collapsing Disabled"
+                IsExpanded="True"
+                ExpandDirection="Down"
+                CornerRadius="{Binding CornerRadius}">
+        <StackPanel>
+          <TextBlock>Expanded content</TextBlock>
+        </StackPanel>
+      </Expander>
+      <Expander x:Name="ExpandingDisabledExpander"
+                Header="Expanding Disabled"
+                IsExpanded="False"
+                ExpandDirection="Down"
+                CornerRadius="{Binding CornerRadius}">
+        <StackPanel>
+          <TextBlock>Expanded content</TextBlock>
+        </StackPanel>
+      </Expander>
     </StackPanel>
   </StackPanel>
 </UserControl>

+ 6 - 0
samples/ControlCatalog/Pages/ExpanderPage.xaml.cs

@@ -10,6 +10,12 @@ namespace ControlCatalog.Pages
         {
             this.InitializeComponent();
             DataContext = new ExpanderPageViewModel();
+
+            var CollapsingDisabledExpander = this.Get<Expander>("CollapsingDisabledExpander");
+            var ExpandingDisabledExpander = this.Get<Expander>("ExpandingDisabledExpander");
+
+            CollapsingDisabledExpander.Collapsing += (s, e) => { e.Handled = true; };
+            ExpandingDisabledExpander.Expanding += (s, e) => { e.Handled = true; };
         }
 
         private void InitializeComponent()

+ 9 - 2
src/Avalonia.Base/Platform/AssetLoader.cs

@@ -3,16 +3,22 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+#if !BUILDTASK
 using Avalonia.Platform.Internal;
 using Avalonia.Utilities;
+#endif
 
 namespace Avalonia.Platform
 {
     /// <summary>
     /// Loads assets compiled into the application binary.
     /// </summary>
-    public class AssetLoader : IAssetLoader
+    public class AssetLoader
+#if !BUILDTASK
+        : IAssetLoader
+#endif
     {
+#if !BUILDTASK
         private static IAssemblyDescriptorResolver s_assemblyDescriptorResolver = new AssemblyDescriptorResolver();
 
         private AssemblyDescriptor? _defaultResmAssembly;
@@ -206,7 +212,8 @@ namespace Avalonia.Platform
 
             return null;
         }
-
+#endif
+        
         public static void RegisterResUriParsers()
         {
             if (!UriParser.IsKnownScheme("avares"))

+ 1 - 1
src/Avalonia.Base/Rect.cs

@@ -243,7 +243,7 @@ namespace Avalonia
         }
 
         /// <summary>
-        /// Determines whether a point in in the bounds of the rectangle.
+        /// Determines whether a point is in the bounds of the rectangle.
         /// </summary>
         /// <param name="p">The point.</param>
         /// <returns>true if the point is in the bounds of the rectangle; otherwise false.</returns>

+ 1 - 0
src/Avalonia.Base/Utilities/StringBuilderCache.cs

@@ -9,6 +9,7 @@ using System;
 using System.Text;
 
 namespace Avalonia.Utilities;
+#nullable enable
 
 // <summary>Provide a cached reusable instance of stringbuilder per thread.</summary>
 internal static class StringBuilderCache

+ 3 - 0
src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj

@@ -55,6 +55,9 @@
       <Compile Include="../Avalonia.Base/Utilities/MathUtilities.cs">
         <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>
       </Compile>
+      <Compile Include="..\Avalonia.Base\Platform\AssetLoader.cs">
+        <Link>Markup\AssetLoader.cs</Link>
+      </Compile>
       <Compile Include="..\Avalonia.Base\Utilities\StringBuilderCache.cs" Link="Utilities\StringBuilderCache.cs" />
       <Compile Include="..\Markup\Avalonia.Markup\Markup\Parsers\ArgumentListParser.cs">
         <Link>Markup/%(RecursiveDir)%(FileName)%(Extension)</Link>

+ 2 - 0
src/Avalonia.Build.Tasks/BuildEngineErrorCode.cs

@@ -5,6 +5,8 @@ namespace Avalonia.Build.Tasks
         InvalidXAML = 1,
         DuplicateXClass = 2,
         LegacyResmScheme = 3,
+        TransformError = 4,
+        EmitError = 4,
 
         Unknown = 9999
     }

+ 18 - 4
src/Avalonia.Build.Tasks/Extensions.cs

@@ -7,15 +7,29 @@ namespace Avalonia.Build.Tasks
     {
         static string FormatErrorCode(BuildEngineErrorCode code) => $"AVLN:{(int)code:0000}";
 
-        public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
+        public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, Exception ex,
+            int lineNumber = 0, int linePosition = 0)
         {
-            engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
+#if DEBUG
+            LogError(engine, code, file, ex.ToString(), lineNumber, linePosition);
+#else
+            LogError(engine, code, file, ex.Message, lineNumber, linePosition);
+#endif
+        }
+        
+        public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message,
+            int lineNumber = 0, int linePosition = 0)
+        {
+            engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "",
+                lineNumber, linePosition, lineNumber, linePosition, message,
                 "", "Avalonia"));
         }
 
-        public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
+        public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message,
+            int lineNumber = 0, int linePosition = 0)
         {
-            engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
+            engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "",
+                lineNumber, linePosition, lineNumber, linePosition, message,
                 "", "Avalonia"));
         }
 

+ 71 - 158
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
+using Avalonia.Platform;
 using Microsoft.Build.Framework;
 using Mono.Cecil;
 using Mono.Cecil.Cil;
@@ -94,7 +95,7 @@ namespace Avalonia.Build.Tasks
             }
             catch (Exception ex)
             {
-                engine.LogError(BuildEngineErrorCode.Unknown, "", ex.Message);
+                engine.LogError(BuildEngineErrorCode.Unknown, "", ex);
                 return new CompileResult(false);
             }
         }
@@ -134,6 +135,10 @@ namespace Avalonia.Build.Tasks
                     engine.LogMessage("Debugging cancelled.", MessageImportance.Normal);
                 }
             }
+            
+            // Some transformers might need to parse "avares://" Uri.
+            AssetLoader.RegisterResUriParsers();
+
             var asm = typeSystem.TargetAssemblyDefinition;
             var avares = new AvaloniaResources(asm, projectDirectory);
             if (avares.Resources.Count(CheckXamlName) == 0)
@@ -231,15 +236,14 @@ namespace Avalonia.Build.Tasks
                 });
                 asm.MainModule.Types.Add(typeDef);
                 var builder = typeSystem.CreateTypeBuilder(typeDef);
-
-                var populateMethodsToTransform = new List<(MethodDefinition populateMethod, string resourceFilePath)>();
-
-                foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x=>x.FilePath.ToLowerInvariant()))
+                
+                IReadOnlyCollection<XamlDocumentResource> parsedXamlDocuments = new List<XamlDocumentResource>();
+                foreach (var res in group.Resources.Where(CheckXamlName).OrderBy(x => x.FilePath.ToLowerInvariant()))
                 {
+                    engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
+
                     try
                     {
-                        engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
-
                         // StreamReader is needed here to handle BOM
                         var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
                         var parsed = XDocumentXamlParser.Parse(xaml);
@@ -276,9 +280,9 @@ namespace Avalonia.Build.Tasks
                         
                         
                         compiler.Transform(parsed);
+
                         var populateName = classType == null ? "Populate:" + res.Name : "!XamlIlPopulate";
-                        var buildName = classType == null ? "Build:" + res.Name : null; 
-                        
+                        var buildName = classType == null ? "Build:" + res.Name : null;
                         var classTypeDefinition =
                             classType == null ? null : typeSystem.GetTypeReference(classType).Resolve();
 
@@ -286,11 +290,57 @@ namespace Avalonia.Build.Tasks
                         var populateBuilder = classTypeDefinition == null ?
                             builder :
                             typeSystem.CreateTypeBuilder(classTypeDefinition);
-                        compiler.Compile(parsed, 
-                            contextClass,
+
+                        ((List<XamlDocumentResource>)parsedXamlDocuments).Add(new XamlDocumentResource(
+                            parsed, res.Uri, res, classType,
+                            populateBuilder,
                             compiler.DefinePopulateMethod(populateBuilder, parsed, populateName,
                                 classTypeDefinition == null),
-                            buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true),
+                            buildName == null ? null : compiler.DefineBuildMethod(builder, parsed, buildName, true)));
+                    }
+                    catch (Exception e)
+                    {
+                        int lineNumber = 0, linePosition = 0;
+                        if (e is XamlParseException xe)
+                        {
+                            lineNumber = xe.LineNumber;
+                            linePosition = xe.LinePosition;
+                        }
+
+                        engine.LogError(BuildEngineErrorCode.TransformError, res.FilePath, e, lineNumber, linePosition);
+                        return false;
+                    }
+                }
+
+                try
+                {
+                    compiler.TransformGroup(parsedXamlDocuments);
+                }
+                catch (XamlDocumentParseException e)
+                {
+                    engine.LogError(BuildEngineErrorCode.TransformError, e.FilePath, e, e.LineNumber, e.LinePosition);
+                }
+                catch (XamlParseException e)
+                {
+                    engine.LogError(BuildEngineErrorCode.TransformError, "", e, e.LineNumber, e.LinePosition);
+                }
+
+                foreach (var document in parsedXamlDocuments)
+                {
+                    var parsed = document.XamlDocument;
+                    var res = (IResource)document.FileSource;
+                    var classType = document.ClassType;
+                    var populateBuilder = document.TypeBuilder;
+
+                    try
+                    {
+                        var classTypeDefinition =
+                            classType == null ? null : typeSystem.GetTypeReference(classType).Resolve();
+
+                        compiler.Compile(parsed, 
+                            contextClass,
+                            document.PopulateMethod,
+                            document.BuildMethod,
                             builder.DefineSubType(compilerConfig.WellKnownTypes.Object, "NamespaceInfo:" + res.Name,
                                 true),
                             (closureName, closureBaseType) =>
@@ -300,23 +350,11 @@ namespace Avalonia.Build.Tasks
                             res.Uri, res
                         );
 
-                        var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve()
-                            .Methods.First(m => m.Name == populateName);
-
-                        // Include populate method and all nested methods/closures used in the populate method,
-                        // So we can replace style/resource includes in all of them. 
-                        populateMethodsToTransform.Add((compiledPopulateMethod, res.FilePath));
-                        populateMethodsToTransform.AddRange(compiledPopulateMethod.Body.Instructions
-                            .Where(b => b.OpCode == OpCodes.Call || b.OpCode == OpCodes.Callvirt || b.OpCode == OpCodes.Ldftn)
-                            .Select(b => b.Operand)
-                            .OfType<MethodReference>()
-                            .Where(m => compiledPopulateMethod.DeclaringType.NestedTypes.Contains(m.DeclaringType))
-                            .Select(m => m.Resolve())
-                            .Where(m => m.HasBody)
-                            .Select(m => (m, res.FilePath)));
-
                         if (classTypeDefinition != null)
                         {
+                            var compiledPopulateMethod = typeSystem.GetTypeReference(populateBuilder).Resolve()
+                                .Methods.First(m => m.Name == document.PopulateMethod.Name);
+
                             var designLoaderFieldType = typeSystem
                                 .GetType("System.Action`1")
                                 .MakeGenericType(typeSystem.GetType("System.Object"));
@@ -370,15 +408,11 @@ namespace Avalonia.Build.Tasks
                                     if (i[c].OpCode == OpCodes.Call)
                                     {
                                         var op = i[c].Operand as MethodReference;
-                                        
-                                        // TODO: Throw an error
-                                        // This usually happens when the same XAML resource was added twice for some weird reason
-                                        // We currently support it for dual-named default theme resources
                                         if (op != null
                                             && op.Name == TrampolineName)
                                         {
-                                            foundXamlLoader = true;
-                                            break;
+                                            throw new InvalidProgramException("Same XAML file was loaded twice." +
+                                                "Make sure there is no x:Class duplicates no files were added to the AvaloniaResource msbuild items group twice.");
                                         }
                                         if (op != null
                                             && op.Name == "Load"
@@ -417,12 +451,12 @@ namespace Avalonia.Build.Tasks
 
                         }
 
-                        if (buildName != null || classTypeDefinition != null)
+                        if (document.BuildMethod != null || classTypeDefinition != null)
                         {
-                            var compiledBuildMethod = buildName == null ?
+                            var compiledBuildMethod = document.BuildMethod == null ?
                                 null :
                                 typeSystem.GetTypeReference(builder).Resolve()
-                                    .Methods.First(m => m.Name == buildName);
+                                    .Methods.First(m => m.Name == document.BuildMethod?.Name);
                             var parameterlessConstructor = compiledBuildMethod != null ?
                                 null :
                                 classTypeDefinition.GetConstructors().FirstOrDefault(c =>
@@ -459,22 +493,12 @@ namespace Avalonia.Build.Tasks
                             lineNumber = xe.LineNumber;
                             linePosition = xe.LinePosition;
                         }
-                        engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", res.FilePath,
-                            lineNumber, linePosition, lineNumber, linePosition,
-                            e.Message, "", "Avalonia"));
+                        engine.LogError(BuildEngineErrorCode.EmitError, res.FilePath, e, lineNumber, linePosition);
                         return false;
                     }
                     res.Remove();
                 }
 
-                foreach (var (populateMethod, resourceFilePath) in populateMethodsToTransform)
-                {
-                    if (!TransformXamlIncludes(engine, typeSystem, populateMethod, resourceFilePath, createRootServiceProviderMethod))
-                    {
-                        return false;
-                    }
-                }
-
                 // Technically that's a hack, but it fixes corert incompatibility caused by deterministic builds
                 int dupeCounter = 1;
                 foreach (var grp in typeDef.NestedTypes.GroupBy(x => x.Name))
@@ -551,116 +575,5 @@ namespace Avalonia.Build.Tasks
 
             return true;
         }
-        
-        private static bool TransformXamlIncludes(
-            IBuildEngine engine, CecilTypeSystem typeSystem,
-            MethodDefinition populateMethod, string resourceFilePath,
-            MethodReference createRootServiceProviderMethod)
-        {
-            var asm = typeSystem.TargetAssemblyDefinition;
-            foreach (var instruction in populateMethod.Body.Instructions.ToArray())
-            {
-                const string resolveStyleIncludeName = "ResolveStyleInclude";
-                const string resolveResourceInclude = "ResolveResourceInclude";
-                if (instruction.OpCode == OpCodes.Call
-                    && instruction.Operand is MethodReference
-                    {
-                        Name: resolveStyleIncludeName or resolveResourceInclude,
-                        DeclaringType: { Name: "XamlIlRuntimeHelpers" }
-                    })
-                {
-                    int lineNumber = 0, linePosition = 0;
-                    bool instructionsModified = false;
-                    try
-                    {
-                        var assetSource = (string)instruction.Previous.Previous.Previous.Operand;
-                        lineNumber = GetConstValue(instruction.Previous.Previous);
-                        linePosition = GetConstValue(instruction.Previous);
-
-                        var index = populateMethod.Body.Instructions.IndexOf(instruction);
-
-                        assetSource = assetSource.Replace("avares://", "");
-
-                        var assemblyNameSeparator = assetSource.IndexOf('/');
-                        var fileNameSeparator = assetSource.LastIndexOf('/');
-                        if (assemblyNameSeparator < 0 || fileNameSeparator < 0)
-                        {
-                            throw new InvalidProgramException(
-                                $"Invalid asset source \"{assetSource}\". It must contain assembly name and relative path.");
-                        }
-
-                        var assetAssemblyName = assetSource.Substring(0, assemblyNameSeparator);
-                        var assetAssembly = typeSystem.FindAssembly(assetAssemblyName)
-                                            ?? throw new InvalidProgramException(
-                                                $"Unable to resolve assembly \"{assetAssemblyName}\"");
-
-                        var fileName = Path.GetFileNameWithoutExtension(assetSource.Replace('/', '.'));
-                        if (assetAssembly.FindType(fileName) is { } type
-                            && type.FindConstructor() is { } ctor)
-                        {
-                            var ctorMethod =
-                                asm.MainModule.ImportReference(typeSystem.GetMethodReference(ctor));
-                            instructionsModified = true;
-                            populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop);
-                            populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop);
-                            populateMethod.Body.Instructions[index - 1] = Instruction.Create(OpCodes.Nop);
-                            populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Newobj, ctorMethod);
-                        }
-                        else
-                        {
-                            var resources = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources")
-                                            ?? throw new InvalidOperationException(
-                                                $"Unable to resolve \"!AvaloniaResources\" type on \"{assetAssemblyName}\" assembly");
-
-                            var relativeName = "Build:" + assetSource.Substring(assemblyNameSeparator);
-                            var buildMethod = resources.FindMethod(m => m.Name == relativeName)
-                                              ?? throw new InvalidOperationException(
-                                                  $"Unable to resolve build method \"{relativeName}\" resource on the \"{assetAssemblyName}\" assembly");
-
-                            var methodReference = asm.MainModule.ImportReference(typeSystem.GetMethodReference(buildMethod));
-                            instructionsModified = true;
-                            populateMethod.Body.Instructions[index - 3] = Instruction.Create(OpCodes.Nop);
-                            populateMethod.Body.Instructions[index - 2] = Instruction.Create(OpCodes.Nop);
-                            populateMethod.Body.Instructions[index - 1] =
-                                Instruction.Create(OpCodes.Call, createRootServiceProviderMethod);
-                            populateMethod.Body.Instructions[index] = Instruction.Create(OpCodes.Call, methodReference);
-                        }
-                    }
-                    catch (Exception e)
-                    {
-                        if (instructionsModified)
-                        {
-                            engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", "XAMLIL", resourceFilePath,
-                                lineNumber, linePosition, lineNumber, linePosition,
-                                e.Message, "", "Avalonia"));
-                            return false;
-                        }
-                        else
-                        {
-                            engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", "XAMLIL",
-                                resourceFilePath,
-                                lineNumber, linePosition, lineNumber, linePosition,
-                                e.Message, "", "Avalonia"));
-                        }
-                    }
-                    
-                    static int GetConstValue(Instruction instruction)
-                    {
-                        if (instruction.OpCode is { Code : >= Code.Ldc_I4_0 and <= Code.Ldc_I4_8 })
-                        {
-                            return instruction.OpCode.Code - Code.Ldc_I4_0;
-                        }
-                        if (instruction.Operand is not null)
-                        {
-                            return Convert.ToInt32(instruction.Operand);
-                        }
-
-                        return 0;
-                    }
-                }
-            }
-
-            return true;
-        }
     }
 }

+ 230 - 21
src/Avalonia.Controls/Expander.cs

@@ -1,7 +1,11 @@
+using System;
 using System.Threading;
 using Avalonia.Animation;
 using Avalonia.Controls.Metadata;
 using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
 
 namespace Avalonia.Controls
 {
@@ -37,12 +41,24 @@ namespace Avalonia.Controls
     [PseudoClasses(":expanded", ":up", ":down", ":left", ":right")]
     public class Expander : HeaderedContentControl
     {
+        /// <summary>
+        /// Defines the <see cref="ContentTransition"/> property.
+        /// </summary>
         public static readonly StyledProperty<IPageTransition?> ContentTransitionProperty =
-            AvaloniaProperty.Register<Expander, IPageTransition?>(nameof(ContentTransition));
+            AvaloniaProperty.Register<Expander, IPageTransition?>(
+                nameof(ContentTransition));
 
+        /// <summary>
+        /// Defines the <see cref="ExpandDirection"/> property.
+        /// </summary>
         public static readonly StyledProperty<ExpandDirection> ExpandDirectionProperty =
-            AvaloniaProperty.Register<Expander, ExpandDirection>(nameof(ExpandDirection), ExpandDirection.Down);
+            AvaloniaProperty.Register<Expander, ExpandDirection>(
+                nameof(ExpandDirection),
+                ExpandDirection.Down);
 
+        /// <summary>
+        /// Defines the <see cref="IsExpanded"/> property.
+        /// </summary>
         public static readonly DirectProperty<Expander, bool> IsExpandedProperty =
             AvaloniaProperty.RegisterDirect<Expander, bool>(
                 nameof(IsExpanded),
@@ -50,47 +66,206 @@ namespace Avalonia.Controls
                 (o, v) => o.IsExpanded = v,
                 defaultBindingMode: Data.BindingMode.TwoWay);
 
+        /// <summary>
+        /// Defines the <see cref="Collapsed"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> CollapsedEvent =
+            RoutedEvent.Register<Expander, RoutedEventArgs>(
+                nameof(Collapsed),
+                RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Defines the <see cref="Collapsing"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> CollapsingEvent =
+            RoutedEvent.Register<Expander, RoutedEventArgs>(
+                nameof(Collapsing),
+                RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Defines the <see cref="Expanded"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> ExpandedEvent =
+            RoutedEvent.Register<Expander, RoutedEventArgs>(
+                nameof(Expanded),
+                RoutingStrategies.Bubble);
+
+        /// <summary>
+        /// Defines the <see cref="Expanding"/> event.
+        /// </summary>
+        public static readonly RoutedEvent<RoutedEventArgs> ExpandingEvent =
+            RoutedEvent.Register<Expander, RoutedEventArgs>(
+                nameof(Expanding),
+                RoutingStrategies.Bubble);
+
+        private bool _ignorePropertyChanged = false;
         private bool _isExpanded;
         private CancellationTokenSource? _lastTransitionCts;
 
-        static Expander()
-        {
-            IsExpandedProperty.Changed.AddClassHandler<Expander>((x, e) => x.OnIsExpandedChanged(e));
-        }
-
+        /// <summary>
+        /// Initializes a new instance of the <see cref="Expander"/> class.
+        /// </summary>
         public Expander()
         {
-            UpdatePseudoClasses(ExpandDirection);
+            UpdatePseudoClasses();
         }
 
+        /// <summary>
+        /// Gets or sets the transition used when expanding or collapsing the content.
+        /// </summary>
         public IPageTransition? ContentTransition
         {
             get => GetValue(ContentTransitionProperty);
             set => SetValue(ContentTransitionProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets the direction in which the <see cref="Expander"/> opens.
+        /// </summary>
         public ExpandDirection ExpandDirection
         {
             get => GetValue(ExpandDirectionProperty);
             set => SetValue(ExpandDirectionProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the <see cref="Expander"/>
+        /// content area is open and visible.
+        /// </summary>
         public bool IsExpanded
         {
-            get { return _isExpanded; }
-            set 
-            { 
-                SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
-                PseudoClasses.Set(":expanded", value);
+            get => _isExpanded;
+            set
+            {
+                // It is important here that IsExpanded is a direct property so events can be invoked
+                // BEFORE the property system gets notified of updated values. This is because events
+                // may be canceled by external code.
+                if (_isExpanded != value)
+                {
+                    RoutedEventArgs eventArgs;
+
+                    if (value)
+                    {
+                        eventArgs = new RoutedEventArgs(ExpandingEvent, this);
+                        OnExpanding(eventArgs);
+                    }
+                    else
+                    {
+                        eventArgs = new RoutedEventArgs(CollapsingEvent, this);
+                        OnCollapsing(eventArgs);
+                    }
+
+                    if (eventArgs.Handled)
+                    {
+                        // If the event was externally handled (canceled) we must still notify the value has changed.
+                        // This property changed notification will update any external code observing this property that itself may have set the new value.
+                        // We are essentially reverted any external state change along with ignoring the IsExpanded property set.
+                        // Remember IsExpanded is usually controlled by a ToggleButton in the control theme.
+                        _ignorePropertyChanged = true;
+
+                        RaisePropertyChanged(
+                            IsExpandedProperty,
+                            oldValue: value,
+                            newValue: _isExpanded,
+                            BindingPriority.LocalValue,
+                            isEffectiveValue: true);
+
+                        _ignorePropertyChanged = false;
+                    }
+                    else
+                    {
+                        SetAndRaise(IsExpandedProperty, ref _isExpanded, value);
+                    }
+                }
             }
         }
 
-        protected virtual async void OnIsExpandedChanged(AvaloniaPropertyChangedEventArgs e)
+        /// <summary>
+        /// Occurs after the content area has closed and only the header is visible.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? Collapsed
+        {
+            add => AddHandler(CollapsedEvent, value);
+            remove => RemoveHandler(CollapsedEvent, value);
+        }
+
+        /// <summary>
+        /// Occurs as the content area is closing.
+        /// </summary>
+        /// <remarks>
+        /// The event args <see cref="RoutedEventArgs.Handled"/> property may be set to true to cancel the event
+        /// and keep the control open (expanded).
+        /// </remarks>
+        public event EventHandler<RoutedEventArgs>? Collapsing
+        {
+            add => AddHandler(CollapsingEvent, value);
+            remove => RemoveHandler(CollapsingEvent, value);
+        }
+
+        /// <summary>
+        /// Occurs after the <see cref="Expander"/> has opened to display both its header and content.
+        /// </summary>
+        public event EventHandler<RoutedEventArgs>? Expanded
+        {
+            add => AddHandler(ExpandedEvent, value);
+            remove => RemoveHandler(ExpandedEvent, value);
+        }
+
+        /// <summary>
+        /// Occurs as the content area is opening.
+        /// </summary>
+        /// <remarks>
+        /// The event args <see cref="RoutedEventArgs.Handled"/> property may be set to true to cancel the event
+        /// and keep the control closed (collapsed).
+        /// </remarks>
+        public event EventHandler<RoutedEventArgs>? Expanding
+        {
+            add => AddHandler(ExpandingEvent, value);
+            remove => RemoveHandler(ExpandingEvent, value);
+        }
+
+        /// <summary>
+        /// Invoked just before the <see cref="Collapsed"/> event.
+        /// </summary>
+        protected virtual void OnCollapsed(RoutedEventArgs eventArgs)
+        {
+            RaiseEvent(eventArgs);
+        }
+
+        /// <summary>
+        /// Invoked just before the <see cref="Collapsing"/> event.
+        /// </summary>
+        protected virtual void OnCollapsing(RoutedEventArgs eventArgs)
+        {
+            RaiseEvent(eventArgs);
+        }
+
+        /// <summary>
+        /// Invoked just before the <see cref="Expanded"/> event.
+        /// </summary>
+        protected virtual void OnExpanded(RoutedEventArgs eventArgs)
+        {
+            RaiseEvent(eventArgs);
+        }
+
+        /// <summary>
+        /// Invoked just before the <see cref="Expanding"/> event.
+        /// </summary>
+        protected virtual void OnExpanding(RoutedEventArgs eventArgs)
+        {
+            RaiseEvent(eventArgs);
+        }
+
+        /// <summary>
+        /// Starts the content transition (if set) and invokes the <see cref="Expanded"/>
+        /// and <see cref="Collapsed"/> events when completed.
+        /// </summary>
+        private async void StartContentTransition()
         {
             if (Content != null && ContentTransition != null && Presenter is Visual visualContent)
             {
                 bool forward = ExpandDirection == ExpandDirection.Left ||
-                                ExpandDirection == ExpandDirection.Up;
+                               ExpandDirection == ExpandDirection.Up;
 
                 _lastTransitionCts?.Cancel();
                 _lastTransitionCts = new CancellationTokenSource();
@@ -104,24 +279,58 @@ namespace Avalonia.Controls
                     await ContentTransition.Start(visualContent, null, forward, _lastTransitionCts.Token);
                 }
             }
+
+            // Expanded/Collapsed events are invoked asynchronously to ensure other events,
+            // such as Click, have time to complete first.
+            Dispatcher.UIThread.Post(() =>
+            {
+                if (IsExpanded)
+                {
+                    OnExpanded(new RoutedEventArgs(ExpandedEvent, this));
+                }
+                else
+                {
+                    OnCollapsed(new RoutedEventArgs(CollapsedEvent, this));
+                }
+            });
         }
 
+        /// <inheritdoc/>
         protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
         {
             base.OnPropertyChanged(change);
 
+            if (_ignorePropertyChanged)
+            {
+                return;
+            }
+
             if (change.Property == ExpandDirectionProperty)
             {
-                UpdatePseudoClasses(change.GetNewValue<ExpandDirection>());
+                UpdatePseudoClasses();
+            }
+            else if (change.Property == IsExpandedProperty)
+            {
+                // Expanded/Collapsed will be raised once transitions are complete
+                StartContentTransition();
+
+                UpdatePseudoClasses();
             }
         }
 
-        private void UpdatePseudoClasses(ExpandDirection d)
+        /// <summary>
+        /// Updates the visual state of the control by applying latest PseudoClasses.
+        /// </summary>
+        private void UpdatePseudoClasses()
         {
-            PseudoClasses.Set(":up", d == ExpandDirection.Up);
-            PseudoClasses.Set(":down", d == ExpandDirection.Down);
-            PseudoClasses.Set(":left", d == ExpandDirection.Left);
-            PseudoClasses.Set(":right", d == ExpandDirection.Right);
+            var expandDirection = ExpandDirection;
+
+            PseudoClasses.Set(":up", expandDirection == ExpandDirection.Up);
+            PseudoClasses.Set(":down", expandDirection == ExpandDirection.Down);
+            PseudoClasses.Set(":left", expandDirection == ExpandDirection.Left);
+            PseudoClasses.Set(":right", expandDirection == ExpandDirection.Right);
+
+            PseudoClasses.Set(":expanded", IsExpanded);
         }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Controls
                 }
             }
 
-            base.OnKeyDown(e);            
+            base.OnKeyDown(e);
         }
     }
 }

+ 1 - 2
src/Avalonia.DesignerSupport/DesignWindowLoader.cs

@@ -38,10 +38,9 @@ namespace Avalonia.DesignerSupport
                 var useCompiledBindings = localAsm?.GetCustomAttributes<AssemblyMetadataAttribute>()
                     .FirstOrDefault(a => a.Key == "AvaloniaUseCompiledBindingsByDefault")?.Value;
 
-                var loaded = loader.Load(stream, new RuntimeXamlLoaderConfiguration
+                var loaded = loader.Load(new RuntimeXamlLoaderDocument(baseUri, stream), new RuntimeXamlLoaderConfiguration
                 {
                     LocalAssembly = localAsm,
-                    BaseUri = baseUri,
                     DesignMode = true,
                     UseCompiledBindingsByDefault = bool.TryParse(useCompiledBindings, out var parsedValue ) && parsedValue 
                 });

+ 8 - 7
src/Avalonia.Themes.Fluent/Controls/Expander.xaml

@@ -103,17 +103,18 @@
                     RenderTransformOrigin="50%,50%"
                     Stretch="None"
                     Stroke="{DynamicResource ExpanderChevronForeground}"
-                    StrokeThickness="1" />
-              <Border.RenderTransform>
-                <RotateTransform />
-              </Border.RenderTransform>
+                    StrokeThickness="1">
+                <Path.RenderTransform>
+                  <RotateTransform />
+                </Path.RenderTransform>
+              </Path>
             </Border>
           </Grid>
         </Border>
       </ControlTemplate>
     </Setter>
 
-    <Style Selector="^:checked /template/ Border#ExpandCollapseChevronBorder">
+    <Style Selector="^:checked /template/ Path#ExpandCollapseChevron">
       <Style.Animations>
         <Animation FillMode="Both" Duration="0:0:0.0625">
           <KeyFrame Cue="100%">
@@ -122,8 +123,8 @@
         </Animation>
       </Style.Animations>
     </Style>
-    
-    <Style Selector="^:not(:checked) /template/ Border#ExpandCollapseChevronBorder">
+
+    <Style Selector="^:not(:checked) /template/ Path#ExpandCollapseChevron">
       <Style.Animations>
         <Animation FillMode="Both" Duration="0:0:0.0625">
           <KeyFrame Cue="0%">

+ 22 - 28
src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs

@@ -1,9 +1,10 @@
 using System;
+using System.Collections.Generic;
 using System.IO;
 using System.Reflection;
 using System.Text;
 using Avalonia.Markup.Xaml.XamlIl;
-
+#nullable enable
 namespace Avalonia.Markup.Xaml
 {
     public static class AvaloniaRuntimeXamlLoader
@@ -17,31 +18,15 @@ namespace Avalonia.Markup.Xaml
         /// <param name="uri">The URI of the XAML being loaded.</param>
         /// <param name="designMode">Indicates whether the XAML is being loaded in design mode.</param>
         /// <returns>The loaded object.</returns>
-        public static object Load(string xaml, Assembly localAssembly = null, object rootInstance = null, Uri uri = null, bool designMode = false)
+        public static object Load(string xaml, Assembly? localAssembly = null, object? rootInstance = null, Uri? uri = null, bool designMode = false)
         {
-            Contract.Requires<ArgumentNullException>(xaml != null);
+            xaml = xaml ?? throw new ArgumentNullException(nameof(xaml));
 
             using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)))
             {
                 return Load(stream, localAssembly, rootInstance, uri, designMode);
             }
         }
-        
-        /// <summary>
-        /// Loads XAML from a string.
-        /// </summary>
-        /// <param name="xaml">The string containing the XAML.</param>
-        /// <param name="configuration">Xaml loader configuration.</param>
-        /// <returns>The loaded object.</returns>
-        public static object Load(string xaml, RuntimeXamlLoaderConfiguration configuration)
-        {
-            Contract.Requires<ArgumentNullException>(xaml != null);
-
-            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)))
-            {
-                return Load(stream, configuration);
-            }
-        }
 
         /// <summary>
         /// Loads XAML from a stream.
@@ -52,19 +37,28 @@ namespace Avalonia.Markup.Xaml
         /// <param name="uri">The URI of the XAML being loaded.</param>
         /// <param name="designMode">Indicates whether the XAML is being loaded in design mode.</param>
         /// <returns>The loaded object.</returns>
-        public static object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null,
+        public static object Load(Stream stream, Assembly? localAssembly = null, object? rootInstance = null, Uri? uri = null,
             bool designMode = false)
-            => AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, designMode, false);
-        
+            => AvaloniaXamlIlRuntimeCompiler.Load(new RuntimeXamlLoaderDocument(uri, rootInstance, stream),
+                new RuntimeXamlLoaderConfiguration { DesignMode = designMode, LocalAssembly = localAssembly });
+
         /// <summary>
         /// Loads XAML from a stream.
         /// </summary>
-        /// <param name="stream">The stream containing the XAML.</param>
+        /// <param name="document">The stream containing the XAML.</param>
         /// <param name="configuration">Xaml loader configuration.</param>
         /// <returns>The loaded object.</returns>
-        public static object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration)
-            => AvaloniaXamlIlRuntimeCompiler.Load(stream, configuration.LocalAssembly, configuration.RootInstance,
-                configuration.BaseUri, configuration.DesignMode, configuration.UseCompiledBindingsByDefault);
+        public static object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration? configuration = null)
+            => AvaloniaXamlIlRuntimeCompiler.Load(document, configuration ?? new RuntimeXamlLoaderConfiguration());
+
+        /// <summary>
+        /// Loads group of XAML files from a stream.
+        /// </summary>
+        /// <param name="documents">Collection of documents.</param>
+        /// <param name="configuration">Xaml loader configuration.</param>
+        /// <returns>The loaded objects per each input document.</returns>
+        public static IReadOnlyList<object> LoadGroup(IReadOnlyCollection<RuntimeXamlLoaderDocument> documents, RuntimeXamlLoaderConfiguration? configuration = null)
+            => AvaloniaXamlIlRuntimeCompiler.LoadGroup(documents, configuration ?? new RuntimeXamlLoaderConfiguration());
 
         /// <summary>
         /// Parse XAML from a string.
@@ -72,7 +66,7 @@ namespace Avalonia.Markup.Xaml
         /// <param name="xaml">The string containing the XAML.</param>
         /// <param name="localAssembly">Default assembly for clr-namespace:.</param>
         /// <returns>The loaded object.</returns>
-        public static object Parse(string xaml, Assembly localAssembly = null)
+        public static object Parse(string xaml, Assembly? localAssembly = null)
             => Load(xaml, localAssembly);
 
         /// <summary>
@@ -82,7 +76,7 @@ namespace Avalonia.Markup.Xaml
         /// <param name="xaml">>The string containing the XAML.</param>
         /// <param name="localAssembly">>Default assembly for clr-namespace:.</param>
         /// <returns>The loaded object.</returns>
-        public static T Parse<T>(string xaml, Assembly localAssembly = null)
+        public static T Parse<T>(string xaml, Assembly? localAssembly = null)
             => (T)Parse(xaml, localAssembly);
             
     }

+ 101 - 32
src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs

@@ -10,6 +10,7 @@ using System.Runtime.InteropServices;
 using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
 using Avalonia.Markup.Xaml.XamlIl.Runtime;
 using Avalonia.Platform;
+using XamlX.Ast;
 using XamlX.Transform;
 using XamlX.TypeSystem;
 using XamlX.IL;
@@ -150,12 +151,12 @@ namespace Avalonia.Markup.Xaml.XamlIl
         }
         
 
-        static object LoadSre(string xaml, Assembly localAssembly, object rootInstance, Uri uri, bool isDesignMode, bool useCompiledBindingsByDefault)
+        static object LoadSre(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
         {
             var success = false;
             try
             {
-                var rv = LoadSreCore(xaml, localAssembly, rootInstance, uri, isDesignMode, useCompiledBindingsByDefault);
+                var rv = LoadSreCore(document, configuration);
                 success = true;
                 return rv;
             }
@@ -166,45 +167,100 @@ namespace Avalonia.Markup.Xaml.XamlIl
             }
         }
 
+        static IReadOnlyList<object> LoadGroupSre(IReadOnlyCollection<RuntimeXamlLoaderDocument> documents,
+            RuntimeXamlLoaderConfiguration configuration)
+        {
+            var success = false;
+            try
+            {
+                var rv = LoadGroupSreCore(documents, configuration);
+                success = true;
+                return rv;
+            }
+            finally
+            {
+                if( _sreCanSave)
+                    DumpRuntimeCompilationResults();
+            }
+        }
         
-        static object LoadSreCore(string xaml, Assembly localAssembly, object rootInstance, Uri uri, bool isDesignMode, bool useCompiledBindingsByDefault)
+        static IReadOnlyList<object> LoadGroupSreCore(IReadOnlyCollection<RuntimeXamlLoaderDocument> documents, RuntimeXamlLoaderConfiguration configuration)
         {
-
             InitializeSre();
+            var localAssembly = configuration.LocalAssembly;
             if (localAssembly?.GetName() != null)
                 EmitIgnoresAccessCheckToAttribute(localAssembly.GetName());
             var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly);
-            var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri);
-            var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N"));
+            var clrPropertyBuilder = _sreBuilder.DefineType("ClrProperties_" + Guid.NewGuid().ToString("N"));
             var indexerClosureType = _sreBuilder.DefineType("IndexerClosure_" + Guid.NewGuid().ToString("N"));
             var trampolineBuilder = _sreBuilder.DefineType("Trampolines_" + Guid.NewGuid().ToString("N"));
-            
+
             var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm,
-                _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter,
-                new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)),
-                new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType)),
-                new XamlIlTrampolineBuilder(_sreTypeSystem.CreateTypeBuilder(trampolineBuilder))), 
+                    _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter,
+                    new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)),
+                    new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType)),
+                    new XamlIlTrampolineBuilder(_sreTypeSystem.CreateTypeBuilder(trampolineBuilder))),
                 _sreEmitMappings,
-                _sreContextType) { EnableIlVerification = true, DefaultCompileBindings = useCompiledBindingsByDefault };
+                _sreContextType)
+            {
+                EnableIlVerification = true,
+                DefaultCompileBindings = configuration.UseCompiledBindingsByDefault,
+                IsDesignMode = configuration.DesignMode
+            };
 
-            IXamlType overrideType = null;
-            if (rootInstance != null)
+            var parsedDocuments = new List<XamlDocumentResource>();
+            var rootInstances = new List<object>();
+
+            foreach (var document in documents)
             {
-                overrideType = _sreTypeSystem.GetType(rootInstance.GetType());
+                string xaml;
+                using (var sr = new StreamReader(document.XamlStream))
+                    xaml = sr.ReadToEnd();
+                
+                IXamlType overrideType = null;
+                if (document.RootInstance != null)
+                {
+                    overrideType = _sreTypeSystem.GetType(document.RootInstance.GetType());
+                }
+                
+                var parsed = compiler.Parse(xaml, overrideType);
+                compiler.Transform(parsed);
+
+                var xamlName = GetSafeUriIdentifier(document.BaseUri)
+                               ?? document.RootInstance?.GetType().Name
+                               ?? ((IXamlAstValueNode)parsed.Root).Type.GetClrType().Name;
+                var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + xamlName);
+                var builder = _sreTypeSystem.CreateTypeBuilder(tb);
+                parsedDocuments.Add(new XamlDocumentResource(parsed, document.BaseUri?.ToString(), null, null,
+                    builder,
+                    compiler.DefinePopulateMethod(builder, parsed, AvaloniaXamlIlCompiler.PopulateName, true),
+                    compiler.DefineBuildMethod(builder, parsed, AvaloniaXamlIlCompiler.BuildName, true)));
+                rootInstances.Add(document.RootInstance);
             }
 
-            compiler.IsDesignMode = isDesignMode;
-            compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType);
-            var created = tb.CreateTypeInfo();
+            compiler.TransformGroup(parsedDocuments);
+
+            var createdTypes = parsedDocuments.Select(document =>
+            {
+                compiler.Compile(document.XamlDocument, document.TypeBuilder, document.PopulateMethod,
+                    document.BuildMethod, document.Uri, document.FileSource);
+                return _sreTypeSystem.GetType(document.TypeBuilder.CreateType());
+            }).ToArray();
+            
             clrPropertyBuilder.CreateTypeInfo();
             indexerClosureType.CreateTypeInfo();
             trampolineBuilder.CreateTypeInfo();
 
-            return LoadOrPopulate(created, rootInstance);
+            return createdTypes.Zip(rootInstances, (l, r) => (l, r)).Select(t => LoadOrPopulate(t.Item1, t.Item2)).ToArray();
+        }
+        
+        static object LoadSreCore(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
+        {
+            return LoadGroupSreCore(new[] { document }, configuration).Single();
         }
 #endif
 
-            static object LoadOrPopulate(Type created, object rootInstance)
+        static object LoadOrPopulate(Type created, object rootInstance)
         {
             var isp = Expression.Parameter(typeof(IServiceProvider));
 
@@ -249,19 +305,37 @@ namespace Avalonia.Markup.Xaml.XamlIl
             }
         }
         
-        public static object Load(Stream stream, Assembly localAssembly, object rootInstance, Uri uri,
-            bool isDesignMode, bool useCompiledBindingsByDefault)
+        public static object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
         {
+#if RUNTIME_XAML_CECIL
             string xaml;
-            using (var sr = new StreamReader(stream))
+            using (var sr = new StreamReader(document.XamlStream))
                 xaml = sr.ReadToEnd();
+            return LoadCecil(xaml, configuration.LocalAssembly, document.RootInstance,document.BaseUri, configuration.UseCompiledBindingsByDefault);
+#else
+            return LoadSre(document, configuration);
+#endif
+        }
+
+        public static IReadOnlyList<object> LoadGroup(IReadOnlyCollection<RuntimeXamlLoaderDocument> documents, RuntimeXamlLoaderConfiguration configuration)
+        {
 #if RUNTIME_XAML_CECIL
-            return LoadCecil(xaml, localAssembly, rootInstance, uri, useCompiledBindingsByDefault);
+            throw new NotImplementedException("Load group was not implemented for the Cecil backend");
 #else
-            return LoadSre(xaml, localAssembly, rootInstance, uri, isDesignMode, useCompiledBindingsByDefault);
+            return LoadGroupSre(documents, configuration);
 #endif
         }
 
+        private static string GetSafeUriIdentifier(Uri uri)
+        {
+            return uri?.ToString()
+                .Replace(":", "_")
+                .Replace("/", "_")
+                .Replace("?", "_")
+                .Replace("=", "_")
+                .Replace(".", "_");
+        }
+        
 #if RUNTIME_XAML_CECIL
         private static Dictionary<string, (Action<IServiceProvider, object> populate, Func<IServiceProvider, object>
                 build)>
@@ -303,12 +377,7 @@ namespace Avalonia.Markup.Xaml.XamlIl
                 overrideType = _cecilTypeSystem.GetType(rootInstance.GetType().FullName);
             }
            
-            var safeUri = uri.ToString()
-                .Replace(":", "_")
-                .Replace("/", "_")
-                .Replace("?", "_")
-                .Replace("=", "_")
-                .Replace(".", "_");
+            var safeUri = GetSafeUriIdentifier(uri);
             if (_cecilGeneratedCache.TryGetValue(safeUri, out var cached))
                 return LoadOrPopulate(cached, rootInstance);
             
@@ -335,7 +404,7 @@ namespace Avalonia.Markup.Xaml.XamlIl
                 {
                     DefaultCompileBindings = useCompiledBindingsByDefault
                 };
-            compiler.ParseAndCompile(xaml, uri.ToString(), tb, overrideType);
+            compiler.ParseAndCompile(xaml, uri.ToString(), null, tb, overrideType);
             var asmPath = Path.Combine(_cecilEmitDir, safeUri + ".dll");
             using(var f = File.Create(asmPath))
                 asm.Write(f);

+ 46 - 5
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@@ -1,6 +1,6 @@
 using System.Collections.Generic;
 using System.Linq;
-
+using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers;
 using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
 using XamlX;
 using XamlX.Ast;
@@ -56,8 +56,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 new AvaloniaXamlIlSetterTransformer(),
                 new AvaloniaXamlIlConstructorServiceProviderTransformer(),
                 new AvaloniaXamlIlTransitionsTypeMetadataTransformer(),
-                new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer(),
-                new AvaloniaXamlIlAssetIncludeTransformer()
+                new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer()
             );
             InsertBefore<ConvertPropertyValuesToAssignmentsTransformer>(
                 new AvaloniaXamlIlOptionMarkupExtensionTransformer());
@@ -83,6 +82,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
 
             Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter());
             Emitters.Add(new AvaloniaXamlIlRootObjectScope.Emitter());
+            
+            GroupTransformers = new()
+            {
+                new AvaloniaXamlIncludeTransformer()
+            };
         }
         public AvaloniaXamlIlCompiler(TransformerConfiguration configuration,
             XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings,
@@ -115,7 +119,27 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             set => _bindingTransformer.CompileBindingsByDefault = value;
         }
 
-        public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlTypeBuilder<IXamlILEmitter> tb, IXamlType overrideRootType)
+        public List<IXamlAstGroupTransformer> GroupTransformers { get; }
+
+        public void TransformGroup(IReadOnlyCollection<IXamlDocumentResource> documents, bool strict = true)
+        {
+            var ctx = new AstGroupTransformationContext(documents, _configuration, strict);
+            foreach (var transformer in GroupTransformers)
+            {
+                foreach (var doc in documents)
+                {
+                    var root = doc.XamlDocument.Root;
+                    ctx.CurrentDocument = doc;
+                    ctx.RootObject = (IXamlAstValueNode)root;
+                    ctx.VisitChildren(ctx.RootObject, transformer);
+                    root = ctx.Visit(root, transformer);
+
+                    doc.XamlDocument.Root = root;
+                }
+            }
+        }
+
+        public XamlDocument Parse(string xaml, IXamlType overrideRootType)
         {
             var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary<string, string>
             {
@@ -148,9 +172,26 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
 
             OverrideRootType(parsed, rootType);
 
+            return parsed;
+        }
+
+        public void Compile(XamlDocument document, IXamlTypeBuilder<IXamlILEmitter> tb, IXamlMethodBuilder<IXamlILEmitter> populateMethod, IXamlMethodBuilder<IXamlILEmitter> buildMethod, string baseUri, IFileSource fileSource)
+        {
+            Compile(document, _contextType, populateMethod, buildMethod,
+                _configuration.TypeMappings.XmlNamespaceInfoProvider == null ?
+                    null :
+                    tb.DefineSubType(_configuration.WellKnownTypes.Object,
+                        "__AvaloniaXamlIlNsInfo", false), (name, bt) => tb.DefineSubType(bt, name, false),
+                (s, returnType, parameters) => tb.DefineDelegateSubType(s, false, returnType, parameters), baseUri,
+                fileSource);
+        }
+        
+        public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlTypeBuilder<IXamlILEmitter> tb, IXamlType overrideRootType)
+        {
+            var parsed = Parse(xaml, overrideRootType);
+
             Transform(parsed);
             Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource);
-
         }
 
         public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType)

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguageParseIntrinsics.cs

@@ -274,7 +274,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
             {
                 var uriText = text.Trim();
 
-                var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.Absolute : UriKind.Relative);
+                var kind = ((!uriText?.StartsWith("/") == true) ? UriKind.RelativeOrAbsolute : UriKind.Relative);
 
                 if (string.IsNullOrWhiteSpace(uriText) || !Uri.TryCreate(uriText, kind, out var _))
                 {

+ 78 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/IXamlAstGroupTransformer.cs

@@ -0,0 +1,78 @@
+using System;
+using System.Collections.Generic;
+using System.Xml;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Transform;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers;
+
+internal class AstGroupTransformationContext : AstTransformationContext
+{
+    public AstGroupTransformationContext(IReadOnlyCollection<IXamlDocumentResource> documents, TransformerConfiguration configuration, bool strictMode = true)
+        : base(configuration, null, strictMode)
+    {
+        Documents = documents;
+    }
+
+    public IXamlDocumentResource CurrentDocument { get; set; }
+    
+    public IReadOnlyCollection<IXamlDocumentResource> Documents { get; }
+    
+    public new IXamlAstNode ParseError(string message, IXamlAstNode node) =>
+        Error(node, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, node));
+
+    public new IXamlAstNode ParseError(string message, IXamlAstNode offender, IXamlAstNode ret) =>
+        Error(ret, new XamlDocumentParseException(CurrentDocument?.FileSource?.FilePath, message, offender));
+
+    class Visitor : IXamlAstVisitor
+    {
+        private readonly AstGroupTransformationContext _context;
+        private readonly IXamlAstGroupTransformer _transformer;
+
+        public Visitor(AstGroupTransformationContext context, IXamlAstGroupTransformer transformer)
+        {
+            _context = context;
+            _transformer = transformer;
+        }
+            
+        public IXamlAstNode Visit(IXamlAstNode node)
+        {
+#if Xaml_DEBUG
+                return _transformer.Transform(_context, node);
+#else
+            try
+            {
+                return _transformer.Transform(_context, node);
+            }
+            catch (Exception e) when (!(e is XmlException))
+            {
+                throw new XamlDocumentParseException(
+                    _context.CurrentDocument?.FileSource?.FilePath,
+                    "Internal compiler error while transforming node " + node + ":\n" + e,
+                    node);
+            }
+#endif
+        }
+
+        public void Push(IXamlAstNode node) => _context.PushParent(node);
+
+        public void Pop() => _context.PopParent();
+    }
+    
+    public IXamlAstNode Visit(IXamlAstNode root, IXamlAstGroupTransformer transformer)
+    {
+        root = root.Visit(new Visitor(this, transformer));
+        return root;
+    }
+
+    public void VisitChildren(IXamlAstNode root, IXamlAstGroupTransformer transformer)
+    {
+        root.VisitChildren(new Visitor(this, transformer));
+    }
+}
+
+internal interface IXamlAstGroupTransformer
+{
+    IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node);
+}

+ 173 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/GroupTransformers/XamlIncludeGroupTransformer.cs

@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers;
+using Avalonia.Platform;
+using XamlX;
+using XamlX.Ast;
+using XamlX.Emit;
+using XamlX.IL;
+using XamlX.Transform;
+using XamlX.Transform.Transformers;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers;
+
+internal class AvaloniaXamlIncludeTransformer : IXamlAstGroupTransformer
+{
+    public IXamlAstNode Transform(AstGroupTransformationContext context, IXamlAstNode node)
+    {
+        if (node is not XamlValueWithManipulationNode valueNode
+            || valueNode.Value is not XamlAstNewClrObjectNode objectNode
+            || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude
+                && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude))
+        {
+            return node;
+        }
+
+        var nodeTypeName = objectNode.Type.GetClrType().Name;
+        var expectedLoadedType = objectNode.Type.GetClrType().GetAllProperties()
+            .FirstOrDefault(p => p.Name == "Loaded")?.PropertyType;
+        if (expectedLoadedType is null)
+        {
+            throw new InvalidOperationException($"\"{nodeTypeName}\".Loaded property is expected to be defined");
+        }
+        
+        if (valueNode.Manipulation is not XamlObjectInitializationNode
+            {
+                Manipulation: XamlPropertyAssignmentNode { Property: { Name: "Source" } } sourceProperty
+            })
+        {
+            return context.ParseError($"Source property must be set on the \"{nodeTypeName}\" node.", node);
+        }
+
+        // We expect that AvaloniaXamlIlLanguageParseIntrinsics has already parsed the Uri and created node like: `new Uri(assetPath, uriKind)`.
+        if (sourceProperty.Values.OfType<XamlAstNewClrObjectNode>().FirstOrDefault() is not { } sourceUriNode
+            || sourceUriNode.Type.GetClrType() != context.GetAvaloniaTypes().Uri
+            || sourceUriNode.Arguments.FirstOrDefault() is not XamlConstantNode { Constant: string originalAssetPath }
+            || sourceUriNode.Arguments.Skip(1).FirstOrDefault() is not XamlConstantNode { Constant: int uriKind })
+        {
+            // TODO: make it a compiler warning
+            // Source value can be set with markup extension instead of the Uri object node, we don't support it here yet.
+            return node;
+        }
+
+        var uriPath = new Uri(originalAssetPath, (UriKind)uriKind);
+        if (!uriPath.IsAbsoluteUri)
+        {
+            var baseUrl = context.CurrentDocument.Uri ?? throw new InvalidOperationException("CurrentDocument URI is null.");
+            uriPath = new Uri(new Uri(baseUrl, UriKind.Absolute), uriPath);
+        }
+        else if (!uriPath.Scheme.Equals("avares", StringComparison.CurrentCultureIgnoreCase))
+        {
+            return context.ParseError(
+                $"\"{nodeTypeName}.Source\" supports only \"avares://\" absolute or relative uri.",
+                sourceUriNode, node);
+        }
+
+        var assetPathUri = Uri.UnescapeDataString(uriPath.AbsoluteUri);
+        var assetPath = assetPathUri.Replace("avares://", "");
+        var assemblyNameSeparator = assetPath.IndexOf('/');
+        var assembly = assetPath.Substring(0, assemblyNameSeparator);
+        var fullTypeName = Path.GetFileNameWithoutExtension(assetPath.Replace('/', '.'));
+
+        // Search file in the current assembly among other XAML resources.
+        if (context.Documents.FirstOrDefault(d => string.Equals(d.Uri, assetPathUri, StringComparison.InvariantCultureIgnoreCase)) is {} targetDocument)
+        {
+            if (targetDocument.BuildMethod is not null)
+            {
+                return FromMethod(context, targetDocument.BuildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+            }
+
+            if (targetDocument.ClassType is not null)
+            {
+                return FromType(context, targetDocument.ClassType, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+            }
+
+            return context.ParseError(
+                $"Unable to resolve XAML resource \"{assetPathUri}\" in the current assembly.",
+                sourceUriNode, node);
+        }
+
+        // If resource wasn't found in the current assembly, search in the others.
+        if (context.Configuration.TypeSystem.FindAssembly(assembly) is not { } assetAssembly)
+        {
+            return context.ParseError($"Assembly \"{assembly}\" was not found from the \"{assetPathUri}\" source.", sourceUriNode, node);
+        }
+
+        var avaResType = assetAssembly.FindType("CompiledAvaloniaXaml.!AvaloniaResources");
+        if (avaResType is null)
+        {
+            return context.ParseError(
+                $"Unable to resolve \"!AvaloniaResources\" type on \"{assembly}\" assembly.", sourceUriNode, node);
+        }
+
+        var relativeName = "Build:" + assetPath.Substring(assemblyNameSeparator);
+        var buildMethod = avaResType.FindMethod(m => m.Name == relativeName);
+        if (buildMethod is not null)
+        {
+            return FromMethod(context, buildMethod, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+        }
+        else if (assetAssembly.FindType(fullTypeName) is { } type)
+        {
+            return FromType(context, type, sourceUriNode, expectedLoadedType, node, assetPathUri, assembly);
+        }
+
+        return context.ParseError(
+            $"Unable to resolve XAML resource \"{assetPathUri}\" in the \"{assembly}\" assembly.",
+            sourceUriNode, node);
+    }
+    
+    private static IXamlAstNode FromType(AstTransformationContext context, IXamlType type, IXamlAstNode li,
+        IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
+    {
+        if (!expectedLoadedType.IsAssignableFrom(type))
+        {
+            return context.ParseError(
+                $"Resource \"{assetPathUri}\" is defined as \"{type}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
+                li, fallbackNode);
+        }
+        
+        IXamlAstNode newObjNode = new XamlAstObjectNode(li, new XamlAstClrTypeReference(li, type, false));
+        newObjNode = new AvaloniaXamlIlConstructorServiceProviderTransformer().Transform(context, newObjNode);
+        newObjNode = new ConstructableObjectTransformer().Transform(context, newObjNode);
+        return new NewObjectTransformer().Transform(context, newObjNode);
+    }
+
+    private static IXamlAstNode FromMethod(AstTransformationContext context, IXamlMethod method, IXamlAstNode li,
+        IXamlType expectedLoadedType, IXamlAstNode fallbackNode, string assetPathUri, string assembly)
+    {
+        if (!expectedLoadedType.IsAssignableFrom(method.ReturnType))
+        {
+            return context.ParseError(
+                $"Resource \"{assetPathUri}\" is defined as \"{method.ReturnType}\" type in the \"{assembly}\" assembly, but expected \"{expectedLoadedType}\".",
+                li, fallbackNode);
+        }
+        
+        var sp = context.Configuration.TypeMappings.ServiceProvider;
+        return new XamlStaticOrTargetedReturnMethodCallNode(li, method,
+            new[] { new NewServiceProviderNode(sp, li) });
+    }
+    
+    internal class NewServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack,
+        IXamlAstEmitableNode<IXamlILEmitter, XamlILNodeEmitResult>
+    {
+        public NewServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo)
+        {
+            Type = new XamlAstClrTypeReference(lineInfo, type, false);
+        }
+
+        public IXamlAstTypeReference Type { get; }
+        public bool NeedsParentStack => true;
+        public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
+        {
+            var method = context.GetAvaloniaTypes().RuntimeHelpers
+                .FindMethod(m => m.Name == "CreateRootServiceProviderV2");
+            codeGen.EmitCall(method);
+
+            return XamlILNodeEmitResult.Type(0, Type.GetClrType());
+        }
+    }
+}

+ 17 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/IXamlDocumentResource.cs

@@ -0,0 +1,17 @@
+using System;
+using XamlX.Ast;
+using XamlX.TypeSystem;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
+
+#nullable enable
+
+internal interface IXamlDocumentResource
+{
+    IXamlMethod? BuildMethod { get; }
+    IXamlType? ClassType { get; }
+    string? Uri { get; }
+    IXamlMethod PopulateMethod { get; }
+    IFileSource? FileSource { get; }
+    XamlDocument XamlDocument { get; }
+}

+ 0 - 93
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAssetIncludeTransformer.cs

@@ -1,93 +0,0 @@
-using System.Linq;
-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 AvaloniaXamlIlAssetIncludeTransformer : IXamlAstTransformer
-{
-    public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node)
-    {
-        if (node is not XamlAstObjectNode objectNode
-            || (objectNode.Type.GetClrType() != context.GetAvaloniaTypes().StyleInclude
-                && objectNode.Type.GetClrType() != context.GetAvaloniaTypes().ResourceInclude))
-        {
-            return node;
-        }
-
-        var nodeTypeName = objectNode.Type.GetClrType().Name;
-
-        var sourceProperty = objectNode.Children.OfType<XamlAstXamlPropertyValueNode>().FirstOrDefault(n => n.Property.GetClrProperty().Name == "Source");
-        var directives = objectNode.Children.OfType<XamlAstXmlDirective>().ToList();
-        if (sourceProperty is null
-            || objectNode.Children.Count != (directives.Count + 1))
-        {
-            throw new XamlParseException($"Unexpected property on the {nodeTypeName} node", node);
-        }
-
-        if (sourceProperty.Values.OfType<XamlAstTextNode>().FirstOrDefault() is not { } sourceTextNode)
-        {
-            // TODO: make it a compiler warning
-            // Source value can be set with markup extension instead of a text node, we don't support it here yet.
-            return node;
-        }
-
-        var originalAssetPath = sourceTextNode.Text;
-        if (!(originalAssetPath.StartsWith("avares://") || originalAssetPath.StartsWith("/")))
-        {
-            return node;
-        }
-
-        var runtimeHelpers = context.GetAvaloniaTypes().RuntimeHelpers;
-        var markerMethodName = "Resolve" + nodeTypeName;
-        var markerMethod = runtimeHelpers.FindMethod(m => m.Name == markerMethodName && m.Parameters.Count == 3);
-        if (markerMethod is null)
-        {
-            throw new XamlParseException($"Marker method \"{markerMethodName}\" was not found for the \"{nodeTypeName}\" node", node);
-        }
-        
-        return new XamlValueWithManipulationNode(
-            node,
-            new AssetIncludeMethodNode(node, markerMethod, originalAssetPath),
-            new XamlManipulationGroupNode(node, directives));
-    }
-
-    private class AssetIncludeMethodNode : XamlAstNode, IXamlAstValueNode, IXamlAstILEmitableNode
-    {
-        private readonly IXamlMethod _method;
-        private readonly string _originalAssetPath;
-
-        public AssetIncludeMethodNode(
-            IXamlAstNode original, IXamlMethod method, string originalAssetPath)
-            : base(original)
-        {
-            _method = method;
-            _originalAssetPath = originalAssetPath;
-        }
-
-        public IXamlAstTypeReference Type => new XamlAstClrTypeReference(this, _method.ReturnType, false);
-
-        public XamlILNodeEmitResult Emit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
-        {
-            var absoluteSource = _originalAssetPath;
-            if (absoluteSource.StartsWith("/"))
-            {
-                // Avoid Uri class here to avoid potential problems with escaping.
-                // Keeping string as close to the original as possible.
-                var absoluteBaseUrl =  context.RuntimeContext.BaseUrl;
-                absoluteSource = absoluteBaseUrl.Substring(0, absoluteBaseUrl.LastIndexOf('/')) + absoluteSource;
-            }
-
-            codeGen.Ldstr(absoluteSource);
-            codeGen.Ldc_I4(Line);
-            codeGen.Ldc_I4(Position);
-            codeGen.EmitCall(_method);
-
-            return XamlILNodeEmitResult.Type(0, _method.ReturnType);
-        }
-    }
-}

+ 9 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs

@@ -1,5 +1,6 @@
 using System.Collections.Generic;
 using System.Linq;
+using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.GroupTransformers;
 using XamlX.Emit;
 using XamlX.IL;
 using XamlX.Transform;
@@ -263,5 +264,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
             ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration));
             return rv;
         }
+        
+        public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this AstGroupTransformationContext ctx)
+        {
+            if (ctx.TryGetItem<AvaloniaXamlIlWellKnownTypes>(out var rv))
+                return rv;
+            ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration));
+            return rv;
+        }
     }
 }

+ 21 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentParseException.cs

@@ -0,0 +1,21 @@
+using XamlX;
+using XamlX.Ast;
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
+
+internal class XamlDocumentParseException : XamlParseException
+{
+    public string FilePath { get; }
+
+    public XamlDocumentParseException(string path, XamlParseException parseException)
+        : base(parseException.Message, parseException.LineNumber, parseException.LinePosition)
+    {
+        FilePath = path;
+    }
+    
+    public XamlDocumentParseException(string path, string message, IXamlLineInfo lineInfo)
+        : base(message, lineInfo.Line, lineInfo.Position)
+    {
+        FilePath = path;
+    }
+}

+ 40 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlDocumentResource.cs

@@ -0,0 +1,40 @@
+using System;
+using XamlX.Ast;
+using XamlX.IL;
+using XamlX.TypeSystem;
+#nullable enable
+
+namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions;
+
+internal class XamlDocumentResource : IXamlDocumentResource
+{
+    public XamlDocumentResource(
+        XamlDocument xamlDocument,
+        string? uri,
+        IFileSource? fileSource,
+        IXamlType? classType,
+        IXamlTypeBuilder<IXamlILEmitter> typeBuilder,
+        IXamlMethodBuilder<IXamlILEmitter> populateMethod,
+        IXamlMethodBuilder<IXamlILEmitter>? buildMethod)
+    {
+        XamlDocument = xamlDocument;
+        Uri = uri;
+        FileSource = fileSource;
+        ClassType = classType;
+        TypeBuilder = typeBuilder;
+        PopulateMethod = populateMethod;
+        BuildMethod = buildMethod;
+    }
+
+    public XamlDocument XamlDocument { get; }
+    public string? Uri { get; }
+    public IFileSource? FileSource { get; }
+
+    public IXamlType? ClassType { get; }
+    public IXamlTypeBuilder<IXamlILEmitter> TypeBuilder { get; }
+    public IXamlMethodBuilder<IXamlILEmitter> PopulateMethod { get; }
+    public IXamlMethodBuilder<IXamlILEmitter>? BuildMethod { get; }
+
+    IXamlMethod? IXamlDocumentResource.BuildMethod => BuildMethod;
+    IXamlMethod IXamlDocumentResource.PopulateMethod => PopulateMethod;
+}

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@@ -1 +1 @@
-Subproject commit cd3682c61577a3518de765f7938295d98ff9808c
+Subproject commit 491de981dd4433ee58bc9540e2cd4a5d168f8168

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@@ -43,6 +43,7 @@
         <Compile Include="MarkupExtensions\RelativeSourceExtension.cs" />
         <Compile Include="Properties\AssemblyInfo.cs" />
         <Compile Include="RuntimeXamlLoaderConfiguration.cs" />
+        <Compile Include="RuntimeXamlLoaderDocument.cs" />
         <Compile Include="Styling\ResourceInclude.cs" />
         <Compile Include="Styling\StyleInclude.cs" />
         <Compile Include="Templates\ControlTemplate.cs" />

+ 12 - 11
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs

@@ -1,7 +1,7 @@
 using System;
 using System.IO;
 using Avalonia.Platform;
-
+#nullable enable
 namespace Avalonia.Markup.Xaml
 {
     /// <summary>
@@ -11,7 +11,7 @@ namespace Avalonia.Markup.Xaml
     {
         public interface IRuntimeXamlLoader
         {
-            object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration);
+            object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration);
         }
         
         /// <summary>
@@ -32,9 +32,10 @@ namespace Avalonia.Markup.Xaml
         /// A base URI to use if <paramref name="uri"/> is relative.
         /// </param>
         /// <returns>The loaded object.</returns>
-        public static object Load(Uri uri, Uri baseUri = null)
+        public static object Load(Uri uri, Uri? baseUri = null)
         {
-            Contract.Requires<ArgumentNullException>(uri != null);
+            if (uri is null)
+                throw new ArgumentNullException(nameof(uri));
 
             var assetLocator = AvaloniaLocator.Current.GetService<IAssetLoader>();
 
@@ -44,14 +45,16 @@ namespace Avalonia.Markup.Xaml
                     "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?");
             }
 
+            var absoluteUri = uri.IsAbsoluteUri
+                ? uri
+                : new Uri(baseUri ?? throw new InvalidOperationException("Cannot load relative Uri when BaseUri is null"), uri);
+
             var compiledLoader = assetLocator.GetAssembly(uri, baseUri)
                 ?.GetType("CompiledAvaloniaXaml.!XamlLoader")
                 ?.GetMethod("TryLoad", new[] {typeof(string)});
             if (compiledLoader != null)
             {
-                var uriString = (!uri.IsAbsoluteUri && baseUri != null ? new Uri(baseUri, uri) : uri)
-                    .ToString();
-                var compiledResult = compiledLoader.Invoke(null, new object[] {uriString});
+                var compiledResult = compiledLoader.Invoke(null, new object[] {absoluteUri.ToString()});
                 if (compiledResult != null)
                     return compiledResult;
             }
@@ -63,11 +66,9 @@ namespace Avalonia.Markup.Xaml
                 var asset = assetLocator.OpenAndGetAssembly(uri, baseUri);
                 using (var stream = asset.stream)
                 {
-                    var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri);
-                    return runtimeLoader.Load(stream, new RuntimeXamlLoaderConfiguration
+                    return runtimeLoader.Load(new RuntimeXamlLoaderDocument(absoluteUri, stream), new RuntimeXamlLoaderConfiguration
                     {
-                        LocalAssembly = asset.assembly,
-                        BaseUri = absoluteUri
+                        LocalAssembly = asset.assembly
                     });
                 }
             }

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml/Converters/AvaloniaUriTypeConverter.cs

@@ -17,7 +17,7 @@ namespace Avalonia.Markup.Xaml.Converters
             if (s == null)
                 return null;
             //On Unix Uri tries to interpret paths starting with "/" as file Uris
-            var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.Absolute;
+            var kind = s.StartsWith("/") ? UriKind.Relative : UriKind.RelativeOrAbsolute;
             if (!Uri.TryCreate(s, kind, out var res))
                 throw new ArgumentException("Unable to parse URI: " + s);
             return res;

+ 1 - 12
src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderConfiguration.cs

@@ -1,4 +1,3 @@
-using System;
 using System.Reflection;
 
 namespace Avalonia.Markup.Xaml;
@@ -7,21 +6,11 @@ namespace Avalonia.Markup.Xaml;
 
 public class RuntimeXamlLoaderConfiguration
 {
-    /// <summary>
-    /// The URI of the XAML being loaded.
-    /// </summary>
-    public Uri? BaseUri { get; set; }
-
     /// <summary>
     /// Default assembly for clr-namespace:.
     /// </summary>
     public Assembly? LocalAssembly { get; set; }
-            
-    /// <summary>
-    /// The optional instance into which the XAML should be loaded.
-    /// </summary>
-    public object? RootInstance { get; set; }
-            
+
     /// <summary>
     /// Defines is CompiledBinding should be used by default.
     /// Default is 'false'.

+ 70 - 0
src/Markup/Avalonia.Markup.Xaml/RuntimeXamlLoaderDocument.cs

@@ -0,0 +1,70 @@
+#nullable enable
+using System;
+using System.IO;
+using System.Text;
+
+namespace Avalonia.Markup.Xaml;
+
+public class RuntimeXamlLoaderDocument
+{
+    public RuntimeXamlLoaderDocument(string xaml)
+    {
+        XamlStream = new MemoryStream(Encoding.UTF8.GetBytes(xaml));
+    }
+
+    public RuntimeXamlLoaderDocument(Uri? baseUri, string xaml)
+        : this(xaml)
+    {
+        BaseUri = baseUri;
+    }
+
+    public RuntimeXamlLoaderDocument(object? rootInstance, string xaml)
+        : this(xaml)
+    {
+        RootInstance = rootInstance;
+    }
+    
+    public RuntimeXamlLoaderDocument(Uri? baseUri, object? rootInstance, string xaml)
+        : this(baseUri, xaml)
+    {
+        RootInstance = rootInstance;
+    }
+    
+    public RuntimeXamlLoaderDocument(Stream stream)
+    {
+        XamlStream = stream;
+    }
+
+    public RuntimeXamlLoaderDocument(Uri? baseUri, Stream stream)
+        : this(stream)
+    {
+        BaseUri = baseUri;
+    }
+
+    public RuntimeXamlLoaderDocument(object? rootInstance, Stream stream)
+        : this(stream)
+    {
+        RootInstance = rootInstance;
+    }
+
+    public RuntimeXamlLoaderDocument(Uri? baseUri, object? rootInstance, Stream stream)
+        : this(baseUri, stream)
+    {
+        RootInstance = rootInstance;
+    }
+    
+    /// <summary>
+    /// The URI of the XAML being loaded.
+    /// </summary>
+    public Uri? BaseUri { get; set; }
+
+    /// <summary>
+    /// The optional instance into which the XAML should be loaded.
+    /// </summary>
+    public object? RootInstance { get; set; }
+    
+    /// <summary>
+    /// The stream containing the XAML.
+    /// </summary>
+    public Stream XamlStream { get; set; }
+}

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml/Styling/ResourceInclude.cs

@@ -42,7 +42,8 @@ namespace Avalonia.Markup.Xaml.Styling
                 if (_loaded == null)
                 {
                     _isLoading = true;
-                    _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(Source, _baseUri);
+                    var source = Source ?? throw new InvalidOperationException("ResourceInclude.Source must be set.");
+                    _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(source, _baseUri);
                     _isLoading = false;
                 }
 

+ 2 - 1
src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs

@@ -51,7 +51,8 @@ namespace Avalonia.Markup.Xaml.Styling
                 if (_loaded == null)
                 {
                     _isLoading = true;
-                    var loaded = (IStyle)AvaloniaXamlLoader.Load(Source, _baseUri);
+                    var source = Source ?? throw new InvalidOperationException("StyleInclude.Source must be set.");
+                    var loaded = (IStyle)AvaloniaXamlLoader.Load(source, _baseUri);
                     _loaded = new[] { loaded };
                     _isLoading = false;
                 }

+ 0 - 10
src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs

@@ -17,16 +17,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
 {
     public static class XamlIlRuntimeHelpers
     {
-        public static IStyle ResolveStyleInclude(string absoluteSource, int line, int position)
-        {
-            return new StyleInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded;
-        }
-
-        public static IResourceDictionary ResolveResourceInclude(string absoluteSource, int line, int position)
-        {
-            return new ResourceInclude((Uri)null) { Source = new Uri(absoluteSource) }.Loaded;
-        }
-
         public static Func<IServiceProvider, object> DeferredTransformationFactoryV1(Func<IServiceProvider, object> builder,
             IServiceProvider provider)
         {

+ 2 - 4
src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs

@@ -8,11 +8,9 @@ namespace Avalonia.Designer.HostApp
 {
     class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader
     {
-        public object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration)
+        public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration)
         {
-            return AvaloniaXamlIlRuntimeCompiler.Load(stream,
-                configuration.LocalAssembly, configuration.RootInstance, configuration.BaseUri,
-                configuration.DesignMode, configuration.UseCompiledBindingsByDefault);
+            return AvaloniaXamlIlRuntimeCompiler.Load(document, configuration);
         }
     }
 }

+ 2 - 4
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@@ -1642,10 +1642,8 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
         xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
         X='{Binding StringProperty, DataType=local:TestDataContext}' />";
-                var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml, new RuntimeXamlLoaderConfiguration
-                {
-                    UseCompiledBindingsByDefault = true
-                });
+                var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(new RuntimeXamlLoaderDocument(xaml),
+                    new RuntimeXamlLoaderConfiguration { UseCompiledBindingsByDefault = true });
                 var compiledPath = ((CompiledBindingExtension)control.X).Path;
 
                 var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));

+ 58 - 50
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/DynamicResourceExtensionTests.cs

@@ -250,30 +250,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         [Fact]
         public void DynamicResource_Can_Be_Assigned_To_Setter_In_Styles_File()
         {
-            var styleXaml = @"
+            var documents = new[]
+            {
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
 <Styles xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Styles.Resources>
         <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
     </Styles.Resources>
-
     <Style Selector='Border'>
         <Setter Property='Background' Value='{DynamicResource brush}'/>
     </Style>
-</Styles>";
-
-            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
-            {
-                var xaml = @"
+</Styles>"),
+                new RuntimeXamlLoaderDocument(@"
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Window.Styles>
-        <StyleInclude Source='test:style.xaml'/>
+        <StyleInclude Source='avares://Tests/Style.xaml'/>
     </Window.Styles>
     <Border Name='border'/>
-</Window>";
-
-                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+</Window>")
+            };
+            
+            using (StyledWindow())
+            {
+                var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                var window = Assert.IsType<Window>(compiled[1]);
                 var border = window.FindControl<Border>("border");
                 var brush = (ISolidColorBrush)border.Background;
 
@@ -284,13 +286,14 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         [Fact]
         public void DynamicResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File()
         {
-            var styleXaml = @"
+            var documents = new[]
+            {
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
 <Styles xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Styles.Resources>
         <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
     </Styles.Resources>
-
     <Style Selector='Button'>
         <Setter Property='Template'>
             <ControlTemplate>
@@ -298,20 +301,21 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             </ControlTemplate>
         </Setter>
     </Style>
-</Styles>";
-
-            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
-            {
-                var xaml = @"
+</Styles>"),
+                new RuntimeXamlLoaderDocument(@"
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Window.Styles>
-        <StyleInclude Source='test:style.xaml'/>
+        <StyleInclude Source='avares://Tests/Style.xaml'/>
     </Window.Styles>
     <Button Name='button'/>
-</Window>";
-
-                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+</Window>")
+            };
+            
+            using (StyledWindow())
+            {
+                var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                var window = Assert.IsType<Window>(compiled[1]);
                 var button = window.FindControl<Button>("button");
 
                 window.Show();
@@ -553,35 +557,37 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         [Fact]
         public void DynamicResource_Can_Be_Found_Across_Xaml_Style_Files()
         {
-            var style1Xaml = @"
+            var documents = new[]
+            {
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style1.xaml"), @"
 <Style xmlns='https://github.com/avaloniaui'
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
   <Style.Resources>
     <Color x:Key='Red'>Red</Color>
   </Style.Resources>
-</Style>";
-            var style2Xaml = @"
+</Style>"),
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style2.xaml"), @"
 <Style xmlns='https://github.com/avaloniaui'
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
   <Style.Resources>
     <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
   </Style.Resources>
-</Style>";
-            using (StyledWindow(
-                ("test:style1.xaml", style1Xaml), 
-                ("test:style2.xaml", style2Xaml)))
-            {
-                var xaml = @"
+</Style>"),
+                new RuntimeXamlLoaderDocument(@"
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Window.Styles>
-        <StyleInclude Source='test:style1.xaml'/>
-        <StyleInclude Source='test:style2.xaml'/>
+        <StyleInclude Source='avares://Tests/Style1.xaml'/>
+        <StyleInclude Source='avares://Tests/Style2.xaml'/>
     </Window.Styles>
     <Border Name='border' Background='{DynamicResource RedBrush}'/>
-</Window>";
-
-                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+</Window>")
+            };
+            
+            using (StyledWindow())
+            {
+                var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                var window = Assert.IsType<Window>(compiled[2]);
                 var border = window.FindControl<Border>("border");
                 var borderBrush = (ISolidColorBrush)border.Background;
 
@@ -593,33 +599,35 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         [Fact]
         public void DynamicResource_Can_Be_Found_In_Nested_Style_File()
         {
-            var style1Xaml = @"
+            var documents = new[]
+            {
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style1.xaml"), @"
 <Styles xmlns='https://github.com/avaloniaui'
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
-  <StyleInclude Source='test:style2.xaml'/>
-</Styles>";
-            var style2Xaml = @"
+  <StyleInclude Source='avares://Tests/Style2.xaml'/>
+</Styles>"),
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style2.xaml"), @"
 <Style xmlns='https://github.com/avaloniaui'
        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
   <Style.Resources>
     <Color x:Key='Red'>Red</Color>
     <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
   </Style.Resources>
-</Style>";
-            using (StyledWindow(
-                ("test:style1.xaml", style1Xaml),
-                ("test:style2.xaml", style2Xaml)))
-            {
-                var xaml = @"
+</Style>"),
+                new RuntimeXamlLoaderDocument(@"
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Window.Styles>
-        <StyleInclude Source='test:style1.xaml'/>
+        <StyleInclude Source='avares://Tests/Style1.xaml'/>
     </Window.Styles>
     <Border Name='border' Background='{DynamicResource RedBrush}'/>
-</Window>";
-
-                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+</Window>")
+            };
+            
+            using (StyledWindow())
+            {
+                var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                var window = Assert.IsType<Window>(compiled[2]);
                 var border = window.FindControl<Border>("border");
                 var borderBrush = (ISolidColorBrush)border.Background;
 

+ 24 - 20
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/ResourceIncludeTests.cs

@@ -14,29 +14,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             [Fact]
             public void ResourceInclude_Loads_ResourceDictionary()
             {
-                var includeXaml = @"
+                var documents = new[]
+                {
+                    new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
 <ResourceDictionary xmlns='https://github.com/avaloniaui'
                     xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <SolidColorBrush x:Key='brush'>#ff506070</SolidColorBrush>
-</ResourceDictionary>
-";
-                using (StartWithResources(("test:include.xaml", includeXaml)))
-                {
-                    var xaml = @"
+</ResourceDictionary>"),
+                    new RuntimeXamlLoaderDocument(@"
 <UserControl xmlns='https://github.com/avaloniaui'
              xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <UserControl.Resources>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
-                <ResourceInclude Source='test:include.xaml'/>
+                <ResourceInclude Source='avares://Tests/Resource.xaml'/>
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </UserControl.Resources>
 
     <Border Name='border' Background='{StaticResource brush}'/>
-</UserControl>";
+</UserControl>")
+                };
 
-                    var userControl = (UserControl)AvaloniaRuntimeXamlLoader.Load(xaml);
+                using (StartWithResources())
+                {
+                    var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                    var userControl = Assert.IsType<UserControl>(compiled[1]);
                     var border = userControl.FindControl<Border>("border");
 
                     var brush = (ISolidColorBrush)border.Background;
@@ -47,31 +50,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             [Fact]
             public void Missing_ResourceKey_In_ResourceInclude_Does_Not_Cause_StackOverflow()
             {
-                var styleXaml = @"
+                var app = Application.Current;
+                var documents = new[]
+                {
+                    new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Resource.xaml"), @"
 <ResourceDictionary xmlns='https://github.com/avaloniaui'
                     xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <StaticResource x:Key='brush' ResourceKey='missing' />
-</ResourceDictionary>";
-
-                using (StartWithResources(("test:style.xaml", styleXaml)))
-                {
-                    var xaml = @"
+</ResourceDictionary>"),
+                    new RuntimeXamlLoaderDocument(app, @"
 <Application xmlns='https://github.com/avaloniaui'
              xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Application.Resources>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
-                <ResourceInclude Source='test:style.xaml'/>
+                <ResourceInclude Source='avares://Tests/Resource.xaml'/>
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
     </Application.Resources>
-</Application>";
-
-                    var app = Application.Current;
+</Application>")
+                };
 
+                using (StartWithResources())
+                {
                     try
                     {
-                        AvaloniaRuntimeXamlLoader.Load(xaml, null, app);
+                        AvaloniaRuntimeXamlLoader.LoadGroup(documents);
                     }
                     catch (KeyNotFoundException)
                     {

+ 25 - 19
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/StaticResourceExtensionTests.cs

@@ -238,7 +238,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         [Fact]
         public void StaticResource_Can_Be_Assigned_To_Setter_In_Styles_File()
         {
-            var styleXaml = @"
+            var documents = new[]
+            {
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
 <Styles xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Styles.Resources>
@@ -248,20 +250,21 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
     <Style Selector='Border'>
         <Setter Property='Background' Value='{StaticResource brush}'/>
     </Style>
-</Styles>";
-
-            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
-            {
-                var xaml = @"
+</Styles>"),
+                new RuntimeXamlLoaderDocument(@"
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Window.Styles>
-        <StyleInclude Source='test:style.xaml'/>
+        <StyleInclude Source='avares://Tests/Style.xaml'/>
     </Window.Styles>
     <Border Name='border'/>
-</Window>";
+</Window>")
+            };
 
-                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+            using (StyledWindow())
+            {
+                var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                var window = Assert.IsType<Window>(compiled[1]);
                 var border = window.FindControl<Border>("border");
                 var brush = (ISolidColorBrush)border.Background;
 
@@ -311,7 +314,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
         [Fact]
         public void StaticResource_Can_Be_Assigned_To_Property_In_ControlTemplate_In_Styles_File()
         {
-            var styleXaml = @"
+            var documents = new[]
+            {
+                new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
 <Styles xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Styles.Resources>
@@ -325,20 +330,21 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
             </ControlTemplate>
         </Setter>
     </Style>
-</Styles>";
-
-            using (StyledWindow(assets: ("test:style.xaml", styleXaml)))
-            {
-                var xaml = @"
+</Styles>"),
+                new RuntimeXamlLoaderDocument(@"
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Window.Styles>
-        <StyleInclude Source='test:style.xaml'/>
+        <StyleInclude Source='avares://Tests/Style.xaml'/>
     </Window.Styles>
     <Button Name='button'/>
-</Window>";
-
-                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+</Window>")
+            };
+            
+            using (StyledWindow())
+            {
+                var compiled = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                var window = Assert.IsType<Window>(compiled[1]);
                 var button = window.FindControl<Button>("button");
 
                 window.Show();

+ 0 - 51
tests/Avalonia.Markup.Xaml.UnitTests/StyleIncludeTests.cs

@@ -1,51 +0,0 @@
-using System;
-using System.Collections.Generic;
-using Avalonia.UnitTests;
-using Xunit;
-
-namespace Avalonia.Markup.Xaml.UnitTests
-{
-    public class StyleIncludeTests : XamlTestBase
-    {
-        [Fact]
-        public void Missing_ResourceKey_In_StyleInclude_Does_Not_Cause_StackOverflow()
-        {
-            var styleXaml = @"
-<Style xmlns='https://github.com/avaloniaui'
-       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
-    <Style.Resources>
-        <StaticResource x:Key='brush' ResourceKey='missing' />
-    </Style.Resources>
-</Style>";
-
-            using (StartWithResources(("test:style.xaml", styleXaml)))
-            {
-                var xaml = @"
-<Application xmlns='https://github.com/avaloniaui'
-             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
-    <Application.Styles>
-        <StyleInclude Source='test:style.xaml'/>
-    </Application.Styles>
-</Application>";
-
-                var app = Application.Current;
-
-                try
-                {
-                    AvaloniaRuntimeXamlLoader.Load(xaml, null, app);
-                }
-                catch (KeyNotFoundException)
-                {
-
-                }
-            }
-        }
-
-        private IDisposable StartWithResources(params (string, string)[] assets)
-        {
-            var assetLoader = new MockAssetLoader(assets);
-            var services = new TestServices(assetLoader: assetLoader);
-            return UnitTestApplication.Start(services);
-        }
-    }
-}

+ 1 - 21
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@@ -18,6 +18,7 @@ using System.Xml;
 using Xunit;
 using Avalonia.Controls.Documents;
 using Avalonia.Metadata;
+using Avalonia.Themes.Simple;
 
 namespace Avalonia.Markup.Xaml.UnitTests.Xaml
 {
@@ -458,27 +459,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             Assert.Equal(10.0, d);
         }
 
-        [Fact]
-        public void StyleInclude_Is_Built()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
-            {
-                var xaml = @"
-<Styles xmlns='https://github.com/avaloniaui'
-        xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
-    <StyleInclude Source='avares://Avalonia.Themes.Simple/Controls/UserControl.xaml'/>
-</Styles>";
-
-                var styles = AvaloniaRuntimeXamlLoader.Parse<Styles>(xaml);
-
-                Assert.True(styles.Count == 1);
-
-                var styleInclude = styles.First() as IStyle;
-
-                Assert.NotNull(styleInclude);
-            }
-        }
-
         [Fact]
         public void Simple_Xaml_Binding_Is_Operational()
         {

+ 12 - 10
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/ResourceDictionaryTests.cs

@@ -33,29 +33,32 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
         [Fact]
         public void DynamicResource_Finds_Resource_In_Parent_Dictionary()
         {
-            var dictionaryXaml = @"
+            using (StyledWindow())
+            {
+                var documents = new[]
+                {
+                    new RuntimeXamlLoaderDocument(new Uri("avares://Avalonia.Markup.Xaml.UnitTests/dict.xaml"), @"
 <ResourceDictionary xmlns='https://github.com/avaloniaui'
                     xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
   <SolidColorBrush x:Key='RedBrush' Color='{DynamicResource Red}'/>
-</ResourceDictionary>";
-
-            using (StyledWindow(assets: ("test:dict.xaml", dictionaryXaml)))
-            {
-                var xaml = @"
+</ResourceDictionary>"),
+                    new RuntimeXamlLoaderDocument(@"
 <Window xmlns='https://github.com/avaloniaui'
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
     <Window.Resources>
         <ResourceDictionary>
             <ResourceDictionary.MergedDictionaries>
-                <ResourceInclude Source='test:dict.xaml'/>
+                <ResourceInclude Source='avares://Avalonia.Markup.Xaml.UnitTests/dict.xaml'/>
             </ResourceDictionary.MergedDictionaries>
         </ResourceDictionary>
         <Color x:Key='Red'>Red</Color>
     </Window.Resources>
     <Button Name='button' Background='{DynamicResource RedBrush}'/>
-</Window>";
+</Window>")
+                };
 
-                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var loaded = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+                var window = Assert.IsType<Window>(loaded[1]);
                 var button = window.FindControl<Button>("button");
 
                 var brush = Assert.IsType<SolidColorBrush>(button.Background);
@@ -276,7 +279,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
         private IDisposable StyledWindow(params (string, string)[] assets)
         {
             var services = TestServices.StyledWindow.With(
-                assetLoader: new MockAssetLoader(assets),
                 theme: () => new Styles
                 {
                     WindowStyle(),

+ 1 - 2
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml

@@ -1,6 +1,5 @@
 <Style xmlns="https://github.com/avaloniaui"
-       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-       x:Precompile="False">
+       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Style.Resources>
     <Color x:Key="Red">Red</Color>
     <Color x:Key="Green">Green</Color>

+ 1 - 2
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/Style2.xaml

@@ -1,6 +1,5 @@
 <Style xmlns="https://github.com/avaloniaui"
-       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
-       x:Precompile="False">
+       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Style.Resources>
     <SolidColorBrush x:Key="RedBrush" Color="{DynamicResource Red}"/>
     <SolidColorBrush x:Key="GreenBrush" Color="{DynamicResource Green}"/>

+ 268 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleIncludeTests.cs

@@ -0,0 +1,268 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using Avalonia.Themes.Simple;
+using Avalonia.UnitTests;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Xaml;
+
+public class StyleIncludeTests
+{
+    [Fact]
+    public void StyleInclude_Is_Built()
+    {
+        using (UnitTestApplication.Start(TestServices.StyledWindow
+                   .With(theme: () => new Styles())))
+        {
+            var xaml = @"
+<ContentControl xmlns='https://github.com/avaloniaui'>
+    <ContentControl.Styles>
+        <StyleInclude Source='avares://Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml'/>
+    </ContentControl.Styles>
+</ContentControl>";
+
+            var window = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
+                
+            Assert.IsType<Style>(window.Styles[0]);
+        }
+    }
+        
+    [Fact]
+    public void StyleInclude_Is_Built_Resources()
+    {
+        using (UnitTestApplication.Start(TestServices.StyledWindow
+                   .With(theme: () => new Styles())))
+        {
+            var xaml = @"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Resources>
+        <StyleInclude x:Key='Include' Source='avares://Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml'/>
+    </ContentControl.Resources>
+</ContentControl>";
+
+            var contentControl = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
+
+            Assert.IsType<Style>(contentControl.Resources["Include"]);
+        }
+    }
+
+    [Fact]
+    public void StyleInclude_Is_Resolved_With_Two_Files()
+    {
+        var documents = new[]
+        {
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <Color x:Key='Red'>Red</Color>
+    </Style.Resources>
+</Style>"),
+            new RuntimeXamlLoaderDocument(@"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Resources>
+        <StyleInclude x:Key='Include' Source='avares://Tests/Style.xaml'/>
+    </ContentControl.Resources>
+</ContentControl>")
+        };
+        
+        var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+        var style = Assert.IsType<Style>(objects[0]);
+        var contentControl = Assert.IsType<ContentControl>(objects[1]);
+
+        Assert.IsType<Style>(contentControl.Resources["Include"]);
+    }
+    
+    [Fact]
+    public void Relative_Back_StyleInclude_Is_Resolved_With_Two_Files()
+    {
+        var documents = new[]
+        {
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Style.xaml"), @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <Color x:Key='Red'>Red</Color>
+    </Style.Resources>
+</Style>"),
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Subfolder/Folder/Root.xaml"), @"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Resources>
+        <StyleInclude x:Key='Include' Source='../Style.xaml'/>
+    </ContentControl.Resources>
+</ContentControl>")
+        };
+        
+        var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+        var style = Assert.IsType<Style>(objects[0]);
+        var contentControl = Assert.IsType<ContentControl>(objects[1]);
+
+        Assert.IsType<Style>(contentControl.Resources["Include"]);
+    }
+    
+    [Fact]
+    public void Relative_Root_StyleInclude_Is_Resolved_With_Two_Files()
+    {
+        var documents = new[]
+        {
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <Color x:Key='Red'>Red</Color>
+    </Style.Resources>
+</Style>"),
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Resources>
+        <StyleInclude x:Key='Include' Source='/Style.xaml'/>
+    </ContentControl.Resources>
+</ContentControl>")
+        };
+        
+        var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+        var style = Assert.IsType<Style>(objects[0]);
+        var contentControl = Assert.IsType<ContentControl>(objects[1]);
+
+        Assert.IsType<Style>(contentControl.Resources["Include"]);
+    }
+    
+    [Fact]
+    public void Relative_StyleInclude_Is_Resolved_With_Two_Files()
+    {
+        var documents = new[]
+        {
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Style.xaml"), @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <Color x:Key='Red'>Red</Color>
+    </Style.Resources>
+</Style>"),
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Resources>
+        <StyleInclude x:Key='Include' Source='Style.xaml'/>
+    </ContentControl.Resources>
+</ContentControl>")
+        };
+        
+        var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+        var style = Assert.IsType<Style>(objects[0]);
+        var contentControl = Assert.IsType<ContentControl>(objects[1]);
+
+        Assert.IsType<Style>(contentControl.Resources["Include"]);
+    }
+    
+    [Fact]
+    public void Relative_Dot_Syntax__StyleInclude_Is_Resolved_With_Two_Files()
+    {
+        var documents = new[]
+        {
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Style.xaml"), @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <Color x:Key='Red'>Red</Color>
+    </Style.Resources>
+</Style>"),
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Folder/Root.xaml"), @"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Resources>
+        <StyleInclude x:Key='Include' Source='./Style.xaml'/>
+    </ContentControl.Resources>
+</ContentControl>")
+        };
+        
+        var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+        var style = Assert.IsType<Style>(objects[0]);
+        var contentControl = Assert.IsType<ContentControl>(objects[1]);
+
+        Assert.IsType<Style>(contentControl.Resources["Include"]);
+    }
+    
+    [Fact]
+    public void NonLatin_StyleInclude_Is_Resolved_With_Two_Files()
+    {
+        var documents = new[]
+        {
+            new RuntimeXamlLoaderDocument(new Uri("avares://アセンブリ/スタイル.xaml"), @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <Color x:Key='Red'>Red</Color>
+    </Style.Resources>
+</Style>"),
+            new RuntimeXamlLoaderDocument(@"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Resources>
+        <StyleInclude x:Key='Include' Source='avares://アセンブリ/スタイル.xaml'/>
+    </ContentControl.Resources>
+</ContentControl>")
+        };
+        
+        var objects = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+        var style = Assert.IsType<Style>(objects[0]);
+        var contentControl = Assert.IsType<ContentControl>(objects[1]);
+
+        Assert.IsType<Style>(contentControl.Resources["Include"]);
+    }
+    
+    [Fact]
+    public void Missing_ResourceKey_In_StyleInclude_Does_Not_Cause_StackOverflow()
+    {
+        var documents = new[]
+        {
+            new RuntimeXamlLoaderDocument(new Uri("avares://Tests/Style.xaml"), @"
+<Style xmlns='https://github.com/avaloniaui'
+       xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Style.Resources>
+        <StaticResource x:Key='brush' ResourceKey='missing' />
+    </Style.Resources>
+</Style>"),
+            new RuntimeXamlLoaderDocument(@"
+<ContentControl xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <ContentControl.Styles>
+        <StyleInclude Source='avares://Tests/Style.xaml'/>
+    </ContentControl.Styles>
+</ContentControl>")
+        };
+
+
+        try
+        {
+            _ = AvaloniaRuntimeXamlLoader.LoadGroup(documents);
+        }
+        catch (KeyNotFoundException)
+        {
+
+        }
+    }
+    
+    [Fact]
+    public void StyleInclude_Should_Be_Replaced_With_Direct_Call()
+    {
+        var control = (ContentControl)AvaloniaRuntimeXamlLoader.Load(@"
+<ContentControl xmlns='https://github.com/avaloniaui'
+                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
+                xmlns:themes='clr-namespace:Avalonia.Themes.Simple;assembly=Avalonia.Themes.Simple'>
+    <ContentControl.Styles>
+        <themes:SimpleTheme />
+        <StyleInclude Source='avares://Avalonia.Themes.Simple/SimpleTheme.xaml'/>
+    </ContentControl.Styles>
+</ContentControl>");
+        Assert.IsType<SimpleTheme>(control.Styles[0]);
+        Assert.IsType<SimpleTheme>(control.Styles[1]);
+    }
+}

+ 0 - 39
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@@ -109,45 +109,6 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             }
         }
 
-        [Fact]
-        public void StyleInclude_Is_Built()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow
-                       .With(theme: () => new Styles())))
-            {
-                var xaml = @"
-<ContentControl xmlns='https://github.com/avaloniaui'>
-    <ContentControl.Styles>
-        <StyleInclude Source='avares://Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml'/>
-    </ContentControl.Styles>
-</ContentControl>";
-
-                var window = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
-                
-                Assert.IsType<Style>(window.Styles[0]);
-            }
-        }
-        
-        [Fact]
-        public void StyleInclude_Is_Built_Resources()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow
-                       .With(theme: () => new Styles())))
-            {
-                var xaml = @"
-<ContentControl xmlns='https://github.com/avaloniaui'
-                xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
-    <ContentControl.Resources>
-        <StyleInclude x:Key='Include' Source='avares://Avalonia.Markup.Xaml.UnitTests/Xaml/Style1.xaml'/>
-    </ContentControl.Resources>
-</ContentControl>";
-
-                var window = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
-
-                Assert.IsType<Style>(window.Resources["Include"]);
-            }
-        }
-        
         [Fact]
         public void Setter_Can_Contain_Template()
         {

+ 2 - 2
tests/Avalonia.Markup.Xaml.UnitTests/XamlTestBase.cs

@@ -20,8 +20,8 @@ namespace Avalonia.Markup.Xaml.UnitTests
         
         class TestXamlLoaderShim : AvaloniaXamlLoader.IRuntimeXamlLoader
         {
-            public object Load(Stream stream, RuntimeXamlLoaderConfiguration configuration) 
-                => AvaloniaRuntimeXamlLoader.Load(stream, configuration);
+            public object Load(RuntimeXamlLoaderDocument document, RuntimeXamlLoaderConfiguration configuration) 
+                => AvaloniaRuntimeXamlLoader.Load(document, configuration);
         }
     }
 }