diff --git a/samples/ControlCatalog.Desktop/Program.cs b/samples/ControlCatalog.Desktop/Program.cs index 3fa5d32ae3..3a6f466b9e 100644 --- a/samples/ControlCatalog.Desktop/Program.cs +++ b/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(this AppT app, Assembly assembly) where AppT : Application - { - // Asset loading searches our own assembly? - PerspexLocator.CurrentMutable.GetService().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(); } diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs index 3f908aa2ce..9fb58c2a73 100644 --- a/src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs +++ b/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(); - 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; + } } } \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs b/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs index 68ae90d60f..8b9cd002e0 100644 --- a/src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs +++ b/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; /// /// Loads XAML for a perspex application. /// @@ -23,6 +23,7 @@ namespace Perspex.Markup.Xaml { private static PerspexParserFactory s_parserFactory; private static IInstanceLifeCycleListener s_lifeCycleListener = new PerspexLifeCycleListener(); + private static Stack s_uriStack = new Stack(); /// /// Initializes a new instance of the class. @@ -41,6 +42,11 @@ namespace Perspex.Markup.Xaml { } + /// + /// Gets the URI of the XAML file currently being loaded. + /// + internal static Uri UriContext => s_uriStack.Count > 0 ? s_uriStack.Peek() : null; + /// /// Loads the XAML into a Perspex component. /// @@ -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. /// /// The URI of the XAML file. + /// + /// A base URI to use if is relative. + /// /// /// The optional instance into which the XAML should be loaded. /// /// The loaded object. - public object Load(Uri uri, object rootInstance = null) + public object Load(Uri uri, Uri baseUri = null, object rootInstance = null) { Contract.Requires(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 /// /// The optional instance into which the XAML should be loaded. /// + /// The URI of the XAML /// The loaded object. - 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 + { + { "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() diff --git a/src/Markup/Perspex.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Perspex.Markup.Xaml/Styling/StyleInclude.cs index 6e74271f58..08641e0dfe 100644 --- a/src/Markup/Perspex.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Perspex.Markup.Xaml/Styling/StyleInclude.cs @@ -11,8 +11,20 @@ namespace Perspex.Markup.Xaml.Styling /// public class StyleInclude : IStyle { + private Uri _baseUri; private IStyle _loaded; + /// + /// Initializes a new instance of the class. + /// + 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; + } + /// /// Gets or sets the source URL. /// @@ -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; diff --git a/src/Perspex.Base/Platform/IAssetLoader.cs b/src/Perspex.Base/Platform/IAssetLoader.cs index 00f9d7e4d3..cd32d7bd3d 100644 --- a/src/Perspex.Base/Platform/IAssetLoader.cs +++ b/src/Perspex.Base/Platform/IAssetLoader.cs @@ -21,22 +21,27 @@ namespace Perspex.Platform /// void SetDefaultAssembly(Assembly asm); - /// /// Checks if an asset with the specified URI exists. /// /// The URI. + /// + /// A base URI to use if is relative. + /// /// True if the asset could be found; otherwise false. - bool Exists(Uri uri); + bool Exists(Uri uri, Uri baseUri = null); /// /// Opens the resource with the requested URI. /// /// The URI. + /// + /// A base URI to use if is relative. + /// /// A stream containing the resource contents. /// /// The resource was not found. /// - Stream Open(Uri uri); + Stream Open(Uri uri, Uri baseUri = null); } } diff --git a/src/Shared/PlatformSupport/AssetLoader.cs b/src/Shared/PlatformSupport/AssetLoader.cs index 86a4bee418..cc60f6de9c 100644 --- a/src/Shared/PlatformSupport/AssetLoader.cs +++ b/src/Shared/PlatformSupport/AssetLoader.cs @@ -15,26 +15,6 @@ namespace Perspex.Shared.PlatformSupport /// 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 Resources { get; } - public string Name { get; } - } - - private static readonly Dictionary AssemblyNameCache = new Dictionary(); @@ -53,7 +33,94 @@ namespace Perspex.Shared.PlatformSupport _defaultAssembly = new AssemblyDescriptor(assembly); } - AssemblyDescriptor GetAssembly(string name) + /// + /// Checks if an asset with the specified URI exists. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// True if the asset could be found; otherwise false. + public bool Exists(Uri uri, Uri baseUri = null) + { + return GetAsset(uri, baseUri) != null; + } + + /// + /// Opens the resource with the requested URI. + /// + /// The URI. + /// + /// A base URI to use if is relative. + /// + /// A stream containing the resource contents. + /// + /// The resource was not found. + /// + 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 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)); - } - - /// - /// Checks if an asset with the specified URI exists. - /// - /// The URI. - /// True if the asset could be found; otherwise false. - public bool Exists(Uri uri) - { - return GetAsset(uri) != null; - } - /// - /// Opens the resource with the requested URI. - /// - /// The URI. - /// A stream containing the resource contents. - /// - /// The resource was not found. - /// - 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 Resources { get; } + public string Name { get; } } } }