Skip to content
This repository has been archived by the owner on May 1, 2024. It is now read-only.

Commit

Permalink
[X] Design time properties (#4743)
Browse files Browse the repository at this point in the history
An alternate xaml resource file provider can request the XamlLoader to
_not_ ignore normally ignored properties in prebuilt XF design xmlns, as
in the following snippet:

```xaml
<ContentPage
    xmlns="http://xamarin.com/schemas/2014/forms""
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml""
    xmlns:d="http://xamarin.com/schemas/2014/forms/design""
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006""
    mc:Ignorable="d"">
  <Label d:Text="Bar" Text="Foo" x:Name="label" />
</ContentPage>
```

The `d:` should be the prefix used by default for this, but any other
prefix will do too.

The `d:Text` property maps to the exact same property as `Text`, as the
XmllnsDefinitionAttributes are identical (that's convenient from a Intelisense
point of view), but, when (and only when) instructed by a provided Xaml resource
loader, the `d:Text` will override the `Text` property.

This works with virtually all properties defined on built-in Xamarin.Forms
controls, but it doesn't mean it's a sane idea to try to assign design value
to all existing properties.

The APi ofr setting the ResourceLoader had to change, and instead of taking
pre-defined arguments, it accepts and returns query and response types. This
is slightly less convenient to invoke through reflection, but way more easy
to extend in the future.
  • Loading branch information
StephaneDelcroix committed Dec 20, 2018
1 parent 7199cd0 commit a206fa0
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 24 deletions.
18 changes: 10 additions & 8 deletions Xamarin.Forms.Build.Tasks/XamlCTask.cs
Original file line number Diff line number Diff line change
Expand Up @@ -264,22 +264,24 @@ bool TryCoreCompile(MethodDefinition initComp, MethodDefinition initCompRuntime,

//First using the ResourceLoader
var nop = Instruction.Create(Nop);
var getResourceProvider = module.ImportPropertyGetterReference(("Xamarin.Forms.Core", "Xamarin.Forms.Internals", "ResourceLoader"), "ResourceProvider", isStatic: true);
il.Emit(Call, getResourceProvider);
il.Emit(Brfalse, nop);
il.Emit(Call, getResourceProvider);

il.Emit(Newobj, module.ImportCtorReference(("Xamarin.Forms.Core", "Xamarin.Forms.Internals", "ResourceLoader/ResourceLoadingQuery"), 0));
il.Emit(Dup); //dup the RLQ

//AssemblyName
il.Emit(Ldtoken, module.ImportReference(initComp.DeclaringType));
il.Emit(Call, module.ImportMethodReference(("mscorlib", "System", "Type"), methodName: "GetTypeFromHandle", parameterTypes: new[] { ("mscorlib", "System", "RuntimeTypeHandle") }, isStatic: true));
il.Emit(Call, module.ImportMethodReference(("mscorlib", "System.Reflection", "IntrospectionExtensions"), methodName: "GetTypeInfo", parameterTypes: new[] { ("mscorlib", "System", "Type") }, isStatic: true));
il.Emit(Callvirt, module.ImportPropertyGetterReference(("mscorlib", "System.Reflection", "TypeInfo"), propertyName: "Assembly", flatten: true));
il.Emit(Callvirt, module.ImportMethodReference(("mscorlib", "System.Reflection", "Assembly"), methodName: "GetName", parameterTypes: null)); //assemblyName

il.Emit(Callvirt, module.ImportPropertySetterReference(("Xamarin.Forms.Core", "Xamarin.Forms.Internals", "ResourceLoader/ResourceLoadingQuery"), "AssemblyName"));
il.Emit(Dup); //dup the RLQ

il.Emit(Ldstr, resourcePath); //resourcePath
il.Emit(Callvirt, module.ImportMethodReference(("mscorlib", "System", "Func`3"),
methodName: "Invoke",
paramCount: 2,
classArguments: new[] { ("mscorlib", "System.Reflection", "AssemblyName"), ("mscorlib", "System", "String"), ("mscorlib", "System", "String") }));
il.Emit(Callvirt, module.ImportPropertySetterReference(("Xamarin.Forms.Core", "Xamarin.Forms.Internals", "ResourceLoader/ResourceLoadingQuery"), "ResourcePath"));

il.Emit(Call, module.ImportMethodReference(("Xamarin.Forms.Core", "Xamarin.Forms.Internals", "ResourceLoader"), "CanProvideContentFor", 1, isStatic: true));
il.Emit(Brfalse, nop);
il.Emit(Ldarg_0);
il.Emit(Call, initCompRuntime);
Expand Down
37 changes: 36 additions & 1 deletion Xamarin.Forms.Core/Internals/ResourceLoader.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,56 @@
using System;
using System.ComponentModel;
using System.Reflection;

namespace Xamarin.Forms.Internals
{
[EditorBrowsable(EditorBrowsableState.Never)]
public static class ResourceLoader
{
static Func<AssemblyName, string, string> resourceProvider;

[Obsolete("You shouldn't have used this one to begin with, don't use the other one either")]
//takes a resource path, returns string content
public static Func<AssemblyName, string, string> ResourceProvider {
get => resourceProvider;
internal set {
DesignMode.IsDesignModeEnabled = true;
resourceProvider = value;
if (value != null)
ResourceProvider2 = rlq => new ResourceLoadingResponse { ResourceContent = value(rlq.AssemblyName, rlq.ResourcePath) };
else
ResourceProvider2 = null;
}
}

static Func<ResourceLoadingQuery, ResourceLoadingResponse> _resourceProvider2;
public static Func<ResourceLoadingQuery, ResourceLoadingResponse> ResourceProvider2 {
get => _resourceProvider2;
internal set {
DesignMode.IsDesignModeEnabled = value != null;
_resourceProvider2 = value;
}
}

[Obsolete("Can't touch this")]
public static bool CanProvideContentFor(ResourceLoadingQuery rlq)
{
if (_resourceProvider2 == null)
return false;
return _resourceProvider2(rlq) != null;
}

public class ResourceLoadingQuery
{
public AssemblyName AssemblyName { get; set; }
public string ResourcePath { get; set; }
}

public class ResourceLoadingResponse
{
public string ResourceContent { get; set; }
public bool UseDesignProperties { get; set; }
}

internal static Action<Exception> ExceptionHandler { get; set; }
}
}
1 change: 1 addition & 0 deletions Xamarin.Forms.Core/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
[assembly: Preserve]

[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms", "Xamarin.Forms")]
[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms/design", "Xamarin.Forms")]

[assembly: StyleProperty("background-color", typeof(VisualElement), nameof(VisualElement.BackgroundColorProperty))]
[assembly: StyleProperty("background-image", typeof(Page), nameof(Page.BackgroundImageProperty))]
Expand Down
34 changes: 34 additions & 0 deletions Xamarin.Forms.Xaml.UnitTests/DesignPropertiesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using System;
using NUnit.Framework;
using Xamarin.Forms.Core.UnitTests;

namespace Xamarin.Forms.Xaml.UnitTests
{
[TestFixture]
public class DesignPropertiesTests
{
[SetUp] public void Setup() => Device.PlatformServices = new MockPlatformServices();
[TearDown] public void TearDown() => Device.PlatformServices = null;

[Test]
public void DesignProperties()
{
var xaml = @"
<ContentPage
xmlns=""http://xamarin.com/schemas/2014/forms""
xmlns:x=""http://schemas.microsoft.com/winfx/2006/xaml""
xmlns:d=""http://xamarin.com/schemas/2014/forms/design""
xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006""
mc:Ignorable=""d"">
<Label d:Text=""Bar"" Text=""Foo"" x:Name=""label"" />
</ContentPage>";

var view = new ContentPage();
XamlLoader.Load(view, xaml, useDesignProperties: true); //this is equiv as LoadFromXaml, but with the bool set

var label = ((Forms.Internals.INameScope)view).FindByName("label") as Label;

Assert.That(label.Text, Is.EqualTo("Bar"));
}
}
}
62 changes: 62 additions & 0 deletions Xamarin.Forms.Xaml.UnitTests/ResourceLoader.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ public void Setup()
public void TearDown()
{
Device.PlatformServices = null;
#pragma warning disable CS0618 // Type or member is obsolete
Xamarin.Forms.Internals.ResourceLoader.ResourceProvider = null;
#pragma warning restore CS0618 // Type or member is obsolete
}

[TestCase(false), TestCase(true)]
Expand All @@ -38,7 +40,9 @@ public void XamlLoadingUsesResourceLoader(bool useCompiledXaml)
var layout = new ResourceLoader(useCompiledXaml);
Assert.That(layout.label.TextColor, Is.EqualTo(Color.FromHex("#368F95")));

#pragma warning disable CS0618 // Type or member is obsolete
Xamarin.Forms.Internals.ResourceLoader.ResourceProvider = (asmName, path) => {
#pragma warning restore CS0618 // Type or member is obsolete
if (path == "ResourceLoader.xaml")
return @"
<ContentPage
Expand All @@ -52,7 +56,63 @@ public void XamlLoadingUsesResourceLoader(bool useCompiledXaml)
};
layout = new ResourceLoader(useCompiledXaml);
Assert.That(layout.label.TextColor, Is.EqualTo(Color.Pink));
}

[Test]
public void XamlLoadingUsesResourceProvider2([Values (false, true)]bool useCompiledXaml)
{
var layout = new ResourceLoader(useCompiledXaml);
Assert.That(layout.label.TextColor, Is.EqualTo(Color.FromHex("#368F95")));

Xamarin.Forms.Internals.ResourceLoader.ResourceProvider2 = (rlq) => {
if (rlq.ResourcePath == "ResourceLoader.xaml")
return new Forms.Internals.ResourceLoader.ResourceLoadingResponse {
ResourceContent = @"
<ContentPage
xmlns=""http://xamarin.com/schemas/2014/forms""
xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml""
x:Class=""Xamarin.Forms.Xaml.UnitTests.ResourceLoader""
xmlns:d=""http://xamarin.com/schemas/2014/forms/design""
xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006""
mc:Ignorable=""d"" >
<Label x:Name = ""label"" TextColor = ""Pink"" d:TextColor = ""HotPink"" />
</ContentPage >"};
return null;
};


layout = new ResourceLoader(useCompiledXaml);
Assert.That(layout.label.TextColor, Is.EqualTo(Color.Pink));
}

[Test]
public void XamlLoadingUsesResourceProvider2WithDesignProperties([Values(false, true)]bool useCompiledXaml)
{
var layout = new ResourceLoader(useCompiledXaml);
Assert.That(layout.label.TextColor, Is.EqualTo(Color.FromHex("#368F95")));

Xamarin.Forms.Internals.ResourceLoader.ResourceProvider2 = (rlq) => {
if (rlq.ResourcePath == "ResourceLoader.xaml")
return new Forms.Internals.ResourceLoader.ResourceLoadingResponse {
UseDesignProperties = true,
ResourceContent = @"
<ContentPage
xmlns=""http://xamarin.com/schemas/2014/forms""
xmlns:x=""http://schemas.microsoft.com/winfx/2009/xaml""
x:Class=""Xamarin.Forms.Xaml.UnitTests.ResourceLoader""
xmlns:d=""http://xamarin.com/schemas/2014/forms/design""
xmlns:mc=""http://schemas.openxmlformats.org/markup-compatibility/2006""
mc:Ignorable=""d"" >
<Label x:Name = ""label"" TextColor = ""Pink"" d:TextColor = ""HotPink"" />
</ContentPage >"};
return null;
};


layout = new ResourceLoader(useCompiledXaml);
Assert.That(layout.label.TextColor, Is.EqualTo(Color.HotPink));
}

[TestCase(false), TestCase(true)]
Expand All @@ -61,7 +121,9 @@ public void RDLoadingUsesResourceLoader(bool useCompiledXaml)
var layout = new ResourceLoader(useCompiledXaml);
Assert.That(layout.label.TextColor, Is.EqualTo(Color.FromHex("#368F95")));

#pragma warning disable CS0618 // Type or member is obsolete
Xamarin.Forms.Internals.ResourceLoader.ResourceProvider = (asmName, path) => {
#pragma warning restore CS0618 // Type or member is obsolete
if (path == "AppResources/Colors.xaml")
return @"
<ResourceDictionary
Expand Down
1 change: 1 addition & 0 deletions Xamarin.Forms.Xaml/Properties/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
[assembly: Preserve]

[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms", "Xamarin.Forms.Xaml")]
[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms/design", "Xamarin.Forms.Xaml")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml", "Xamarin.Forms.Xaml")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml", "System", AssemblyName = "mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
[assembly: XmlnsDefinition("http://schemas.microsoft.com/winfx/2006/xaml", "System", AssemblyName = "System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089")]
Expand Down
19 changes: 15 additions & 4 deletions Xamarin.Forms.Xaml/PruneIgnoredNodesVisitor.cs
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;

namespace Xamarin.Forms.Xaml
{
class PruneIgnoredNodesVisitor : IXamlNodeVisitor
{
public PruneIgnoredNodesVisitor(bool useDesignProperties = false) => UseDesignProperties = useDesignProperties;

public TreeVisitingMode VisitingMode => TreeVisitingMode.TopDown;
public bool StopOnDataTemplate => false;
public bool StopOnResourceDictionary => false;
public bool VisitNodeOnDataTemplate => true;
public bool UseDesignProperties { get; }
public bool SkipChildren(INode node, INode parentNode) => false;
public bool IsResourceDictionary(ElementNode node) => false;

Expand All @@ -17,12 +21,19 @@ public void Visit(ElementNode node, INode parentNode)
foreach (var propertyKvp in node.Properties)
{
var propertyName = propertyKvp.Key;
var propertyValue = (propertyKvp.Value as ValueNode)?.Value as string;
if (propertyValue == null)
if (!((propertyKvp.Value as ValueNode)?.Value is string propertyValue))
continue;
if (!propertyName.Equals(XamlParser.McUri, "Ignorable"))
continue;
(parentNode.IgnorablePrefixes ?? (parentNode.IgnorablePrefixes = new List<string>())).AddRange(propertyValue.Split(','));
var prefixes = propertyValue.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries).ToList();
if (UseDesignProperties) //if we're in design mode for this file
{
for (var i = 0; i < prefixes.Count; i++)
if (node.NamespaceResolver.LookupNamespace(prefixes[i]) == XamlParser.XFDesignUri)
prefixes.RemoveAt(i--);
}

(parentNode.IgnorablePrefixes ?? (parentNode.IgnorablePrefixes = new List<string>())).AddRange(prefixes);
}

foreach (var propertyKvp in node.Properties.ToList())
Expand Down
48 changes: 48 additions & 0 deletions Xamarin.Forms.Xaml/RemoveDuplicateDesignNodes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using System.Linq;

namespace Xamarin.Forms.Xaml
{
class RemoveDuplicateDesignNodes : IXamlNodeVisitor
{
public TreeVisitingMode VisitingMode => TreeVisitingMode.TopDown;
public bool StopOnDataTemplate => false;
public bool StopOnResourceDictionary => false;
public bool VisitNodeOnDataTemplate => true;
public bool SkipChildren(INode node, INode parentNode) => false;
public bool IsResourceDictionary(ElementNode node) => false;

public void Visit(ValueNode node, INode parentNode)
{
}

public void Visit(MarkupNode node, INode parentNode)
{
}

public void Visit(ElementNode node, INode parentNode)
{
if (node.Properties == null || node.Properties.Count == 0)
return;
var props = node.Properties.ToList();
for (var i = 0; i < props.Count; i++) {
var key = props[i].Key;
if (key.NamespaceURI != XamlParser.XFDesignUri)
continue;
var k = new XmlName(XamlParser.XFUri, key.LocalName);
if (node.Properties.Remove(k))
continue;
if (node.NamespaceResolver.LookupPrefix(XamlParser.XFUri) == "")
node.Properties.Remove(new XmlName("", k.LocalName));
}
}

public void Visit(RootNode node, INode parentNode)
{
Visit((ElementNode)node, parentNode);
}

public void Visit(ListNode node, INode parentNode)
{
}
}
}
Loading

0 comments on commit a206fa0

Please sign in to comment.