Browse Source

Supply a base URI when loading assets.

This allows assets to be loaded relative to the URI which loaded them,
e.g. a bitmap will now be loaded from the same assembly as the XAML file
that loads the bitmap rather than the entry assembly.
Steven Kirk 9 years ago
parent
commit
cb98a09548

+ 0 - 19
samples/ControlCatalog.Desktop/Program.cs

@@ -6,24 +6,6 @@ using Perspex;
 using System.Reflection;
 using Perspex.Platform;
 
-
-// Not sure where the best home for this is
-namespace Perspex
-{
-    public static class SharedApplicationExtensions
-    {
-        // For true Portable apps we need to select the PCL assembly NOT the host platform exe. Unfortunately
-        // Win32 subsystem registers one by default (the wrong one) and so this can override that.
-        //
-        public static AppT UseAssetAssembly<AppT>(this AppT app, Assembly assembly) where AppT : Application
-        {
-            // Asset loading searches our own assembly?
-            PerspexLocator.CurrentMutable.GetService<IAssetLoader>().SetDefaultAssembly(assembly);
-            return app;
-        }
-    }
-}
-
 namespace ControlCatalog
 {
     internal class Program
@@ -34,7 +16,6 @@ namespace ControlCatalog
 
             new App()
                 .ConfigureRenderSystem(args)
-                .UseAssetAssembly(typeof(App).Assembly)
                 .LoadFromXaml()
                 .RunWithMainWindow<MainWindow>();
         }

+ 9 - 1
src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs

@@ -24,6 +24,7 @@ namespace Perspex.Markup.Xaml.Converters
         public object ConvertFrom(IValueContext context, CultureInfo culture, object value)
         {
             var uri = new Uri((string)value, UriKind.RelativeOrAbsolute);
+            var baseUri = GetBaseUri(context);
             var scheme = uri.IsAbsoluteUri ? uri.Scheme : "file";
 
             switch (scheme)
@@ -32,7 +33,7 @@ namespace Perspex.Markup.Xaml.Converters
                     return new Bitmap((string)value);
                 default:
                     var assets = PerspexLocator.Current.GetService<IAssetLoader>();
-                    return new Bitmap(assets.Open(uri));
+                    return new Bitmap(assets.Open(uri, baseUri));
             }
         }
 
@@ -40,5 +41,12 @@ namespace Perspex.Markup.Xaml.Converters
         {
             throw new NotImplementedException();
         }
+
+        private Uri GetBaseUri(IValueContext context)
+        {
+            object result;
+            context.ParsingDictionary.TryGetValue("Uri", out result);
+            return result as Uri;
+        }
     }
 }

+ 44 - 15
src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs

@@ -15,7 +15,7 @@ namespace Perspex.Markup.Xaml
     using Controls;
     using Data;
     using OmniXaml.ObjectAssembler;
-
+    using System.Linq;
     /// <summary>
     /// Loads XAML for a perspex application.
     /// </summary>
@@ -23,6 +23,7 @@ namespace Perspex.Markup.Xaml
     {
         private static PerspexParserFactory s_parserFactory;
         private static IInstanceLifeCycleListener s_lifeCycleListener = new PerspexLifeCycleListener();
+        private static Stack<Uri> s_uriStack = new Stack<Uri>();
 
         /// <summary>
         /// Initializes a new instance of the <see cref="PerspexXamlLoader"/> class.
@@ -41,6 +42,11 @@ namespace Perspex.Markup.Xaml
         {
         }
 
+        /// <summary>
+        /// Gets the URI of the XAML file currently being loaded.
+        /// </summary>
+        internal static Uri UriContext => s_uriStack.Count > 0 ? s_uriStack.Peek() : null;
+
         /// <summary>
         /// Loads the XAML into a Perspex component.
         /// </summary>
@@ -84,7 +90,7 @@ namespace Perspex.Markup.Xaml
                     {
                         var initialize = rootInstance as ISupportInitialize;
                         initialize?.BeginInit();
-                        return Load(stream, rootInstance);
+                        return Load(stream, rootInstance, uri);
                     }
                 }
             }
@@ -96,11 +102,14 @@ namespace Perspex.Markup.Xaml
         /// 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, object rootInstance = null)
+        public object Load(Uri uri, Uri baseUri = null, object rootInstance = null)
         {
             Contract.Requires<ArgumentNullException>(uri != null);
 
@@ -112,9 +121,9 @@ namespace Perspex.Markup.Xaml
                     "Could not create IAssetLoader : maybe Application.RegisterServices() wasn't called?");
             }
 
-            using (var stream = assetLocator.Open(uri))
+            using (var stream = assetLocator.Open(uri, baseUri))
             {
-                return Load(stream, rootInstance);
+                return Load(stream, rootInstance, uri);
             }
         }
 
@@ -143,23 +152,43 @@ namespace Perspex.Markup.Xaml
         /// <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)
+        public object Load(Stream stream, object rootInstance = null, Uri uri = null)
         {
-            var result = base.Load(stream, new Settings
+            try
             {
-                RootInstance = rootInstance,
-                InstanceLifeCycleListener = s_lifeCycleListener,
-            });
+                if (uri != null)
+                {
+                    s_uriStack.Push(uri);
+                }
 
-            var topLevel = result as TopLevel;
+                var result = base.Load(stream, new Settings
+                {
+                    RootInstance = rootInstance,
+                    InstanceLifeCycleListener = s_lifeCycleListener,
+                    ParsingContext = new Dictionary<string, object>
+                    {
+                        { "Uri", uri }
+                    }
+                });
+
+                var topLevel = result as TopLevel;
+
+                if (topLevel != null)
+                {
+                    DelayedBinding.ApplyBindings(topLevel);
+                }
 
-            if (topLevel != null)
+                return result;
+            }
+            finally
             {
-                DelayedBinding.ApplyBindings(topLevel);
+                if (uri != null)
+                {
+                    s_uriStack.Pop();
+                }
             }
-
-            return result;
         }
 
         private static PerspexParserFactory GetParserFactory()

+ 13 - 1
src/Markup/Perspex.Markup.Xaml/Styling/StyleInclude.cs

@@ -11,8 +11,20 @@ namespace Perspex.Markup.Xaml.Styling
     /// </summary>
     public class StyleInclude : IStyle
     {
+        private Uri _baseUri;
         private IStyle _loaded;
 
+        /// <summary>
+        /// Initializes a new instance of the <see cref="StyleInclude"/> class.
+        /// </summary>
+        public StyleInclude()
+        {
+            // StyleInclude will usually be loaded from XAML and its URI can be relative to the
+            // XAML file that its included in, so store the current XAML file's URI if any as
+            // a base URI.
+            _baseUri = PerspexXamlLoader.UriContext;
+        }
+
         /// <summary>
         /// Gets or sets the source URL.
         /// </summary>
@@ -28,7 +40,7 @@ namespace Perspex.Markup.Xaml.Styling
                 if (_loaded == null)
                 {
                     var loader = new PerspexXamlLoader();
-                    _loaded = (IStyle)loader.Load(Source);
+                    _loaded = (IStyle)loader.Load(Source, _baseUri);
                 }
 
                 return _loaded;

+ 8 - 3
src/Perspex.Base/Platform/IAssetLoader.cs

@@ -21,22 +21,27 @@ namespace Perspex.Platform
         /// <param name="asm"></param>
         void SetDefaultAssembly(Assembly asm);
 
-
         /// <summary>
         /// Checks if an asset with the specified URI exists.
         /// </summary>
         /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
         /// <returns>True if the asset could be found; otherwise false.</returns>
-        bool Exists(Uri uri);
+        bool Exists(Uri uri, Uri baseUri = null);
 
         /// <summary>
         /// Opens the resource with the requested URI.
         /// </summary>
         /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
         /// <returns>A stream containing the resource contents.</returns>
         /// <exception cref="FileNotFoundException">
         /// The resource was not found.
         /// </exception>
-        Stream Open(Uri uri);
+        Stream Open(Uri uri, Uri baseUri = null);
     }
 }

+ 108 - 79
src/Shared/PlatformSupport/AssetLoader.cs

@@ -15,26 +15,6 @@ namespace Perspex.Shared.PlatformSupport
     /// </summary>
     public class AssetLoader : IAssetLoader
     {
-        class AssemblyDescriptor
-        {
-            public AssemblyDescriptor(Assembly assembly)
-            {
-                Assembly = assembly;
-
-                if (assembly != null)
-                {
-                    Resources = assembly.GetManifestResourceNames()
-                        .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n));
-                    Name = assembly.GetName().Name;
-                }
-            }
-
-            public Assembly Assembly { get; }
-            public Dictionary<string, IAssetDescriptor> Resources { get; }
-            public string Name { get; }
-        }
-
-
         private static readonly Dictionary<string, AssemblyDescriptor> AssemblyNameCache
             = new Dictionary<string, AssemblyDescriptor>();
 
@@ -53,7 +33,94 @@ namespace Perspex.Shared.PlatformSupport
             _defaultAssembly = new AssemblyDescriptor(assembly);
         }
 
-        AssemblyDescriptor GetAssembly(string name)
+        /// <summary>
+        /// Checks if an asset with the specified URI exists.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <returns>True if the asset could be found; otherwise false.</returns>
+        public bool Exists(Uri uri, Uri baseUri = null)
+        {
+            return GetAsset(uri, baseUri) != null;
+        }
+
+        /// <summary>
+        /// Opens the resource with the requested URI.
+        /// </summary>
+        /// <param name="uri">The URI.</param>
+        /// <param name="baseUri">
+        /// A base URI to use if <paramref name="uri"/> is relative.
+        /// </param>
+        /// <returns>A stream containing the resource contents.</returns>
+        /// <exception cref="FileNotFoundException">
+        /// The resource was not found.
+        /// </exception>
+        public Stream Open(Uri uri, Uri baseUri = null)
+        {
+            var asset = GetAsset(uri, baseUri);
+
+            if (asset == null)
+            {
+                throw new FileNotFoundException($"The resource {uri} could not be found.");
+            }
+
+            return asset.GetStream();
+        }
+
+        private IAssetDescriptor GetAsset(Uri uri, Uri baseUri)
+        {
+            if (!uri.IsAbsoluteUri || uri.Scheme == "resm")
+            {
+                var uriQueryParams = ParseQueryString(uri);
+                var baseUriQueryParams = uri != null ? ParseQueryString(uri) : null;
+                var asm = GetAssembly(uri) ?? GetAssembly(baseUri) ?? _defaultAssembly;
+
+                if (asm == null && _defaultAssembly == null)
+                {
+                    throw new ArgumentException(
+                        "No default assembly, entry assembly or explicit assembly specified; " +
+                        "don't know where to look up for the resource, try specifiyng assembly explicitly.");
+                }
+
+                IAssetDescriptor rv;
+
+                var resourceKey = uri.AbsolutePath;
+
+#if __IOS__
+                // TODO: HACK: to get iOS up and running. Using Shared projects for resources
+                // is flawed as this alters the reource key locations across platforms
+                // I think we need to use Portable libraries from now on to avoid that.
+                if(asm.Name.Contains("iOS"))
+                {
+                    resourceKey = resourceKey.Replace("TestApplication", "Perspex.iOSTestApplication");
+                }
+#endif
+
+                asm.Resources.TryGetValue(resourceKey, out rv);
+                return rv;
+            }
+            throw new ArgumentException($"Invalid uri, see https://github.com/Perspex/Perspex/issues/282#issuecomment-166982104", nameof(uri));
+        }
+
+        private AssemblyDescriptor GetAssembly(Uri uri)
+        {
+            if (uri != null)
+            {
+                var qs = ParseQueryString(uri);
+                string assemblyName;
+
+                if (qs.TryGetValue("assembly", out assemblyName))
+                {
+                    return GetAssembly(assemblyName);
+                }
+            }
+
+            return null;
+        }
+
+        private AssemblyDescriptor GetAssembly(string name)
         {
             if (name == null)
             {
@@ -82,13 +149,20 @@ namespace Perspex.Shared.PlatformSupport
             return rv;
         }
 
-        interface IAssetDescriptor
+        private Dictionary<string, string> ParseQueryString(Uri uri)
         {
-            Stream GetStream();
+            return uri.Query.TrimStart('?')
+                .Split(new[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
+                .Select(p => p.Split('='))
+                .ToDictionary(p => p[0], p => p[1]);
         }
 
+        private interface IAssetDescriptor
+        {
+            Stream GetStream();
+        }
 
-        class AssemblyResourceDescriptor : IAssetDescriptor
+        private class AssemblyResourceDescriptor : IAssetDescriptor
         {
             private readonly Assembly _asm;
             private readonly string _name;
@@ -104,69 +178,24 @@ namespace Perspex.Shared.PlatformSupport
                 return _asm.GetManifestResourceStream(_name);
             }
         }
-        
 
-        IAssetDescriptor GetAsset(Uri uri)
+        private class AssemblyDescriptor
         {
-            if (!uri.IsAbsoluteUri || uri.Scheme == "resm")
+            public AssemblyDescriptor(Assembly assembly)
             {
-                var qs = uri.Query.TrimStart('?')
-                    .Split(new[] {'&'}, StringSplitOptions.RemoveEmptyEntries)
-                    .Select(p => p.Split('='))
-                    .ToDictionary(p => p[0], p => p[1]);
-                //TODO: Replace _defaultAssembly by current one (need support from OmniXAML)
-                var asm = _defaultAssembly;
-                if (qs.ContainsKey("assembly"))
-                    asm = GetAssembly(qs["assembly"]);
-
-                if (asm == null && _defaultAssembly == null)
-                    throw new ArgumentException(
-                        "No defaultAssembly, entry assembly or explicit assembly specified, don't know where to look up for the resource, try specifiyng assembly explicitly");
-
-                IAssetDescriptor rv;
-
-                var resourceKey = uri.AbsolutePath;
+                Assembly = assembly;
 
-#if __IOS__
-                // TODO: HACK: to get iOS up and running. Using Shared projects for resources
-                // is flawed as this alters the reource key locations across platforms
-                // I think we need to use Portable libraries from now on to avoid that.
-                if(asm.Name.Contains("iOS"))
+                if (assembly != null)
                 {
-                    resourceKey = resourceKey.Replace("TestApplication", "Perspex.iOSTestApplication");
+                    Resources = assembly.GetManifestResourceNames()
+                        .ToDictionary(n => n, n => (IAssetDescriptor)new AssemblyResourceDescriptor(assembly, n));
+                    Name = assembly.GetName().Name;
                 }
-#endif
-
-                asm.Resources.TryGetValue(resourceKey, out rv);
-                return rv;
             }
-            throw new ArgumentException($"Invalid uri, see https://github.com/Perspex/Perspex/issues/282#issuecomment-166982104", nameof(uri));
-        }
-
-        /// <summary>
-        /// Checks if an asset with the specified URI exists.
-        /// </summary>
-        /// <param name="uri">The URI.</param>
-        /// <returns>True if the asset could be found; otherwise false.</returns>
-        public bool Exists(Uri uri)
-        {
-            return GetAsset(uri) != null;
-        }
 
-        /// <summary>
-        /// Opens the resource with the requested URI.
-        /// </summary>
-        /// <param name="uri">The URI.</param>
-        /// <returns>A stream containing the resource contents.</returns>
-        /// <exception cref="FileNotFoundException">
-        /// The resource was not found.
-        /// </exception>
-        public Stream Open(Uri uri)
-        {
-            var asset = GetAsset(uri);
-            if (asset == null)
-                throw new FileNotFoundException($"The resource {uri} could not be found.");
-            return asset.GetStream();
+            public Assembly Assembly { get; }
+            public Dictionary<string, IAssetDescriptor> Resources { get; }
+            public string Name { get; }
         }
     }
 }