From 43143bd10f2825f0b37991596523a5ca38864105 Mon Sep 17 00:00:00 2001 From: Stephane Delcroix Date: Fri, 3 Nov 2017 10:22:07 +0100 Subject: [PATCH] [Xaml[C]] ResourceDictionary.Source (#1229) * [Xaml[C]] ResourceDictionary.Source * [Xaml[C]] update to the new XamlResourceIdAttribute * [Xaml[C]] load RD with codebehind even when referenced through Source * [Xaml] make sure we do not generate file:// uri * fix typo --- .../BindablePropertyConverter.cs | 4 +- .../BindingTypeConverter.cs | 5 +- .../CompiledConverters/BoundsTypeConverter.cs | 5 +- .../CompiledConverters/ColorTypeConverter.cs | 5 +- .../ConstraintTypeConverter.cs | 5 +- .../ICompiledTypeConverter.cs | 5 +- .../LayoutOptionsConverter.cs | 5 +- .../ListStringTypeConverter.cs | 4 +- .../RDSourceTypeConverter.cs | 87 +++++++++++++++ .../RectangleTypeConverter.cs | 5 +- .../ThicknessTypeConverter.cs | 5 +- .../CompiledConverters/TypeTypeConverter.cs | 5 +- .../CompiledConverters/UriTypeConverter.cs | 17 ++- Xamarin.Forms.Build.Tasks/NodeILExtensions.cs | 12 +-- .../Xamarin.Forms.Build.Tasks.csproj | 1 + Xamarin.Forms.Core/ResourceDictionary.cs | 93 +++++++++++++++- Xamarin.Forms.Core/Xamarin.Forms.Core.csproj | 1 + Xamarin.Forms.Core/Xaml/IResourcesLoader.cs | 11 ++ .../Xaml/XamlResourceIdAttribute.cs | 39 +++++++ .../DefaultCtorRouting.xaml.cs | 3 +- .../Issues/Bz43733.xaml.cs | 2 + .../ResourceDictionaryWithSource.xaml | 14 +++ .../ResourceDictionaryWithSource.xaml.cs | 54 ++++++++++ .../TestSharedResourceDictionary.xaml.cs | 2 + .../Xamarin.Forms.Xaml.UnitTests.csproj | 6 ++ Xamarin.Forms.Xaml/ResourcesLoader.cs | 25 +++++ Xamarin.Forms.Xaml/Xamarin.Forms.Xaml.csproj | 1 + Xamarin.Forms.Xaml/XamlLoader.cs | 11 +- ...sourceDictionary+RDSourceTypeConverter.xml | 101 ++++++++++++++++++ .../Xamarin.Forms/ResourceDictionary.xml | 54 ++++++++++ docs/Xamarin.Forms.Core/index.xml | 1 + docs/Xamarin.Forms.Xaml/index.xml | 3 + 32 files changed, 552 insertions(+), 39 deletions(-) create mode 100644 Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs create mode 100644 Xamarin.Forms.Core/Xaml/IResourcesLoader.cs create mode 100644 Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml create mode 100644 Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml.cs create mode 100644 Xamarin.Forms.Xaml/ResourcesLoader.cs create mode 100644 docs/Xamarin.Forms.Core/Xamarin.Forms/ResourceDictionary+RDSourceTypeConverter.xml diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs index 5010c020e47..45fcd7b71cf 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/BindablePropertyConverter.cs @@ -12,8 +12,10 @@ namespace Xamarin.Forms.Core.XamlC { class BindablePropertyConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (IsNullOrEmpty(value)) { yield return Instruction.Create(OpCodes.Ldnull); yield break; diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/BindingTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/BindingTypeConverter.cs index 8c8f0411abd..16827e4b5c2 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/BindingTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/BindingTypeConverter.cs @@ -7,13 +7,16 @@ using Xamarin.Forms.Xaml; using static System.String; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class BindingTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (IsNullOrEmpty(value)) throw new XamlParseException($"Cannot convert \"{value}\" into {typeof(Binding)}", node); diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/BoundsTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/BoundsTypeConverter.cs index 88550bf14e3..440303e1e89 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/BoundsTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/BoundsTypeConverter.cs @@ -7,13 +7,16 @@ using Mono.Cecil.Cil; using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class BoundsTypeConverter : ICompiledTypeConverter { - IEnumerable ICompiledTypeConverter.ConvertFromString(string value, ModuleDefinition module, BaseNode node) + IEnumerable ICompiledTypeConverter.ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (string.IsNullOrEmpty(value)) throw new XamlParseException($"Cannot convert \"{value}\" into {typeof(Rectangle)}", node); diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/ColorTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/ColorTypeConverter.cs index cfb9d0165aa..41b1ed508d8 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/ColorTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/ColorTypeConverter.cs @@ -6,13 +6,16 @@ using Mono.Cecil.Cil; using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class ColorTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + do { if (string.IsNullOrEmpty(value)) break; diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/ConstraintTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/ConstraintTypeConverter.cs index e166247da2a..5b29716d74c 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/ConstraintTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/ConstraintTypeConverter.cs @@ -6,13 +6,16 @@ using Mono.Cecil.Cil; using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class ConstraintTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + double size; if (string.IsNullOrEmpty(value) || !double.TryParse(value, NumberStyles.Number, CultureInfo.InvariantCulture, out size)) diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/ICompiledTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/ICompiledTypeConverter.cs index d594cb73621..908fd6f3294 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/ICompiledTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/ICompiledTypeConverter.cs @@ -3,12 +3,13 @@ using Mono.Cecil; using Xamarin.Forms.Xaml; using System; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Xaml { interface ICompiledTypeConverter { - IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node); + IEnumerable ConvertFromString(string value, ILContext context, BaseNode node); } } @@ -17,7 +18,7 @@ namespace Xamarin.Forms.Core.XamlC //only used in unit tests to make sure the compiled InitializeComponent is invoked class IsCompiledTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { if (value != "IsCompiled?") throw new Exception(); diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/LayoutOptionsConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/LayoutOptionsConverter.cs index d7c4597f616..cf1a4997a95 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/LayoutOptionsConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/LayoutOptionsConverter.cs @@ -6,13 +6,16 @@ using Mono.Cecil.Cil; using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class LayoutOptionsConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + do { if (string.IsNullOrEmpty(value)) break; diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/ListStringTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/ListStringTypeConverter.cs index 3f1c05697ad..a8069a9cd42 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/ListStringTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/ListStringTypeConverter.cs @@ -12,8 +12,10 @@ namespace Xamarin.Forms.Core.XamlC { class ListStringTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (value == null) { yield return Instruction.Create(OpCodes.Ldnull); yield break; diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs new file mode 100644 index 00000000000..b5417331e08 --- /dev/null +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/RDSourceTypeConverter.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Mono.Cecil; +using Mono.Cecil.Cil; + +using static Mono.Cecil.Cil.Instruction; +using static Mono.Cecil.Cil.OpCodes; + +using Xamarin.Forms.Build.Tasks; +using Xamarin.Forms.Xaml; + +namespace Xamarin.Forms.Core.XamlC +{ + class RDSourceTypeConverter : ICompiledTypeConverter + { + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) + { + var module = context.Body.Method.Module; + var body = context.Body; + + INode rootNode = node; + while (!(rootNode is ILRootNode)) + rootNode = rootNode.Parent; + + var rdNode = node.Parent as IElementNode; + + var rootTargetPath = GetPathForType(module, ((ILRootNode)rootNode).TypeReference); + var uri = new Uri(value, UriKind.Relative); + + var resourceId = ResourceDictionary.RDSourceTypeConverter.GetResourceId(uri, rootTargetPath, s => GetResourceIdForPath(module, s)); + //abuse the converter, produce some side effect, but leave the stack untouched + //public void SetAndLoadSource(Uri value, string resourceID, Assembly assembly, System.Xml.IXmlLineInfo lineInfo) + yield return Create(Ldloc, context.Variables[rdNode]); //the resourcedictionary + foreach (var instruction in (new UriTypeConverter()).ConvertFromString(value, context, node)) + yield return instruction; //the Uri + + //keep the Uri for later + yield return Create(Dup); + var uriVarDef = new VariableDefinition(module.ImportReference(typeof(Uri))); + body.Variables.Add(uriVarDef); + yield return Create(Stloc, uriVarDef); + + yield return Create(Ldstr, resourceId); //resourceId + + var getTypeFromHandle = module.ImportReference(typeof(Type).GetMethod("GetTypeFromHandle", new[] { typeof(RuntimeTypeHandle) })); + var getAssembly = module.ImportReference(typeof(Type).GetProperty("Assembly").GetGetMethod()); + yield return Create(Ldtoken, module.ImportReference(((ILRootNode)rootNode).TypeReference)); + yield return Create(Call, module.ImportReference(getTypeFromHandle)); + yield return Create(Callvirt, module.ImportReference(getAssembly)); //assembly + + foreach (var instruction in node.PushXmlLineInfo(context)) + yield return instruction; //lineinfo + + var setAndLoadSource = module.ImportReference(typeof(ResourceDictionary).GetMethod("SetAndLoadSource")); + yield return Create(Callvirt, module.ImportReference(setAndLoadSource)); + + //ldloc the stored uri as return value + yield return Create(Ldloc, uriVarDef); + } + + static string GetPathForType(ModuleDefinition module, TypeReference type) + { + foreach (var ca in type.Module.GetCustomAttributes()) { + if (!TypeRefComparer.Default.Equals(ca.AttributeType, module.ImportReference(typeof(XamlResourceIdAttribute)))) + continue; + if (!TypeRefComparer.Default.Equals(ca.ConstructorArguments[2].Value as TypeReference, type)) + continue; + return ca.ConstructorArguments[1].Value as string; + } + return null; + } + + static string GetResourceIdForPath(ModuleDefinition module, string path) + { + foreach (var ca in module.GetCustomAttributes()) { + if (!TypeRefComparer.Default.Equals(ca.AttributeType, module.ImportReference(typeof(XamlResourceIdAttribute)))) + continue; + if (ca.ConstructorArguments[1].Value as string != path) + continue; + return ca.ConstructorArguments[0].Value as string; + } + return null; + } + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/RectangleTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/RectangleTypeConverter.cs index 1175dd221a7..db7d4d02263 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/RectangleTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/RectangleTypeConverter.cs @@ -6,13 +6,16 @@ using Mono.Cecil.Cil; using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class RectangleTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (string.IsNullOrEmpty(value)) throw new XamlParseException($"Cannot convert \"{value}\" into {typeof(Rectangle)}", node); double x, y, w, h; diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/ThicknessTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/ThicknessTypeConverter.cs index adca4d89851..e2ef44bde04 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/ThicknessTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/ThicknessTypeConverter.cs @@ -6,13 +6,16 @@ using Mono.Cecil.Cil; using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class ThicknessTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (!string.IsNullOrEmpty(value)) { double l, t, r, b; var thickness = value.Split(','); diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/TypeTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/TypeTypeConverter.cs index b8f02cc17ad..30626749943 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/TypeTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/TypeTypeConverter.cs @@ -12,8 +12,10 @@ namespace Xamarin.Forms.Core.XamlC { class TypeTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (string.IsNullOrEmpty(value)) goto error; @@ -40,5 +42,4 @@ public IEnumerable ConvertFromString(string value, ModuleDefinition throw new XamlParseException($"Cannot convert \"{value}\" into {typeof(Type)}", node); } } - } \ No newline at end of file diff --git a/Xamarin.Forms.Build.Tasks/CompiledConverters/UriTypeConverter.cs b/Xamarin.Forms.Build.Tasks/CompiledConverters/UriTypeConverter.cs index 534a8badabf..c14b3a557b1 100644 --- a/Xamarin.Forms.Build.Tasks/CompiledConverters/UriTypeConverter.cs +++ b/Xamarin.Forms.Build.Tasks/CompiledConverters/UriTypeConverter.cs @@ -4,25 +4,32 @@ using Mono.Cecil; using Mono.Cecil.Cil; + +using static Mono.Cecil.Cil.Instruction; +using static Mono.Cecil.Cil.OpCodes; + using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Core.XamlC { class UriTypeConverter : ICompiledTypeConverter { - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { + var module = context.Body.Method.Module; + if (string.IsNullOrWhiteSpace(value)) { - yield return Instruction.Create(OpCodes.Ldnull); + yield return Create(Ldnull); yield break; } var uriCtor = module.ImportReference(typeof(Uri)).Resolve().Methods.FirstOrDefault(md => md.IsConstructor && md.Parameters.Count == 2 && md.Parameters[1].ParameterType.FullName == "System.UriKind"); var uriCtorRef = module.ImportReference(uriCtor); - yield return Instruction.Create(OpCodes.Ldstr, value); - yield return Instruction.Create(OpCodes.Ldc_I4_0); //UriKind.RelativeOrAbsolute - yield return Instruction.Create(OpCodes.Newobj, uriCtorRef); + yield return Create(Ldstr, value); + yield return Create(Ldc_I4_0); //UriKind.RelativeOrAbsolute + yield return Create(Newobj, uriCtorRef); } } } \ No newline at end of file diff --git a/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs b/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs index ca0f9fd6413..34e93e40dc8 100644 --- a/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs +++ b/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs @@ -58,7 +58,7 @@ public static IEnumerable PushConvertedValue(this ValueNode node, I var compiledConverter = Activator.CreateInstance (compiledConverterType); var converter = typeof(ICompiledTypeConverter).GetMethods ().FirstOrDefault (md => md.Name == "ConvertFromString"); var instructions = (IEnumerable)converter.Invoke (compiledConverter, new object[] { - node.Value as string, context.Body.Method.Module, node as BaseNode}); + node.Value as string, context, node as BaseNode}); foreach (var i in instructions) yield return i; if (targetTypeRef.IsValueType && boxValueTypes) @@ -310,20 +310,18 @@ public static IEnumerable PushXmlLineInfo(this INode node, ILContex var module = context.Body.Method.Module; var xmlLineInfo = node as IXmlLineInfo; - if (xmlLineInfo == null) - { + if (xmlLineInfo == null) { yield return Instruction.Create(OpCodes.Ldnull); yield break; } MethodReference ctor; - if (xmlLineInfo.HasLineInfo()) - { + if (xmlLineInfo.HasLineInfo()) { yield return Instruction.Create(OpCodes.Ldc_I4, xmlLineInfo.LineNumber); yield return Instruction.Create(OpCodes.Ldc_I4, xmlLineInfo.LinePosition); - ctor = module.ImportReference(typeof (XmlLineInfo).GetConstructor(new[] { typeof (int), typeof (int) })); + ctor = module.ImportReference(typeof(XmlLineInfo).GetConstructor(new[] { typeof(int), typeof(int) })); } else - ctor = module.ImportReference(typeof (XmlLineInfo).GetConstructor(new Type[] { })); + ctor = module.ImportReference(typeof(XmlLineInfo).GetConstructor(new Type[] { })); yield return Instruction.Create(OpCodes.Newobj, ctor); } diff --git a/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj b/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj index 9bf039f0d26..6d171f9371c 100644 --- a/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj +++ b/Xamarin.Forms.Build.Tasks/Xamarin.Forms.Build.Tasks.csproj @@ -101,6 +101,7 @@ + diff --git a/Xamarin.Forms.Core/ResourceDictionary.cs b/Xamarin.Forms.Core/ResourceDictionary.cs index b3cb321463f..f72d691de4e 100644 --- a/Xamarin.Forms.Core/ResourceDictionary.cs +++ b/Xamarin.Forms.Core/ResourceDictionary.cs @@ -1,12 +1,16 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; + using Xamarin.Forms.Internals; -using System.Collections.ObjectModel; -using System.Collections.Specialized; +using Xamarin.Forms.Xaml; namespace Xamarin.Forms { @@ -16,14 +20,19 @@ public class ResourceDictionary : IResourceDictionary, IDictionary _innerDictionary = new Dictionary(); ResourceDictionary _mergedInstance; Type _mergedWith; + Uri _source; - [TypeConverter (typeof(TypeTypeConverter))] + [TypeConverter(typeof(TypeTypeConverter))] + [Obsolete("Use Source")] public Type MergedWith { get { return _mergedWith; } set { if (_mergedWith == value) return; + if (_source != null) + throw new ArgumentException("MergedWith can not be used with Source"); + if (!typeof(ResourceDictionary).GetTypeInfo().IsAssignableFrom(value.GetTypeInfo())) throw new ArgumentException("MergedWith should inherit from ResourceDictionary"); @@ -36,6 +45,33 @@ public Type MergedWith { } } + [TypeConverter(typeof(RDSourceTypeConverter))] + public Uri Source { + get { return _source; } + set { + if (_source == value) + return; + throw new InvalidOperationException("Source can only be set from XAML."); //through the RDSourceTypeConverter + } + } + + //Used by the XamlC compiled converter + [EditorBrowsable(EditorBrowsableState.Never)] + public void SetAndLoadSource(Uri value, string resourceID, Assembly assembly, System.Xml.IXmlLineInfo lineInfo) + { + _source = value; + if (_mergedWith != null) + throw new ArgumentException("Source can not be used with MergedWith"); + + //this will return a type if the RD as an x:Class element, and codebehind + var type = XamlResourceIdAttribute.GetTypeForResourceId(assembly, resourceID); + if (type != null) + _mergedInstance = s_instances.GetValue(type, (key) => (ResourceDictionary)Activator.CreateInstance(key)); + else + _mergedInstance = DependencyService.Get().CreateResourceDictionary(resourceID, assembly, lineInfo); + OnValuesChanged(_mergedInstance.ToArray()); + } + ICollection _mergedDictionaries; public ICollection MergedDictionaries { get { @@ -256,5 +292,54 @@ void OnValuesChanged(params KeyValuePair[] values) } event EventHandler ValuesChanged; + + [Xaml.ProvideCompiled("Xamarin.Forms.Core.XamlC.RDSourceTypeConverter")] + public class RDSourceTypeConverter : TypeConverter, IExtendedTypeConverter + { + object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceProvider serviceProvider) + { + if (serviceProvider == null) + throw new ArgumentNullException(nameof(serviceProvider)); + + var targetRD = (serviceProvider.GetService(typeof(Xaml.IProvideValueTarget)) as Xaml.IProvideValueTarget)?.TargetObject as ResourceDictionary; + if (targetRD == null) + return null; + + var rootObjectType = (serviceProvider.GetService(typeof(Xaml.IRootObjectProvider)) as Xaml.IRootObjectProvider)?.RootObject.GetType(); + if (rootObjectType == null) + return null; + + var lineInfo = (serviceProvider.GetService(typeof(Xaml.IXmlLineInfoProvider)) as Xaml.IXmlLineInfoProvider)?.XmlLineInfo; + var rootTargetPath = XamlResourceIdAttribute.GetPathForType(rootObjectType); + var uri = new Uri(value, UriKind.Relative); //we don't want file:// uris, even if they start with '/' + var resourceId = GetResourceId(uri, rootTargetPath, + s => XamlResourceIdAttribute.GetResourceIdForPath(rootObjectType.GetTypeInfo().Assembly, s)); + targetRD.SetAndLoadSource(uri, resourceId, rootObjectType.GetTypeInfo().Assembly, lineInfo); + return uri; + } + + internal static string GetResourceId(Uri uri, string rootTargetPath, Func getResourceIdForPath) + { + //need a fake scheme so it's not seen as file:// uri, and the forward slashes are valid on all plats + var resourceUri = uri.OriginalString.StartsWith("/", StringComparison.Ordinal) + ? new Uri($"pack://{uri.OriginalString}", UriKind.Absolute) + : new Uri($"pack:///{rootTargetPath}/../{uri.OriginalString}", UriKind.Absolute); + + //drop the leading '/' + var resourcePath = resourceUri.AbsolutePath.Substring(1); + + return getResourceIdForPath(resourcePath); + } + + object IExtendedTypeConverter.ConvertFrom(CultureInfo culture, object value, IServiceProvider serviceProvider) + { + throw new NotImplementedException(); + } + + public override object ConvertFromInvariantString(string value) + { + throw new NotImplementedException(); + } + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj b/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj index 1aaae10071b..52b33ca4aa3 100644 --- a/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj +++ b/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj @@ -461,6 +461,7 @@ + diff --git a/Xamarin.Forms.Core/Xaml/IResourcesLoader.cs b/Xamarin.Forms.Core/Xaml/IResourcesLoader.cs new file mode 100644 index 00000000000..8e92431d148 --- /dev/null +++ b/Xamarin.Forms.Core/Xaml/IResourcesLoader.cs @@ -0,0 +1,11 @@ +using System; +using System.Reflection; +using System.Xml; + +namespace Xamarin.Forms +{ + interface IResourcesLoader + { + ResourceDictionary CreateResourceDictionary(string resourceID, Assembly assembly, IXmlLineInfo lineInfo); + } +} \ No newline at end of file diff --git a/Xamarin.Forms.Core/Xaml/XamlResourceIdAttribute.cs b/Xamarin.Forms.Core/Xaml/XamlResourceIdAttribute.cs index 99d499befc2..a7f3e1b5f5a 100644 --- a/Xamarin.Forms.Core/Xaml/XamlResourceIdAttribute.cs +++ b/Xamarin.Forms.Core/Xaml/XamlResourceIdAttribute.cs @@ -1,4 +1,5 @@ using System; +using System.Reflection; namespace Xamarin.Forms.Xaml { @@ -15,5 +16,43 @@ public XamlResourceIdAttribute(string resourceId, string path, Type type) Path = path; Type = type; } + + internal static string GetResourceIdForType(Type type) + { + var assembly = type.GetTypeInfo().Assembly; + foreach (var xria in assembly.GetCustomAttributes()) { + if (xria.Type == type) + return xria.ResourceId; + } + return null; + } + + internal static string GetPathForType(Type type) + { + var assembly = type.GetTypeInfo().Assembly; + foreach (var xria in assembly.GetCustomAttributes()) { + if (xria.Type == type) + return xria.Path; + } + return null; + } + + internal static string GetResourceIdForPath(Assembly assembly, string path) + { + foreach (var xria in assembly.GetCustomAttributes()) { + if (xria.Path == path) + return xria.ResourceId; + } + return null; + } + + internal static Type GetTypeForResourceId(Assembly assembly, string resourceId) + { + foreach (var xria in assembly.GetCustomAttributes()) { + if (xria.ResourceId == resourceId) + return xria.Type; + } + return null; + } } } \ No newline at end of file diff --git a/Xamarin.Forms.Xaml.UnitTests/DefaultCtorRouting.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/DefaultCtorRouting.xaml.cs index 250eed72427..174961c3775 100644 --- a/Xamarin.Forms.Xaml.UnitTests/DefaultCtorRouting.xaml.cs +++ b/Xamarin.Forms.Xaml.UnitTests/DefaultCtorRouting.xaml.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using Xamarin.Forms.Core.UnitTests; using Xamarin.Forms.Xaml; +using Xamarin.Forms.Build.Tasks; namespace Xamarin.Forms.Xaml.UnitTests { @@ -53,7 +54,7 @@ public override object ConvertFromInvariantString(string value) return false; } - public IEnumerable ConvertFromString(string value, ModuleDefinition module, BaseNode node) + public IEnumerable ConvertFromString(string value, ILContext context, BaseNode node) { if (value != "IsCompiled?") throw new Exception(); diff --git a/Xamarin.Forms.Xaml.UnitTests/Issues/Bz43733.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/Issues/Bz43733.xaml.cs index 2f15579f2d7..dbf427116d5 100644 --- a/Xamarin.Forms.Xaml.UnitTests/Issues/Bz43733.xaml.cs +++ b/Xamarin.Forms.Xaml.UnitTests/Issues/Bz43733.xaml.cs @@ -47,7 +47,9 @@ public void ThrowOnMissingDictionary(bool useCompiledXaml) { Application.Current = new MockApplication { Resources = new ResourceDictionary { +#pragma warning disable 618 MergedWith = typeof(Bz43733Rd), +#pragma warning restore 618 } }; var p = new Bz43733(useCompiledXaml); diff --git a/Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml b/Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml new file mode 100644 index 00000000000..c07829a2db3 --- /dev/null +++ b/Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml @@ -0,0 +1,14 @@ + + + + + + + + + + \ No newline at end of file diff --git a/Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml.cs new file mode 100644 index 00000000000..64831828bbb --- /dev/null +++ b/Xamarin.Forms.Xaml.UnitTests/ResourceDictionaryWithSource.xaml.cs @@ -0,0 +1,54 @@ +using System; +using NUnit.Framework; +using Xamarin.Forms.Core.UnitTests; + +namespace Xamarin.Forms.Xaml.UnitTests +{ + public partial class ResourceDictionaryWithSource : ContentPage + { + public ResourceDictionaryWithSource() + { + InitializeComponent(); + } + + public ResourceDictionaryWithSource(bool useCompiledXaml) + { + //this stub will be replaced at compile time + } + + [TestFixture] + public class Tests + { + [SetUp] + public void Setup() + { + Device.PlatformServices = new MockPlatformServices(); + } + + [TearDown] + public void TearDown() + { + Device.PlatformServices = null; + } + + [TestCase(false), TestCase(true)] + public void RDWithSourceAreFound(bool useCompiledXaml) + { + var layout = new ResourceDictionaryWithSource(useCompiledXaml); + Assert.That(layout.label.TextColor, Is.EqualTo(Color.Pink)); + } + + [TestCase(false), TestCase(true)] + public void RelativeAndAbsoluteURI(bool useCompiledXaml) + { + var layout = new ResourceDictionaryWithSource(useCompiledXaml); + Assert.That(((ResourceDictionary)layout.Resources["relURI"]).Source, Is.EqualTo(new Uri("./SharedResourceDictionary.xaml", UriKind.Relative))); + Assert.That(((ResourceDictionary)layout.Resources["relURI"])["sharedfoo"], Is.TypeOf