1
0
Эх сурвалжийг харах

Merge pull request #1389 from AvaloniaUI/local-assembly-for-xaml

Support default clr-namespace when loading XAML from Uri or previewer
Steven Kirk 7 жил өмнө
parent
commit
c5b94d663b

+ 2 - 2
samples/BindingTest/MainWindow.xaml

@@ -1,7 +1,7 @@
 <Window xmlns="https://github.com/avaloniaui"
         xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
-        xmlns:vm="clr-namespace:BindingTest.ViewModels;assembly=BindingTest"
-        xmlns:local="clr-namespace:BindingTest;assembly=BindingTest">
+        xmlns:vm="clr-namespace:BindingTest.ViewModels"
+        xmlns:local="clr-namespace:BindingTest">
   <Window.Styles>
     <Style Selector="TextBlock.h1">
       <Setter Property="FontSize" Value="18"/>

+ 2 - 2
samples/ControlCatalog/DecoratedWindow.xaml

@@ -1,7 +1,7 @@
 <Window xmlns="https://github.com/avaloniaui" MinWidth="500" MinHeight="300"
         Title="Avalonia Control Gallery"
-        Icon="resm:ControlCatalog.Assets.test_icon.ico?assembly=ControlCatalog"
-        xmlns:local="clr-namespace:ControlCatalog;assembly=ControlCatalog" HasSystemDecorations="False">
+        Icon="resm:ControlCatalog.Assets.test_icon.ico"
+        xmlns:local="clr-namespace:ControlCatalog" HasSystemDecorations="False">
     <Grid RowDefinitions="5,*,5" ColumnDefinitions="5,*,5">
         <DockPanel  Grid.Column="1"  Grid.Row="1" >
             <Grid Name="TitleBar" Background="LightBlue" DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto">

+ 1 - 1
samples/ControlCatalog/MainView.xaml

@@ -1,5 +1,5 @@
 <UserControl xmlns="https://github.com/avaloniaui"
-        xmlns:pages="clr-namespace:ControlCatalog.Pages;assembly=ControlCatalog"
+        xmlns:pages="clr-namespace:ControlCatalog.Pages"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <TabControl Classes="sidebar" Name="Sidebar">
     <TabControl.Transition>

+ 1 - 1
samples/ControlCatalog/MainWindow.xaml

@@ -1,6 +1,6 @@
 <Window xmlns="https://github.com/avaloniaui" MinWidth="500" MinHeight="300"
         Title="Avalonia Control Gallery"
         Icon="resm:ControlCatalog.Assets.test_icon.ico?assembly=ControlCatalog"
-        xmlns:local="clr-namespace:ControlCatalog;assembly=ControlCatalog">
+        xmlns:local="clr-namespace:ControlCatalog">
     <local:MainView/>
 </Window>

+ 1 - 1
samples/RenderTest/MainWindow.xaml

@@ -1,6 +1,6 @@
 <Window xmlns="https://github.com/avaloniaui"
         Title="Avalonia Render Test"
-        xmlns:pages="clr-namespace:RenderTest.Pages;assembly=RenderTest">
+        xmlns:pages="clr-namespace:RenderTest.Pages">
   <DockPanel>
     <Menu DockPanel.Dock="Top">
       <MenuItem Header="Rendering">

+ 16 - 0
src/Avalonia.Base/Platform/IAssetLoader.cs

@@ -43,5 +43,21 @@ namespace Avalonia.Platform
         /// The resource was not found.
         /// </exception>
         Stream Open(Uri uri, Uri baseUri = null);
+
+        /// <summary>
+        /// Opens the resource with the requested URI and returns the resource string and the
+        /// assembly containing the resource.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <returns>
+        /// The stream containing the resource contents together with the assembly.
+        /// </returns>
+        /// <exception cref="FileNotFoundException">
+        /// The resource was not found.
+        /// </exception>
+        Tuple<Stream, Assembly> OpenAndGetAssembly(Uri uri, Uri baseUri = null);
     }
 }

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

@@ -1,6 +1,7 @@
 using System;
 using System.IO;
 using System.Linq;
+using System.Reflection;
 using System.Text;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
@@ -30,7 +31,8 @@ namespace Avalonia.DesignerSupport
                         new Uri("resm:Fake.xaml?assembly=" + Path.GetFileNameWithoutExtension(assemblyPath));
                 }
 
-                var loaded = loader.Load(stream, null, baseUri);
+                var localAsm = assemblyPath != null ? Assembly.LoadFile(Path.GetFullPath(assemblyPath)) : null;
+                var loaded = loader.Load(stream, localAsm, null, baseUri);
                 var styles = loaded as Styles;
                 if (styles != null)
                 {

+ 1 - 1
src/Avalonia.DesignerSupport/DesignerAssist.cs

@@ -24,7 +24,7 @@ namespace Avalonia.DesignerSupport
 
                 var loader = new AvaloniaXamlLoader();
                 var baseLight = (IStyle)loader.Load(
-                    new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"));
+                    new Uri("resm:Avalonia.Themes.Default.Accents.BaseLight.xaml?assembly=Avalonia.Themes.Default"), null);
                 Styles.Add(baseLight);
             }
         }

+ 1 - 1
src/Avalonia.Diagnostics/Views/TreePageView.xaml

@@ -1,5 +1,5 @@
 <UserControl xmlns="https://github.com/avaloniaui"
-             xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels;assembly=Avalonia.Diagnostics">
+             xmlns:vm="clr-namespace:Avalonia.Diagnostics.ViewModels">
   <Grid ColumnDefinitions="*,4,3*">    
     <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
       <TreeView.DataTemplates>

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

@@ -29,7 +29,6 @@
         <Compile Include="..\..\Shared\SharedAssemblyInfo.cs">
             <Link>Properties\SharedAssemblyInfo.cs</Link>
         </Compile>
-        <Compile Include="AvaloniaXamlLoaderPortableXaml.cs" />
         <Compile Include="AvaloniaXamlLoader.cs" />
         <Compile Include="Converters\CornerRadiusTypeConverter.cs" />
         <Compile Include="Converters\MatrixTypeConverter.cs" />

+ 232 - 6
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs

@@ -1,11 +1,237 @@
-namespace Avalonia.Markup.Xaml
+// Copyright (c) The Avalonia Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml.Data;
+using Avalonia.Markup.Xaml.PortableXaml;
+using Avalonia.Platform;
+using Portable.Xaml;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using System.Text;
+
+namespace Avalonia.Markup.Xaml
 {
-    public class AvaloniaXamlLoader : AvaloniaXamlLoaderPortableXaml
+    /// <summary>
+    /// Loads XAML for a avalonia application.
+    /// </summary>
+    public class AvaloniaXamlLoader
     {
-        public static object Parse(string xaml)
-                => new AvaloniaXamlLoader().Load(xaml);
+        private readonly AvaloniaXamlSchemaContext _context = GetContext();
+
+        private static AvaloniaXamlSchemaContext GetContext()
+        {
+            var result = AvaloniaLocator.Current.GetService<AvaloniaXamlSchemaContext>();
+
+            if (result == null)
+            {
+                result = AvaloniaXamlSchemaContext.Create();
+
+                AvaloniaLocator.CurrentMutable
+                    .Bind<AvaloniaXamlSchemaContext>()
+                    .ToConstant(result);
+            }
+
+            return result;
+        }
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="AvaloniaXamlLoader"/> class.
+        /// </summary>
+        public AvaloniaXamlLoader()
+        {
+        }
+
+        /// <summary>
+        /// Loads the XAML into a Avalonia component.
+        /// </summary>
+        /// <param name="obj">The object to load the XAML into.</param>
+        public static void Load(object obj)
+        {
+            Contract.Requires<ArgumentNullException>(obj != null);
+
+            var loader = new AvaloniaXamlLoader();
+            loader.Load(obj.GetType(), obj);
+        }
+
+        /// <summary>
+        /// Loads the XAML for a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <param name="rootInstance">
+        /// The optional instance into which the XAML should be loaded.
+        /// </param>
+        /// <returns>The loaded object.</returns>
+        public object Load(Type type, object rootInstance = null)
+        {
+            Contract.Requires<ArgumentNullException>(type != null);
+
+            // HACK: Currently Visual Studio is forcing us to change the extension of xaml files
+            // in certain situations, so we try to load .xaml and if that's not found we try .xaml.
+            // Ideally we'd be able to use .xaml everywhere
+            var assetLocator = AvaloniaLocator.Current.GetService<IAssetLoader>();
+
+            if (assetLocator == null)
+            {
+                throw new InvalidOperationException(
+                    "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?");
+            }
+
+            foreach (var uri in GetUrisFor(type))
+            {
+                if (assetLocator.Exists(uri))
+                {
+                    using (var stream = assetLocator.Open(uri))
+                    {
+                        var initialize = rootInstance as ISupportInitialize;
+                        initialize?.BeginInit();
+                        try
+                        {
+                            return Load(stream, type.Assembly, rootInstance, uri);
+                        }
+                        finally
+                        {
+                            initialize?.EndInit();
+                        }
+                    }
+                }
+            }
+
+            throw new FileNotFoundException("Unable to find view for " + type.FullName);
+        }
+
+        /// <summary>
+        /// Loads XAML from a URI.
+        /// </summary>
+        /// <param name="uri">The URI of the XAML file.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <param name="rootInstance">
+        /// The optional instance into which the XAML should be loaded.
+        /// </param>
+        /// <returns>The loaded object.</returns>
+        public object Load(Uri uri, Uri baseUri = null, object rootInstance = null)
+        {
+            Contract.Requires<ArgumentNullException>(uri != null);
+
+            var assetLocator = AvaloniaLocator.Current.GetService<IAssetLoader>();
+
+            if (assetLocator == null)
+            {
+                throw new InvalidOperationException(
+                    "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?");
+            }
+
+            var asset = assetLocator.OpenAndGetAssembly(uri, baseUri);
+            using (var stream = asset.Item1)
+            {
+                try
+                {
+                    return Load(stream, asset.Item2, rootInstance, uri);
+                }
+                catch (Exception e)
+                {
+                    var uriString = uri.ToString();
+                    if (!uri.IsAbsoluteUri)
+                    {
+                        uriString = new Uri(baseUri, uri).AbsoluteUri;
+                    }
+                    throw new XamlLoadException("Error loading xaml at " + uriString, e);
+                }
+            }
+        }
+
+        /// <summary>
+        /// Loads XAML from a string.
+        /// </summary>
+        /// <param name="xaml">The string containing the XAML.</param>
+        /// <param name="localAssembly">Default assembly for clr-namespace:</param>
+        /// <param name="rootInstance">
+        /// The optional instance into which the XAML should be loaded.
+        /// </param>
+        /// <returns>The loaded object.</returns>
+        public object Load(string xaml, Assembly localAssembly = null, object rootInstance = null)
+        {
+            Contract.Requires<ArgumentNullException>(xaml != null);
+
+            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)))
+            {
+                return Load(stream, localAssembly, rootInstance);
+            }
+        }
+
+        /// <summary>
+        /// Loads XAML from a stream.
+        /// </summary>
+        /// <param name="stream">The stream containing the XAML.</param>
+        /// <param name="localAssembly">Default assembly for clr-namespace</param>
+        /// <param name="rootInstance">
+        /// The optional instance into which the XAML should be loaded.
+        /// </param>
+        /// <param name="uri">The URI of the XAML</param>
+        /// <returns>The loaded object.</returns>
+        public object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null)
+        {
+            var readerSettings = new XamlXmlReaderSettings()
+            {
+                BaseUri = uri,
+                LocalAssembly = localAssembly
+            };
+
+            var reader = new XamlXmlReader(stream, _context, readerSettings);
+
+            object result = LoadFromReader(
+                reader,
+                AvaloniaXamlContext.For(readerSettings, rootInstance));
+
+            var topLevel = result as TopLevel;
+
+            if (topLevel != null)
+            {
+                DelayedBinding.ApplyBindings(topLevel);
+            }
+
+            return result;
+        }
+
+        internal static object LoadFromReader(XamlReader reader, AvaloniaXamlContext context = null, IAmbientProvider parentAmbientProvider = null)
+        {
+            var writer = AvaloniaXamlObjectWriter.Create(
+                                    reader.SchemaContext,
+                                    context,
+                                    parentAmbientProvider);
+
+            XamlServices.Transform(reader, writer);
+            writer.ApplyAllDelayedProperties();
+            return writer.Result;
+        }
+
+        internal static object LoadFromReader(XamlReader reader)
+        {
+            //return XamlServices.Load(reader);
+            return LoadFromReader(reader, null);
+        }
+
+        /// <summary>
+        /// Gets the URI for a type.
+        /// </summary>
+        /// <param name="type">The type.</param>
+        /// <returns>The URI.</returns>
+        private static IEnumerable<Uri> GetUrisFor(Type type)
+        {
+            var asm = type.GetTypeInfo().Assembly.GetName().Name;
+            var typeName = type.FullName;
+            yield return new Uri("resm:" + typeName + ".xaml?assembly=" + asm);
+            yield return new Uri("resm:" + typeName + ".paml?assembly=" + asm);
+        }
+        
+        public static object Parse(string xaml, Assembly localAssembly = null)
+            => new AvaloniaXamlLoader().Load(xaml, localAssembly);
 
-        public static T Parse<T>(string xaml)
-                     => (T)Parse(xaml);
+        public static T Parse<T>(string xaml, Assembly localAssembly = null)
+            => (T)Parse(xaml, localAssembly);
     }
 }

+ 0 - 228
src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoaderPortableXaml.cs

@@ -1,228 +0,0 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml.Data;
-using Avalonia.Markup.Xaml.PortableXaml;
-using Avalonia.Platform;
-using Portable.Xaml;
-using System;
-using System.Collections.Generic;
-using System.IO;
-using System.Reflection;
-using System.Text;
-
-namespace Avalonia.Markup.Xaml
-{
-    /// <summary>
-    /// Loads XAML for a avalonia application.
-    /// </summary>
-    public class AvaloniaXamlLoaderPortableXaml
-    {
-        private readonly AvaloniaXamlSchemaContext _context = GetContext();
-
-        private static AvaloniaXamlSchemaContext GetContext()
-        {
-            var result = AvaloniaLocator.Current.GetService<AvaloniaXamlSchemaContext>();
-
-            if (result == null)
-            {
-                result = AvaloniaXamlSchemaContext.Create();
-
-                AvaloniaLocator.CurrentMutable
-                    .Bind<AvaloniaXamlSchemaContext>()
-                    .ToConstant(result);
-            }
-
-            return result;
-        }
-
-        /// <summary>
-        /// Initializes a new instance of the <see cref="AvaloniaXamlLoader"/> class.
-        /// </summary>
-        public AvaloniaXamlLoaderPortableXaml()
-        {
-        }
-
-        /// <summary>
-        /// Loads the XAML into a Avalonia component.
-        /// </summary>
-        /// <param name="obj">The object to load the XAML into.</param>
-        public static void Load(object obj)
-        {
-            Contract.Requires<ArgumentNullException>(obj != null);
-
-            var loader = new AvaloniaXamlLoader();
-            loader.Load(obj.GetType(), obj);
-        }
-
-        /// <summary>
-        /// Loads the XAML for a type.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <param name="rootInstance">
-        /// The optional instance into which the XAML should be loaded.
-        /// </param>
-        /// <returns>The loaded object.</returns>
-        public object Load(Type type, object rootInstance = null)
-        {
-            Contract.Requires<ArgumentNullException>(type != null);
-
-            // HACK: Currently Visual Studio is forcing us to change the extension of xaml files
-            // in certain situations, so we try to load .xaml and if that's not found we try .xaml.
-            // Ideally we'd be able to use .xaml everywhere
-            var assetLocator = AvaloniaLocator.Current.GetService<IAssetLoader>();
-
-            if (assetLocator == null)
-            {
-                throw new InvalidOperationException(
-                    "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?");
-            }
-
-            foreach (var uri in GetUrisFor(type))
-            {
-                if (assetLocator.Exists(uri))
-                {
-                    using (var stream = assetLocator.Open(uri))
-                    {
-                        var initialize = rootInstance as ISupportInitialize;
-                        initialize?.BeginInit();
-                        try
-                        {
-                            return Load(stream, rootInstance, uri);
-                        }
-                        finally
-                        {
-                            initialize?.EndInit();
-                        }
-                    }
-                }
-            }
-
-            throw new FileNotFoundException("Unable to find view for " + type.FullName);
-        }
-
-        /// <summary>
-        /// Loads XAML from a URI.
-        /// </summary>
-        /// <param name="uri">The URI of the XAML file.</param>
-        /// <param name="baseUri">
-        /// A base URI to use if <paramref name="uri"/> is relative.
-        /// </param>
-        /// <param name="rootInstance">
-        /// The optional instance into which the XAML should be loaded.
-        /// </param>
-        /// <returns>The loaded object.</returns>
-        public object Load(Uri uri, Uri baseUri = null, object rootInstance = null)
-        {
-            Contract.Requires<ArgumentNullException>(uri != null);
-
-            var assetLocator = AvaloniaLocator.Current.GetService<IAssetLoader>();
-
-            if (assetLocator == null)
-            {
-                throw new InvalidOperationException(
-                    "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?");
-            }
-
-            using (var stream = assetLocator.Open(uri, baseUri))
-            {
-                try
-                {
-                    return Load(stream, rootInstance, uri);
-                }
-                catch (Exception e)
-                {
-                    var uriString = uri.ToString();
-                    if (!uri.IsAbsoluteUri)
-                    {
-                        uriString = new Uri(baseUri, uri).AbsoluteUri;
-                    }
-                    throw new XamlLoadException("Error loading xaml at " + uriString, e);
-                }
-            }
-        }
-
-        /// <summary>
-        /// Loads XAML from a string.
-        /// </summary>
-        /// <param name="xaml">The string containing the XAML.</param>
-        /// <param name="rootInstance">
-        /// The optional instance into which the XAML should be loaded.
-        /// </param>
-        /// <returns>The loaded object.</returns>
-        public object Load(string xaml, object rootInstance = null)
-        {
-            Contract.Requires<ArgumentNullException>(xaml != null);
-
-            using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml)))
-            {
-                return Load(stream, rootInstance);
-            }
-        }
-
-        /// <summary>
-        /// Loads XAML from a stream.
-        /// </summary>
-        /// <param name="stream">The stream containing the XAML.</param>
-        /// <param name="rootInstance">
-        /// The optional instance into which the XAML should be loaded.
-        /// </param>
-        /// <param name="uri">The URI of the XAML</param>
-        /// <returns>The loaded object.</returns>
-        public object Load(Stream stream, object rootInstance = null, Uri uri = null)
-        {
-            var readerSettings = new XamlXmlReaderSettings()
-            {
-                BaseUri = uri,
-                LocalAssembly = rootInstance?.GetType().GetTypeInfo().Assembly
-            };
-
-            var reader = new XamlXmlReader(stream, _context, readerSettings);
-
-            object result = LoadFromReader(
-                reader,
-                AvaloniaXamlContext.For(readerSettings, rootInstance));
-
-            var topLevel = result as TopLevel;
-
-            if (topLevel != null)
-            {
-                DelayedBinding.ApplyBindings(topLevel);
-            }
-
-            return result;
-        }
-
-        internal static object LoadFromReader(XamlReader reader, AvaloniaXamlContext context = null, IAmbientProvider parentAmbientProvider = null)
-        {
-            var writer = AvaloniaXamlObjectWriter.Create(
-                                    reader.SchemaContext,
-                                    context,
-                                    parentAmbientProvider);
-
-            XamlServices.Transform(reader, writer);
-            writer.ApplyAllDelayedProperties();
-            return writer.Result;
-        }
-
-        internal static object LoadFromReader(XamlReader reader)
-        {
-            //return XamlServices.Load(reader);
-            return LoadFromReader(reader, null);
-        }
-
-        /// <summary>
-        /// Gets the URI for a type.
-        /// </summary>
-        /// <param name="type">The type.</param>
-        /// <returns>The URI.</returns>
-        private static IEnumerable<Uri> GetUrisFor(Type type)
-        {
-            var asm = type.GetTypeInfo().Assembly.GetName().Name;
-            var typeName = type.FullName;
-            yield return new Uri("resm:" + typeName + ".xaml?assembly=" + asm);
-            yield return new Uri("resm:" + typeName + ".paml?assembly=" + asm);
-        }
-    }
-}

+ 21 - 2
src/Shared/PlatformSupport/AssetLoader.cs

@@ -67,7 +67,23 @@ namespace Avalonia.Shared.PlatformSupport
         /// <exception cref="FileNotFoundException">
         /// The resource was not found.
         /// </exception>
-        public Stream Open(Uri uri, Uri baseUri = null)
+        public Stream Open(Uri uri, Uri baseUri = null) => OpenAndGetAssembly(uri, baseUri).Item1;
+        
+        /// <summary>
+        /// Opens the resource with the requested URI and returns the resource string and the
+        /// assembly containing the resource.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <returns>
+        /// The stream containing the resource contents together with the assembly.
+        /// </returns>
+        /// <exception cref="FileNotFoundException">
+        /// The resource was not found.
+        /// </exception>
+        public Tuple<Stream, Assembly> OpenAndGetAssembly(Uri uri, Uri baseUri = null)
         {
             var asset = GetAsset(uri, baseUri);
 
@@ -76,7 +92,7 @@ namespace Avalonia.Shared.PlatformSupport
                 throw new FileNotFoundException($"The resource {uri} could not be found.");
             }
 
-            return asset.GetStream();
+            return Tuple.Create(asset.GetStream(), asset.Assembly);
         }
 
         private IAssetDescriptor GetAsset(Uri uri, Uri baseUri)
@@ -162,6 +178,7 @@ namespace Avalonia.Shared.PlatformSupport
         private interface IAssetDescriptor
         {
             Stream GetStream();
+            Assembly Assembly { get; }
         }
 
         private class AssemblyResourceDescriptor : IAssetDescriptor
@@ -179,6 +196,8 @@ namespace Avalonia.Shared.PlatformSupport
             {
                 return _asm.GetManifestResourceStream(_name);
             }
+
+            public Assembly Assembly => _asm;
         }
 
         private class AssemblyDescriptor

+ 5 - 0
tests/Avalonia.UnitTests/MockAssetLoader.cs

@@ -27,6 +27,11 @@ namespace Avalonia.UnitTests
         {
             return new MemoryStream(Encoding.UTF8.GetBytes(_assets[uri]));
         }
+        
+        public Tuple<Stream, Assembly> OpenAndGetAssembly(Uri uri, Uri baseUri = null)
+        {
+            return Tuple.Create(Open(uri, baseUri), (Assembly)null);
+        }
 
         public void SetDefaultAssembly(Assembly asm)
         {