Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Xaml] Simplify loading ResourceDictionary from Source #21429

Merged
merged 7 commits into from
Mar 27, 2024
60 changes: 29 additions & 31 deletions src/Controls/src/Core/ResourceDictionary.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ public Uri Source
{
if (_source == value)
return;
throw new InvalidOperationException("Source can only be set from XAML."); //through the RDSourceTypeConverter
throw new InvalidOperationException("Source can only be set from XAML."); // through SetSource
}
}

Expand All @@ -50,24 +50,19 @@ public void SetAndLoadSource(Uri value, string resourcePath, Assembly assembly,
if (type != null)
{
_mergedInstance = s_instances.GetValue(type, _ => (ResourceDictionary)Activator.CreateInstance(type));
OnValuesChanged(_mergedInstance.ToArray());
}
else
{
if (RuntimeFeature.IsXamlRuntimeParsingSupported)
{
_mergedInstance = DependencyService.Get<IResourcesLoader>().CreateFromResource<ResourceDictionary>(resourcePath, assembly, lineInfo);
}
else
{
// This codepath is only ever hit when XamlC is explicitly disabled for a given resource dictionary.
// The developer had to add [XamlCompilation(XamlCompilationOptions.Skip)] or <?xaml-comp compile="false" ?> to their code.
// XamlC will produce a warning in this case (MAUIG0070 or XC0010).
throw new InvalidOperationException(
$"The resource '{resourcePath}' has not been compiled using XamlC and parsing XAML resources at runtime is disabled. "
+ "Ensure the resource is compiled using XamlC. Alternatively, enable parsing XAML resources at runtime by setting "
+ "the MauiXamlRuntimeParsingSupport MSBuild property to true.");
}
}
}

internal static ResourceDictionary GetOrCreateInstance([DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicParameterlessConstructor)] Type type)
{
return s_instances.GetValue(type, _ => (ResourceDictionary)Activator.CreateInstance(type));
}

internal void SetSource(Uri source, ResourceDictionary sourceInstance)
{
_source = source;
_mergedInstance = sourceInstance;
OnValuesChanged(_mergedInstance.ToArray());
}

Expand Down Expand Up @@ -396,26 +391,29 @@ object IExtendedTypeConverter.ConvertFromInvariantString(string value, IServiceP
if (rootObjectType == null)
return null;

var lineInfo = (serviceProvider.GetService(typeof(Xaml.IXmlLineInfoProvider)) as Xaml.IXmlLineInfoProvider)?.XmlLineInfo;
var rootTargetPath = XamlResourceIdAttribute.GetPathForType(rootObjectType);
var assembly = rootObjectType.Assembly;
return GetUriWithExplicitAssembly(value, rootObjectType.Assembly);
}

internal static Uri GetUriWithExplicitAssembly(string value, Assembly defaultAssembly)
{
(value, var assembly) = SplitUriAndAssembly(value, defaultAssembly);
return CombineUriAndAssembly(value, assembly);
}

internal static ValueTuple<string, Assembly> SplitUriAndAssembly(string value, Assembly defaultAssembly)
{
if (value.IndexOf(";assembly=", StringComparison.Ordinal) != -1)
{
var parts = value.Split(new[] { ";assembly=" }, StringSplitOptions.RemoveEmptyEntries);
value = parts[0];
var asmName = parts[1];
assembly = Assembly.Load(asmName);
return (parts[0], Assembly.Load(parts[1]));
}

var uri = new Uri(value, UriKind.Relative); //we don't want file:// uris, even if they start with '/'
var resourcePath = GetResourcePath(uri, rootTargetPath);

//Re-add the assembly= in all cases, so HotReload doesn't have to make assumptions
uri = new Uri($"{value};assembly={assembly.GetName().Name}", UriKind.Relative);
targetRD.SetAndLoadSource(uri, resourcePath, assembly, lineInfo);
return (value, defaultAssembly);
}

return uri;
internal static Uri CombineUriAndAssembly(string value, Assembly assembly)
{
return new Uri($"{value};assembly={assembly.GetName().Name}", UriKind.Relative);
}

internal static string GetResourcePath(Uri uri, string rootTargetPath)
Expand Down
30 changes: 29 additions & 1 deletion src/Controls/src/Core/StyleSheets/StyleSheet.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
using System.Runtime.CompilerServices;
using System.Xml;

using Microsoft.Maui.Controls.Xaml;

namespace Microsoft.Maui.Controls.StyleSheets
{
/// <include file="../../../docs/Microsoft.Maui.Controls.StyleSheets/StyleSheet.xml" path="Type[@FullName='Microsoft.Maui.Controls.StyleSheets.StyleSheet']/Docs/*" />
Expand All @@ -24,7 +26,7 @@ public sealed class StyleSheet : IStyle
public static StyleSheet FromResource(string resourcePath, Assembly assembly, IXmlLineInfo lineInfo = null)
{
var styleSheet = new StyleSheet();
var resString = DependencyService.Get<IResourcesLoader>().GetResource(resourcePath, assembly, styleSheet, lineInfo);
var resString = GetResource(resourcePath, assembly, styleSheet, lineInfo);
using (var textReader = new StringReader(resString))
using (var cssReader = new CssReader(textReader))
Parse(styleSheet, cssReader);
Expand Down Expand Up @@ -114,5 +116,31 @@ void Apply(Element styleable)
}

void IStyle.UnApply(BindableObject bindable) => throw new NotImplementedException();

private static string GetResource(string resourcePath, Assembly assembly, object target, IXmlLineInfo lineInfo)
{
var resourceLoadingResponse = Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
{
AssemblyName = assembly.GetName(),
ResourcePath = resourcePath,
Instance = target
});

var alternateResource = resourceLoadingResponse?.ResourceContent;
if (alternateResource != null)
return alternateResource;

var resourceId = XamlResourceIdAttribute.GetResourceIdForPath(assembly, resourcePath);
if (resourceId == null)
throw new XamlParseException($"Resource '{resourcePath}' not found.", lineInfo);

using (var stream = assembly.GetManifestResourceStream(resourceId))
{
if (stream == null)
throw new XamlParseException($"No resource found for '{resourceId}'.", lineInfo);
using (var reader = new StreamReader(stream))
return reader.ReadToEnd();
}
}
}
}
14 changes: 0 additions & 14 deletions src/Controls/src/Core/Xaml/IResourcesLoader.cs

This file was deleted.

7 changes: 7 additions & 0 deletions src/Controls/src/Xaml/ApplyPropertiesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,13 @@ public static void SetPropertyValue(object xamlelement, XmlName propertyName, ob
var serviceProvider = new XamlServiceProvider(node, context);
var xKey = node is IElementNode eNode && eNode.Properties.ContainsKey(XmlName.xKey) ? ((ValueNode)eNode.Properties[XmlName.xKey]).Value as string : null;

// Special handling for ResourceDictionary.Source
if (xamlelement is ResourceDictionary rd && propertyName.LocalName == "Source" && propertyName.NamespaceURI == "")
{
ResourceDictionaryHelpers.LoadFromSource(rd, (string)value, rootElement.GetType(), lineInfo);
return;
}

if (TrySetPropertyValue(xamlelement, propertyName, xKey, value, rootElement, lineInfo, serviceProvider, out var xpe))
return;

Expand Down
6 changes: 6 additions & 0 deletions src/Controls/src/Xaml/CreateValuesVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,17 +56,23 @@ public void Visit(ElementNode node, INode parentNode)
}
Context.Types[node] = type;
if (IsXaml2009LanguagePrimitive(node))
{
value = CreateLanguagePrimitive(type, node);
}
else if (node.Properties.ContainsKey(XmlName.xArguments) || node.Properties.ContainsKey(XmlName.xFactoryMethod))
{
value = CreateFromFactory(type, node);
}
else if (
type.GetTypeInfo()
.DeclaredConstructors.Any(
ci =>
ci.IsPublic && ci.GetParameters().Length != 0 &&
ci.GetParameters().All(pi => pi.CustomAttributes.Any(attr => attr.AttributeType == typeof(ParameterAttribute)))) &&
ValidateCtorArguments(type, node, out string ctorargname))
{
value = CreateFromParameterizedConstructor(type, node);
}
else if (!type.GetTypeInfo().DeclaredConstructors.Any(ci => ci.IsPublic && ci.GetParameters().Length == 0) &&
!ValidateCtorArguments(type, node, out ctorargname))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,6 @@ static MauiAppBuilder SetupDefaults(this MauiAppBuilder builder)
#if WINDOWS || ANDROID || IOS || MACCATALYST || TIZEN
// initialize compatibility DependencyService
DependencyService.SetToInitialized();
DependencyService.Register<Xaml.ResourcesLoader>();
DependencyService.Register<Xaml.ValueConverterProvider>();
DependencyService.Register<PlatformSizeService>();

Expand Down
60 changes: 60 additions & 0 deletions src/Controls/src/Xaml/ResourceDictionaryHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using System;
using System.IO;
using System.Reflection;
using System.Xml;

namespace Microsoft.Maui.Controls.Xaml
{
internal static class ResourceDictionaryHelpers
{
internal static void LoadFromSource(ResourceDictionary rd, string value, Type rootType, IXmlLineInfo lineInfo)
{
(value, var assembly) = ResourceDictionary.RDSourceTypeConverter.SplitUriAndAssembly(value, rootType.Assembly);

var rootTargetPath = XamlResourceIdAttribute.GetPathForType(rootType);
var resourcePath = ResourceDictionary.RDSourceTypeConverter.GetResourcePath(new Uri(value, UriKind.Relative), rootTargetPath);
var sourceUri = ResourceDictionary.RDSourceTypeConverter.CombineUriAndAssembly(value, assembly);

var type = XamlResourceIdAttribute.GetTypeForPath(assembly, resourcePath);
var sourceInstance = type is not null
? ResourceDictionary.GetOrCreateInstance(type)
: CreateFromResource(resourcePath, assembly, lineInfo);

rd.SetSource(sourceUri, sourceInstance);

static ResourceDictionary CreateFromResource(string resourcePath, Assembly assembly, IXmlLineInfo lineInfo)
{
var rd = new ResourceDictionary();

var resourceLoadingResponse = Maui.Controls.Internals.ResourceLoader.ResourceProvider2?.Invoke(new Maui.Controls.Internals.ResourceLoader.ResourceLoadingQuery
{
AssemblyName = assembly.GetName(),
ResourcePath = resourcePath,
Instance = rd,
});

var alternateResource = resourceLoadingResponse?.ResourceContent;
if (alternateResource != null)
{
XamlLoader.Load(rd, alternateResource, resourceLoadingResponse.UseDesignProperties);
return rd;
}

var resourceId = XamlResourceIdAttribute.GetResourceIdForPath(assembly, resourcePath);
if (resourceId == null)
throw new XamlParseException($"Resource '{resourcePath}' not found.", lineInfo);

using (var stream = assembly.GetManifestResourceStream(resourceId))
{
if (stream == null)
throw new XamlParseException($"No resource found for '{resourceId}'.", lineInfo);
using (var reader = new StreamReader(stream))
{
rd.LoadFromXaml(reader.ReadToEnd(), assembly);
return rd;
}
}
}
}
}
}
71 changes: 0 additions & 71 deletions src/Controls/src/Xaml/ResourcesLoader.cs

This file was deleted.

Loading