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.
pull/525/merge
Steven Kirk 10 years ago
parent
commit
cb98a09548
  1. 19
      samples/ControlCatalog.Desktop/Program.cs
  2. 10
      src/Markup/Perspex.Markup.Xaml/Converters/BitmapTypeConverter.cs
  3. 59
      src/Markup/Perspex.Markup.Xaml/PerspexXamlLoader.cs
  4. 14
      src/Markup/Perspex.Markup.Xaml/Styling/StyleInclude.cs
  5. 11
      src/Perspex.Base/Platform/IAssetLoader.cs
  6. 187
      src/Shared/PlatformSupport/AssetLoader.cs

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

10
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;
}
}
}

59
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()

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

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

187
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; }
}
}
}

Loading…
Cancel
Save