diff --git a/.nuspec/Xamarin.Forms.Debug.targets b/.nuspec/Xamarin.Forms.Debug.targets
index a239c55a974..941a5d9f62d 100644
--- a/.nuspec/Xamarin.Forms.Debug.targets
+++ b/.nuspec/Xamarin.Forms.Debug.targets
@@ -24,4 +24,4 @@
DebugSymbols = "$(DebugSymbols)"
DebugType = "$(DebugType)"/>
-
\ No newline at end of file
+
diff --git a/.nuspec/Xamarin.Forms.targets b/.nuspec/Xamarin.Forms.targets
index 96c2dfbcfb7..e210ee6309d 100644
--- a/.nuspec/Xamarin.Forms.targets
+++ b/.nuspec/Xamarin.Forms.targets
@@ -109,4 +109,19 @@
DebugType = "$(DebugType)"
KeepXamlResources = "$(XFKeepXamlResources)" />
+
+
+
+ RenameCss;
+ $(PrepareResourcesDependsOn)
+
+
+
+
+
+
+ %(EmbeddedResource.RelativeDir)%(EmbeddedResource.Filename)%(EmbeddedResource.Extension)
+
+
+
diff --git a/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs b/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs
index ca0f9fd6413..d04c0a52061 100644
--- a/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs
+++ b/Xamarin.Forms.Build.Tasks/NodeILExtensions.cs
@@ -457,6 +457,19 @@ public static IEnumerable PushServiceProvider(this INode node, ILCo
yield return Instruction.Create(OpCodes.Newobj, ctor);
+ //Add a CurrentAssemblyProvider
+ {
+ yield return Instruction.Create(OpCodes.Dup); //Dupicate the serviceProvider
+ yield return Instruction.Create(OpCodes.Ldtoken, module.ImportReference(typeof(ICurrentAssemblyProvider)));
+ yield return Instruction.Create(OpCodes.Call, module.ImportReference(getTypeFromHandle));
+ var currentAssemblyproviderCtor = module.ImportReference(typeof(CurrentAssemblyProvider).GetConstructor(new []{ typeof(System.Reflection.Assembly) }));
+ //call class [mscorlib]System.Reflection.Assembly class [mscorlib]System.Reflection.Assembly::GetExecutingAssembly()
+ var getExecutingAssembly = module.ImportReference(typeof(System.Reflection.Assembly).GetMethod("GetExecutingAssembly"));
+ yield return Instruction.Create(OpCodes.Call, getExecutingAssembly);
+ yield return Instruction.Create(OpCodes.Newobj, currentAssemblyproviderCtor);
+ yield return Instruction.Create(OpCodes.Callvirt, addService);
+ }
+
//Add a SimpleValueTargetProvider
var pushParentIl = node.PushParentObjectsArray(context).ToList();
if (pushParentIl[pushParentIl.Count - 1].OpCode != OpCodes.Ldnull)
diff --git a/Xamarin.Forms.Core.UnitTests/LabelTests.cs b/Xamarin.Forms.Core.UnitTests/LabelTests.cs
index 5e2ffb53f71..6fcaab9579d 100644
--- a/Xamarin.Forms.Core.UnitTests/LabelTests.cs
+++ b/Xamarin.Forms.Core.UnitTests/LabelTests.cs
@@ -189,8 +189,7 @@ public void FontSizeCanBeSetFromStyle ()
public void ManuallySetFontSizeNotOverridenByStyle ()
{
var label = new Label ();
-
- Assert.AreEqual (10.0, label.FontSize);
+ Assume.That (label.FontSize, Is.EqualTo(10.0));
label.SetValue (Label.FontSizeProperty, 2.0, false);
Assert.AreEqual (2.0, label.FontSize);
@@ -199,6 +198,19 @@ public void ManuallySetFontSizeNotOverridenByStyle ()
Assert.AreEqual (2.0, label.FontSize);
}
+ [Test]
+ public void ManuallySetFontSizeNotOverridenByFontSetInStyle()
+ {
+ var label = new Label();
+ Assume.That(label.FontSize, Is.EqualTo(10.0));
+
+ label.SetValue(Label.FontSizeProperty, 2.0);
+ Assert.AreEqual(2.0, label.FontSize);
+
+ label.SetValue(Label.FontProperty, Font.SystemFontOfSize(1.0), fromStyle: true);
+ Assert.AreEqual(2.0, label.FontSize);
+ }
+
[Test]
public void ChangingHorizontalTextAlignmentFiresXAlignChanged ()
{
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/IStylableTest.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/IStylableTest.cs
new file mode 100644
index 00000000000..f43be2e7977
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/IStylableTest.cs
@@ -0,0 +1,51 @@
+using System;
+
+using NUnit.Framework;
+
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ [TestFixture]
+ public class IStylableTest
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ Device.PlatformServices = new MockPlatformServices();
+ Internals.Registrar.RegisterAll(new Type[0]);
+ }
+
+ [TestCase]
+ public void GetBackgroundColor()
+ {
+ var label = new Label();
+ var bp = ((IStylable)label).GetProperty("background-color");
+ Assert.AreSame(VisualElement.BackgroundColorProperty, bp);
+ }
+
+ [TestCase]
+ public void GetLabelColor()
+ {
+ var label = new Label();
+ var bp = ((IStylable)label).GetProperty("color");
+ Assert.AreSame(Label.TextColorProperty, bp);
+ }
+
+ [TestCase]
+ public void GetEntryColor()
+ {
+ var entry = new Entry();
+ var bp = ((IStylable)entry).GetProperty("color");
+ Assert.AreSame(Entry.TextColorProperty, bp);
+ }
+
+ [TestCase]
+ public void GetGridColor()
+ {
+ var grid = new Grid();
+ var bp = ((IStylable)grid).GetProperty("color");
+ Assert.Null(bp);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/MockStylable.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/MockStylable.cs
new file mode 100644
index 00000000000..fa03ab44e39
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/MockStylable.cs
@@ -0,0 +1,14 @@
+using System.Collections.Generic;
+using NUnit.Framework;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ class MockStylable : IStyleSelectable
+ {
+ public IEnumerable Children { get; set; }
+ public IList Classes { get; set; }
+ public string Id { get; set; }
+ public string Name { get; set; }
+ public IStyleSelectable Parent { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/SelectorTests.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/SelectorTests.cs
new file mode 100644
index 00000000000..49dd838d826
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/SelectorTests.cs
@@ -0,0 +1,110 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using NUnit.Framework;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ [TestFixture]
+ public class SelectorTests
+ {
+ IStyleSelectable Page;
+ IStyleSelectable StackLayout => Page.Children.First();
+ IStyleSelectable Label0 => StackLayout.Children.Skip(0).First();
+ IStyleSelectable Label1 => StackLayout.Children.Skip(1).First();
+ IStyleSelectable Label2 => ContentView0.Children.First();
+ IStyleSelectable Label3 => StackLayout.Children.Skip(3).First();
+ IStyleSelectable Label4 => StackLayout.Children.Skip(4).First();
+ IStyleSelectable ContentView0 => StackLayout.Children.Skip(2).First();
+
+ [SetUp]
+ public void SetUp()
+ {
+ Page = new MockStylable {
+ Name = "Page",
+ Children = new List {
+ new MockStylable {
+ Name = "StackLayout",
+ Children = new List {
+ new MockStylable {Name = "Label", Classes = new[]{"test"}}, //Label0
+ new MockStylable {Name = "Label"}, //Label1
+ new MockStylable { //ContentView0
+ Name = "ContentView",
+ Classes = new[]{"test"},
+ Children = new List {
+ new MockStylable {Name = "Label", Classes = new[]{"test"}}, //Label2
+ }
+ },
+ new MockStylable {Name = "Label", Id="foo"}, //Label3
+ new MockStylable {Name = "Label"}, //Label4
+ }
+ }
+ }
+ };
+ SetParents(Page);
+ }
+
+ void SetParents(IStyleSelectable stylable, IStyleSelectable parent = null)
+ {
+ ((MockStylable)stylable).Parent = parent;
+ if (stylable.Children == null)
+ return;
+ foreach (var s in stylable.Children)
+ SetParents(s, stylable);
+ }
+
+ [TestCase("label", true, true, true, true, true, false)]
+ [TestCase(" label", true, true, true, true, true, false)]
+ [TestCase("label ", true, true, true, true, true, false)]
+ [TestCase(".test", true, false, true, false, false, true)]
+ [TestCase("label.test", true, false, true, false, false, false)]
+ [TestCase("stacklayout>label.test", true, false, false, false, false, false)]
+ [TestCase("stacklayout >label.test", true, false, false, false, false, false)]
+ [TestCase("stacklayout> label.test", true, false, false, false, false, false)]
+ [TestCase("stacklayout label.test", true, false, true, false, false, false)]
+ [TestCase("stacklayout label.test", true, false, true, false, false, false)]
+ [TestCase("stacklayout .test", true, false, true, false, false, true)]
+ [TestCase("*", true, true, true, true, true, true)]
+ [TestCase("#foo", false, false, false, true, false, false)]
+ [TestCase("label#foo", false, false, false, true, false, false)]
+ [TestCase("div#foo", false, false, false, false, false, false)]
+ [TestCase(".test,#foo", true, false, true, true, false, true)]
+ [TestCase(".test ,#foo", true, false, true, true, false, true)]
+ [TestCase(".test, #foo", true, false, true, true, false, true)]
+ [TestCase("#foo,.test", true, false, true, true, false, true)]
+ [TestCase("#foo ,.test", true, false, true, true, false, true)]
+ [TestCase("#foo, .test", true, false, true, true, false, true)]
+ [TestCase("contentview+label", false, false, false, true, false, false)]
+ [TestCase("contentview +label", false, false, false, true, false, false)]
+ [TestCase("contentview+ label", false, false, false, true, false, false)]
+ [TestCase("contentview~label", false, false, false, true, true, false)]
+ [TestCase("contentview ~label", false, false, false, true, true, false)]
+ [TestCase("contentview\r\n~label", false, false, false, true, true, false)]
+ [TestCase("contentview~ label", false, false, false, true, true, false)]
+ [TestCase("label~*", false, true, false, true, true, true)]
+ [TestCase("label~.test", false, false, false, false, false, true)]
+ [TestCase("label~#foo", false, false, false, true, false, false)]
+ [TestCase("page contentview stacklayout label", false, false, false, false, false, false)]
+ [TestCase("page stacklayout contentview label", false, false, true, false, false, false)]
+ [TestCase("page contentview label", false, false, true, false, false, false)]
+ [TestCase("page contentview>label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout contentview label", false, false, true, false, false, false)]
+ [TestCase("page stacklayout>contentview label", false, false, true, false, false, false)]
+ [TestCase("page stacklayout contentview>label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout>contentview label", false, false, true, false, false, false)]
+ [TestCase("page>stack/* comment * */layout>contentview label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout contentview>label", false, false, true, false, false, false)]
+ [TestCase("page stacklayout>contentview>label", false, false, true, false, false, false)]
+ [TestCase("page>stacklayout>contentview>label", false, false, true, false, false, false)]
+ public void TestCase(string selectorString, bool label0match, bool label1match, bool label2match, bool label3match, bool label4match, bool content0match)
+ {
+ var selector = Selector.Parse(new CssReader(new StringReader(selectorString)));
+ Assert.AreEqual(label0match, selector.Matches(Label0));
+ Assert.AreEqual(label1match, selector.Matches(Label1));
+ Assert.AreEqual(label2match, selector.Matches(Label2));
+ Assert.AreEqual(label3match, selector.Matches(Label3));
+ Assert.AreEqual(label4match, selector.Matches(Label4));
+ Assert.AreEqual(content0match, selector.Matches(ContentView0));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/StyleSheets/StyleTests.cs b/Xamarin.Forms.Core.UnitTests/StyleSheets/StyleTests.cs
new file mode 100644
index 00000000000..6680dddeff7
--- /dev/null
+++ b/Xamarin.Forms.Core.UnitTests/StyleSheets/StyleTests.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+
+using NUnit.Framework;
+
+using Xamarin.Forms.Core.UnitTests;
+
+namespace Xamarin.Forms.StyleSheets.UnitTests
+{
+ [TestFixture]
+ public class StyleTests
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ Device.PlatformServices = new MockPlatformServices();
+ Internals.Registrar.RegisterAll(new Type[0]);
+ }
+
+ [Test]
+ public void PropertiesAreApplied()
+ {
+ var styleString = @"background-color: #ff0000;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var ve = new VisualElement();
+ Assume.That(ve.BackgroundColor, Is.EqualTo(Color.Default));
+ style.Apply(ve);
+ Assert.That(ve.BackgroundColor, Is.EqualTo(Color.Red));
+ }
+
+ [Test]
+ public void PropertiesSetByStyleDoesNotOverrideManualOne()
+ {
+ var styleString = @"background-color: #ff0000;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var ve = new VisualElement() { BackgroundColor = Color.Pink };
+ Assume.That(ve.BackgroundColor, Is.EqualTo(Color.Pink));
+
+ style.Apply(ve);
+ Assert.That(ve.BackgroundColor, Is.EqualTo(Color.Pink));
+ }
+
+ [Test]
+ public void StylesAreCascading()
+ {
+ var styleString = @"background-color: #ff0000; color: #00ff00;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var label = new Label();
+ var layout = new StackLayout {
+ Children = {
+ label,
+ }
+ };
+
+ Assume.That(layout.BackgroundColor, Is.EqualTo(Color.Default));
+ Assume.That(label.BackgroundColor, Is.EqualTo(Color.Default));
+ Assume.That(label.TextColor, Is.EqualTo(Color.Default));
+
+ style.Apply(layout);
+ Assert.That(layout.BackgroundColor, Is.EqualTo(Color.Red));
+ Assert.That(label.BackgroundColor, Is.EqualTo(Color.Red));
+ Assert.That(label.TextColor, Is.EqualTo(Color.Lime));
+ }
+
+ [Test]
+ public void PropertiesAreOnlySetOnMatchingElements()
+ {
+ var styleString = @"background-color: #ff0000; color: #00ff00;";
+ var style = Style.Parse(new CssReader(new StringReader(styleString)), '}');
+ Assume.That(style, Is.Not.Null);
+
+ var layout = new StackLayout();
+ Assert.That(layout.GetValue(TextElement.TextColorProperty), Is.EqualTo(Color.Default));
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
index c62cb9730c6..e1d6a0d7503 100644
--- a/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
+++ b/Xamarin.Forms.Core.UnitTests/Xamarin.Forms.Core.UnitTests.csproj
@@ -187,6 +187,10 @@
+
+
+
+
@@ -219,4 +223,7 @@
Images/crimson.jpg
-
\ No newline at end of file
+
+
+
+
diff --git a/Xamarin.Forms.Core/Element.cs b/Xamarin.Forms.Core/Element.cs
index 1f8725f2631..b185eb07516 100644
--- a/Xamarin.Forms.Core/Element.cs
+++ b/Xamarin.Forms.Core/Element.cs
@@ -9,7 +9,7 @@
namespace Xamarin.Forms
{
- public abstract class Element : BindableObject, IElement, INameScope, IElementController
+ public abstract partial class Element : BindableObject, IElement, INameScope, IElementController
{
public static readonly BindableProperty MenuProperty = BindableProperty.CreateAttached(nameof(Menu), typeof(Menu), typeof(Element), null);
diff --git a/Xamarin.Forms.Core/Entry.cs b/Xamarin.Forms.Core/Entry.cs
index c8914566c6b..d89532a6b68 100644
--- a/Xamarin.Forms.Core/Entry.cs
+++ b/Xamarin.Forms.Core/Entry.cs
@@ -129,4 +129,4 @@ void ITextElement.OnTextColorPropertyChanged(Color oldValue, Color newValue)
{
}
}
-}
\ No newline at end of file
+}
diff --git a/Xamarin.Forms.Core/IRootObjectProvider.cs b/Xamarin.Forms.Core/IRootObjectProvider.cs
deleted file mode 100644
index 883033a58de..00000000000
--- a/Xamarin.Forms.Core/IRootObjectProvider.cs
+++ /dev/null
@@ -1,7 +0,0 @@
-namespace Xamarin.Forms.Xaml
-{
- public interface IRootObjectProvider
- {
- object RootObject { get; }
- }
-}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/Label.cs b/Xamarin.Forms.Core/Label.cs
index f17093a159f..f9030a295a4 100644
--- a/Xamarin.Forms.Core/Label.cs
+++ b/Xamarin.Forms.Core/Label.cs
@@ -191,4 +191,4 @@ void ITextElement.OnTextColorPropertyChanged(Color oldValue, Color newValue)
{
}
}
-}
\ No newline at end of file
+}
diff --git a/Xamarin.Forms.Core/Properties/AssemblyInfo.cs b/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
index 429aca999d2..2f6ce234f6f 100644
--- a/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
+++ b/Xamarin.Forms.Core/Properties/AssemblyInfo.cs
@@ -2,6 +2,7 @@
using System.Runtime.CompilerServices;
using Xamarin.Forms;
using Xamarin.Forms.Internals;
+using Xamarin.Forms.StyleSheets;
[assembly: AssemblyTitle("Xamarin.Forms.Core")]
[assembly: AssemblyDescription("")]
@@ -59,4 +60,7 @@
[assembly: InternalsVisibleTo("Xamarin.Forms.CarouselView")]
[assembly: Preserve]
-[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms", "Xamarin.Forms")]
\ No newline at end of file
+[assembly: XmlnsDefinition("http://xamarin.com/schemas/2014/forms", "Xamarin.Forms")]
+
+[assembly: StyleProperty("color", typeof(ITextElement), nameof(TextElement.TextColorProperty))]
+[assembly: StyleProperty("background-color", typeof(VisualElement), nameof(VisualElement.BackgroundColorProperty))]
diff --git a/Xamarin.Forms.Core/Registrar.cs b/Xamarin.Forms.Core/Registrar.cs
index 91b0375fc07..1c161b5b31d 100644
--- a/Xamarin.Forms.Core/Registrar.cs
+++ b/Xamarin.Forms.Core/Registrar.cs
@@ -97,14 +97,14 @@ public Type GetHandlerType(Type viewType)
type = attribute.Type;
- if (type.Name.StartsWith("_"))
+ if (type.Name.StartsWith("_", StringComparison.Ordinal))
{
// TODO: Remove attribute2 once renderer names have been unified across all platforms
var attribute2 = type.GetTypeInfo().GetCustomAttribute();
if (attribute2 != null)
type = attribute2.Type;
- if (type.Name.StartsWith("_"))
+ if (type.Name.StartsWith("_", StringComparison.Ordinal))
{
Register(viewType, null); // Cache this result so we don't work through this chain again
return null;
@@ -156,6 +156,7 @@ static Registrar()
}
internal static Dictionary Effects { get; } = new Dictionary();
+ internal static Dictionary StyleProperties { get; } = new Dictionary();
public static IEnumerable ExtraAssemblies { get; set; }
@@ -165,9 +166,7 @@ public static void RegisterAll(Type[] attrTypes)
{
Assembly[] assemblies = Device.GetAssemblies();
if (ExtraAssemblies != null)
- {
assemblies = assemblies.Union(ExtraAssemblies).ToArray();
- }
Assembly defaultRendererAssembly = Device.PlatformServices.GetType().GetTypeInfo().Assembly;
int indexOfExecuting = Array.IndexOf(assemblies, defaultRendererAssembly);
@@ -185,36 +184,38 @@ public static void RegisterAll(Type[] attrTypes)
foreach (Type attrType in attrTypes)
{
Attribute[] attributes = assembly.GetCustomAttributes(attrType).ToArray();
- if (attributes.Length == 0)
- continue;
-
- foreach (HandlerAttribute attribute in attributes)
+ var length = attributes.Length;
+ for (var i = 0; i < length;i++)
{
+ var attribute = (HandlerAttribute)attributes[i];
if (attribute.ShouldRegister())
Registered.Register(attribute.HandlerType, attribute.TargetType);
}
}
string resolutionName = assembly.FullName;
+ var resolutionNameAttribute = (ResolutionGroupNameAttribute)assembly.GetCustomAttribute(typeof(ResolutionGroupNameAttribute));
+ if (resolutionNameAttribute != null)
+ resolutionName = resolutionNameAttribute.ShortName;
Attribute[] effectAttributes = assembly.GetCustomAttributes(typeof(ExportEffectAttribute)).ToArray();
- if (effectAttributes.Length > 0)
+ var exportEffectsLength = effectAttributes.Length;
+ for (var i = 0; i < exportEffectsLength;i++)
{
- var resolutionNameAttribute = (ResolutionGroupNameAttribute)assembly.GetCustomAttribute(typeof(ResolutionGroupNameAttribute));
- if (resolutionNameAttribute != null)
- {
- resolutionName = resolutionNameAttribute.ShortName;
- }
+ var effect = (ExportEffectAttribute)effectAttributes[i];
+ Effects [resolutionName + "." + effect.Id] = effect.Type;
+ }
- foreach (Attribute attribute in effectAttributes)
- {
- var effect = (ExportEffectAttribute)attribute;
- Effects[resolutionName + "." + effect.Id] = effect.Type;
- }
+ Attribute[] styleAttributes = assembly.GetCustomAttributes(typeof(StyleSheets.StylePropertyAttribute)).ToArray();
+ var stylePropertiesLength = styleAttributes.Length;
+ for (var i = 0; i < stylePropertiesLength; i++)
+ {
+ var attribute = (StyleSheets.StylePropertyAttribute)styleAttributes[i];
+ StyleProperties[attribute.CssPropertyName] = attribute;
}
}
DependencyService.Initialize(assemblies);
}
}
-}
\ No newline at end of file
+}
diff --git a/Xamarin.Forms.Core/StyleSheets/CharExtensions.cs b/Xamarin.Forms.Core/StyleSheets/CharExtensions.cs
new file mode 100644
index 00000000000..a5a00a67714
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/CharExtensions.cs
@@ -0,0 +1,41 @@
+using System.Runtime.CompilerServices;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ static class CharExtensions
+ {
+ //w [ \t\r\n\f]*
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsW(this char c)
+ {
+ return c == ' '
+ || c == '\t'
+ || c == '\r'
+ || c == '\n'
+ || c == '\f';
+ }
+
+ //nmstart [_a-z]|{nonascii}|{escape}
+ //escape {unicode}|\\[^\n\r\f0-9a-f]
+ //nonascii [^\0-\237]
+ // TODO support escape and nonascii
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNmStart(this char c)
+ {
+ return c == '_' || char.IsLetter(c);
+ }
+
+ //nmchar [_a-z0-9-]|{nonascii}|{escape}
+ //unicode \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
+ //escape {unicode}|\\[^\n\r\f0-9a-f]
+ //nonascii [^\0-\237]
+ //TODO support escape, nonascii and unicode
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public static bool IsNmChar(this char c)
+ {
+ return c == '_'
+ || c == '-'
+ || char.IsLetterOrDigit(c);
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/CssReader.cs b/Xamarin.Forms.Core/StyleSheets/CssReader.cs
new file mode 100644
index 00000000000..250fb6ccde6
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/CssReader.cs
@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ sealed class CssReader : TextReader
+ {
+ readonly TextReader _reader;
+
+ public CssReader(TextReader reader)
+ {
+ if (reader == null)
+ throw new ArgumentNullException(nameof(reader));
+
+ _reader = reader;
+ }
+
+ readonly Queue _cache = new Queue();
+
+ //skip comments
+ //TODO unescape escaped sequences
+ public override int Peek()
+ {
+ if (_cache.Count > 0)
+ return _cache.Peek();
+
+ int p = _reader.Peek();
+ if (p <= 0)
+ return p;
+ if (unchecked((char)p) != '/')
+ return p;
+
+ _cache.Enqueue(unchecked((char)_reader.Read()));
+ p = _reader.Peek();
+ if (p <= 0)
+ return _cache.Peek();
+ if (unchecked((char)p) != '*')
+ return _cache.Peek();
+
+ _cache.Clear();
+ _reader.Read(); //consume the '*'
+
+ bool hasStar = false;
+ while (true) {
+ var next = _reader.Read();
+ if (next <= 0)
+ return next;
+ if (unchecked((char)next) == '*')
+ hasStar = true;
+ else if (hasStar && unchecked((char)next) == '/')
+ return Peek(); //recursively call self for comments followign comments
+ else
+ hasStar = false;
+ }
+ }
+
+ //skip comments
+ //TODO unescape escaped sequences
+ public override int Read()
+ {
+ if (_cache.Count > 0)
+ return _cache.Dequeue();
+
+ int p = _reader.Read();
+ if (p <= 0)
+ return p;
+ var c = unchecked((char)p);
+ if (c != '/')
+ return p;
+
+ _cache.Enqueue(c);
+ p = _reader.Read();
+ if (p <= 0)
+ return _cache.Dequeue();
+ c = unchecked((char)p);
+ if (c != '*')
+ return _cache.Dequeue();
+
+ _cache.Clear();
+ _reader.Read(); //consume the '*'
+
+ bool hasStar = false;
+ while (true) {
+ var next = _reader.Read();
+ if (next <= 0)
+ return next;
+ if (unchecked((char)next) == '*')
+ hasStar = true;
+ else if (hasStar && unchecked((char)next) == '/')
+ return Read(); //recursively call self for comments followign comments
+ else
+ hasStar = false;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/IStyleSelectable.cs b/Xamarin.Forms.Core/StyleSheets/IStyleSelectable.cs
new file mode 100644
index 00000000000..4dd337b7838
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/IStyleSelectable.cs
@@ -0,0 +1,19 @@
+using System;
+using System.Collections.Generic;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ interface IStyleSelectable
+ {
+ string Name { get; }
+ string Id { get; }
+ IStyleSelectable Parent { get; }
+ IList Classes { get; }
+ IEnumerable Children { get; }
+ }
+
+ interface IStylable
+ {
+ BindableProperty GetProperty(string key);
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/Selector.cs b/Xamarin.Forms.Core/StyleSheets/Selector.cs
new file mode 100644
index 00000000000..ae27e67a155
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/Selector.cs
@@ -0,0 +1,281 @@
+using System;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ abstract class Selector
+ {
+ Selector()
+ {
+ }
+
+ public static Selector Parse(CssReader reader, char stopChar = '\0')
+ {
+ Selector root = All, workingRoot = All;
+ Operator workingRootParent = null;
+ Action setCurrentSelector = (op, sel) => SetCurrentSelector(ref root, ref workingRoot, ref workingRootParent, op, sel);
+
+ int p;
+ reader.SkipWhiteSpaces();
+ while ((p = reader.Peek()) > 0) {
+ switch (unchecked((char)p)) {
+ case '*':
+ setCurrentSelector(new And(), All);
+ reader.Read();
+ break;
+ case '.':
+ reader.Read();
+ var className = reader.ReadIdent();
+ if (className == null)
+ return Invalid;
+ setCurrentSelector(new And(), new Class(className));
+ break;
+ case '#':
+ reader.Read();
+ var id = reader.ReadName();
+ if (id == null)
+ return Invalid;
+ setCurrentSelector(new And(), new Id(id));
+ break;
+ case '[':
+ throw new NotImplementedException("Attributes not implemented");
+ case ',':
+ reader.Read();
+ setCurrentSelector(new Or(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case '+':
+ reader.Read();
+ setCurrentSelector(new Adjacent(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case '~':
+ reader.Read();
+ setCurrentSelector(new Sibling(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case '>':
+ reader.Read();
+ setCurrentSelector(new Child(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ case ' ':
+ case '\t':
+ case '\n':
+ case '\r':
+ case '\f':
+ reader.Read();
+ bool processWs = false;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (char.IsWhiteSpace(c)) {
+ reader.Read();
+ continue;
+ }
+ processWs = (c != '+'
+ && c != '>'
+ && c != ','
+ && c != '~'
+ && c != stopChar);
+ break;
+ }
+ if (!processWs)
+ break;
+ setCurrentSelector(new Descendent(), All);
+ reader.SkipWhiteSpaces();
+ break;
+ default:
+ if (unchecked((char)p) == stopChar)
+ return root;
+
+ var elementName = reader.ReadIdent();
+ if (elementName == null)
+ return Invalid;
+ setCurrentSelector(new And(), new Element(elementName));
+ break;
+ }
+ }
+ return root;
+ }
+
+ static void SetCurrentSelector(ref Selector root, ref Selector workingRoot, ref Operator workingRootParent, Operator op, Selector sel)
+ {
+ var updateRoot = root == workingRoot;
+
+ op.Left = workingRoot;
+ op.Right = sel;
+ workingRoot = op;
+ if (workingRootParent != null)
+ workingRootParent.Right = workingRoot;
+
+ if (updateRoot)
+ root = workingRoot;
+
+ if (workingRoot is Or) {
+ workingRootParent = (Operator)workingRoot;
+ workingRoot = sel;
+ }
+ }
+
+ public abstract bool Matches(IStyleSelectable styleable);
+
+ internal static Selector Invalid = new Generic(s => false);
+ internal static Selector All = new Generic(s => true);
+
+ abstract class UnarySelector : Selector
+ {
+ }
+
+ abstract class Operator : Selector
+ {
+ public Selector Left { get; set; } = Invalid;
+ public Selector Right { get; set; } = Invalid;
+ }
+
+ sealed class Generic : UnarySelector
+ {
+ readonly Func func;
+ public Generic(Func func)
+ {
+ this.func = func;
+ }
+
+ public override bool Matches(IStyleSelectable styleable) => func(styleable);
+ }
+
+ sealed class Class : UnarySelector
+ {
+ public Class(string className)
+ {
+ ClassName = className;
+ }
+
+ public string ClassName { get; }
+ public override bool Matches(IStyleSelectable styleable)
+ => styleable.Classes != null && styleable.Classes.Contains(ClassName);
+ }
+
+ sealed class Id : UnarySelector
+ {
+ public Id(string id)
+ {
+ IdName = id;
+ }
+
+ public string IdName { get; }
+ public override bool Matches(IStyleSelectable styleable) => styleable.Id == IdName;
+ }
+
+ sealed class Or : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable) => Right.Matches(styleable) || Left.Matches(styleable);
+ }
+
+ sealed class And : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable) => Right.Matches(styleable) && Left.Matches(styleable);
+ }
+
+ sealed class Element : UnarySelector
+ {
+ public Element(string elementName)
+ {
+ ElementName = elementName;
+ }
+ public string ElementName { get; }
+ public override bool Matches(IStyleSelectable styleable) =>
+ string.Equals(styleable.Name, ElementName, StringComparison.OrdinalIgnoreCase);
+ }
+
+ sealed class Child : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable) =>
+ Right.Matches(styleable) && styleable.Parent != null && Left.Matches(styleable.Parent);
+ }
+
+ sealed class Descendent : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable)
+ {
+ if (!Right.Matches(styleable))
+ return false;
+ var parent = styleable.Parent;
+ while (parent != null) {
+ if (Left.Matches(parent))
+ return true;
+ parent = parent.Parent;
+ }
+ return false;
+ }
+ }
+
+ sealed class Adjacent : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable)
+ {
+ if (!Right.Matches(styleable))
+ return false;
+ if (styleable.Parent == null)
+ return false;
+
+ IStyleSelectable prev = null;
+ foreach (var elem in styleable.Parent.Children) {
+ if (elem == styleable && prev != null)
+ return Left.Matches(prev);
+ prev = elem;
+ }
+ return false;
+ //var index = styleable.Parent.Children.IndexOf(styleable);
+ //if (index == 0)
+ // return false;
+ //var adjacent = styleable.Parent.Children[index - 1];
+ //return Left.Matches(adjacent);
+ }
+ }
+
+ sealed class Sibling : Operator
+ {
+ public override bool Matches(IStyleSelectable styleable)
+ {
+ if (!Right.Matches(styleable))
+ return false;
+ if (styleable.Parent == null)
+ return false;
+
+ int selfIndex = 0;
+ bool foundSelfInParent = false;
+ foreach (var elem in styleable.Parent.Children) {
+ if (elem == styleable) {
+ foundSelfInParent = true;
+ break;
+ }
+ ++selfIndex;
+ }
+
+ if (!foundSelfInParent)
+ return false;
+
+ int index = 0;
+ foreach (var elem in styleable.Parent.Children) {
+ if (index >= selfIndex)
+ return false;
+ if (Left.Matches(elem))
+ return true;
+ ++index;
+ }
+
+ return false;
+
+ //var index = styleable.Parent.Children.IndexOf(styleable);
+ //if (index == 0)
+ // return false;
+ //int siblingIndex = -1;
+ //for (var i = 0; i < index; i++)
+ // if (Left.Matches(styleable.Parent.Children[i])) {
+ // siblingIndex = i;
+ // break;
+ // }
+ //return siblingIndex != -1;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/Style.cs b/Xamarin.Forms.Core/StyleSheets/Style.cs
new file mode 100644
index 00000000000..080a81a93d4
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/Style.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.Diagnostics.Contracts;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using Xamarin.Forms.Xaml;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ sealed class Style
+ {
+ Style()
+ {
+ }
+
+ public IDictionary Declarations { get; set; } = new Dictionary();
+ Dictionary, object> convertedValues = new Dictionary, object>();
+
+ public static Style Parse(CssReader reader, char stopChar = '\0')
+ {
+ Style style = new Style();
+ string propertyName = null, propertyValue = null;
+
+ int p;
+ reader.SkipWhiteSpaces();
+ bool readingName = true;
+ while ((p = reader.Peek()) > 0) {
+ switch (unchecked((char)p)) {
+ case ':':
+ reader.Read();
+ readingName = false;
+ reader.SkipWhiteSpaces();
+ break;
+ case ';':
+ reader.Read();
+ if (!string.IsNullOrEmpty(propertyName) && !string.IsNullOrEmpty(propertyValue))
+ style.Declarations.Add(propertyName, propertyValue);
+ propertyName = propertyValue = null;
+ readingName = true;
+ reader.SkipWhiteSpaces();
+ break;
+ default:
+ if ((char)p == stopChar)
+ return style;
+
+ if (readingName) {
+ propertyName = reader.ReadIdent();
+ if (propertyName == null)
+ throw new Exception();
+ } else
+ propertyValue = reader.ReadUntil(stopChar, ';', ':');
+ break;
+ }
+ }
+ return style;
+ }
+
+ public void Apply(VisualElement styleable)
+ {
+ if (styleable == null)
+ throw new ArgumentNullException(nameof(styleable));
+
+ foreach (var decl in Declarations) {
+ var property = ((IStylable)styleable).GetProperty(decl.Key);
+ if (property == null)
+ continue;
+ object value;
+ if (!convertedValues.TryGetValue(decl, out value))
+ convertedValues[decl] = (value = Convert(decl.Value, property));
+ styleable.SetValue(property, value, fromStyle: true);
+ }
+
+ foreach (var child in styleable.LogicalChildrenInternal) {
+ var ve = child as VisualElement;
+ if (ve == null)
+ continue;
+ Apply(ve);
+ }
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static object Convert(object value, BindableProperty property)
+ {
+ Func minforetriever = () => property.DeclaringType.GetRuntimeProperty(property.PropertyName) as MemberInfo
+ ?? property.DeclaringType.GetRuntimeMethod("Get" + property.PropertyName, new[] { typeof(BindableObject) }) as MemberInfo;
+ return value.ConvertTo(property.ReturnType, minforetriever, null);
+ }
+
+ public void UnApply(IStylable styleable)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/StylePropertyAttribute.cs b/Xamarin.Forms.Core/StyleSheets/StylePropertyAttribute.cs
new file mode 100644
index 00000000000..0dbacd0f44c
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/StylePropertyAttribute.cs
@@ -0,0 +1,21 @@
+using System;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ [AttributeUsage(AttributeTargets.Assembly, AllowMultiple = true, Inherited = true)]
+ sealed class StylePropertyAttribute : Attribute
+ {
+ public string CssPropertyName { get; }
+ public string BindablePropertyName { get; }
+ public Type TargetType { get; }
+ public BindableProperty BindableProperty { get; set; }
+
+
+ public StylePropertyAttribute(string cssPropertyName, Type targetType, string bindablePropertyName)
+ {
+ CssPropertyName = cssPropertyName;
+ BindablePropertyName = bindablePropertyName;
+ TargetType = targetType;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/StyleSheet.cs b/Xamarin.Forms.Core/StyleSheets/StyleSheet.cs
new file mode 100644
index 00000000000..3103c2a75d6
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/StyleSheet.cs
@@ -0,0 +1,94 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ sealed class StyleSheet : IStyle
+ {
+ StyleSheet()
+ {
+ }
+
+ public IDictionary Styles { get; set; } = new Dictionary();
+
+ public static StyleSheet Parse(TextReader reader)
+ {
+ if (reader == null)
+ throw new ArgumentNullException(nameof(reader));
+
+ return Parse(new CssReader(reader));
+ }
+
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ static StyleSheet Parse(CssReader reader)
+ {
+ var sheet = new StyleSheet();
+
+ Style style = null;
+ var selector = Selector.All;
+
+ int p;
+ bool inStyle = false;
+ reader.SkipWhiteSpaces();
+ while ((p = reader.Peek()) > 0) {
+ switch ((char)p) {
+ case '@':
+ throw new NotSupportedException("AT-rules not supported");
+ case '{':
+ reader.Read();
+ style = Style.Parse(reader, '}');
+ inStyle = true;
+ break;
+ case '}':
+ reader.Read();
+ if (!inStyle)
+ throw new Exception();
+ inStyle = false;
+ sheet.Styles.Add(selector, style);
+ style = null;
+ selector = Selector.All;
+ break;
+ default:
+ selector = Selector.Parse(reader, '{');
+ break;
+ }
+ }
+ return sheet;
+ }
+
+ public Type TargetType => typeof(VisualElement);
+
+ public void Apply(BindableObject bindable)
+ {
+ var styleable = bindable as VisualElement;
+ if (styleable == null)
+ return;
+ Apply(styleable);
+ }
+
+ void Apply(VisualElement styleable)
+ {
+ ApplyCore(styleable);
+ foreach (var child in styleable.LogicalChildrenInternal)
+ Apply(child);
+ }
+
+ void ApplyCore(VisualElement styleable)
+ {
+ foreach (var kvp in Styles) {
+ var selector = kvp.Key;
+ var style = kvp.Value;
+ if (!selector.Matches(styleable))
+ continue;
+ style.Apply(styleable);
+ }
+ }
+
+ public void UnApply(BindableObject bindable)
+ {
+ throw new NotImplementedException();
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/StyleSheets/TextReaderExtensions.cs b/Xamarin.Forms.Core/StyleSheets/TextReaderExtensions.cs
new file mode 100644
index 00000000000..6060bd6c5f3
--- /dev/null
+++ b/Xamarin.Forms.Core/StyleSheets/TextReaderExtensions.cs
@@ -0,0 +1,76 @@
+using System;
+using System.IO;
+using System.Linq;
+using System.Text;
+
+namespace Xamarin.Forms.StyleSheets
+{
+ static class TextReaderExtensions
+ {
+ //ident [-]?{nmstart}{nmchar}*
+ public static string ReadIdent(this TextReader reader)
+ {
+ var sb = new StringBuilder();
+ bool first = true;
+ bool hasLeadingDash = false;
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (first && !hasLeadingDash && c == '-') {
+ sb.Append((char)reader.Read());
+ hasLeadingDash = true;
+ } else if (first && c.IsNmStart()) {
+ sb.Append((char)reader.Read());
+ first = false;
+ } else if (first) { //a nmstart is expected
+ throw new Exception();
+ } else if (c.IsNmChar())
+ sb.Append((char)reader.Read());
+ else
+ break;
+ }
+ return sb.ToString();
+ }
+
+ //name {nmchar}+
+ public static string ReadName(this TextReader reader)
+ {
+ var sb = new StringBuilder();
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (c.IsNmChar())
+ sb.Append((char)reader.Read());
+ else
+ break;
+ }
+ return sb.ToString();
+ }
+
+ public static string ReadUntil(this TextReader reader, params char[] limit)
+ {
+ var sb = new StringBuilder();
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (limit != null && limit.Contains(c))
+ break;
+ reader.Read();
+ sb.Append(c);
+ }
+ return sb.ToString();
+ }
+
+ //w [ \t\r\n\f]*
+ public static void SkipWhiteSpaces(this TextReader reader)
+ {
+ int p;
+ while ((p = reader.Peek()) > 0) {
+ var c = unchecked((char)p);
+ if (!c.IsW())
+ break;
+ reader.Read();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Core/VisualElement.cs b/Xamarin.Forms.Core/VisualElement.cs
index 520b06b1787..959f750a792 100644
--- a/Xamarin.Forms.Core/VisualElement.cs
+++ b/Xamarin.Forms.Core/VisualElement.cs
@@ -616,6 +616,7 @@ protected override void OnParentSet()
NavigationProxy.Inner = null;
}
#pragma warning restore 0618
+ ApplyStyleSheetOnParentSet();
}
protected virtual void OnSizeAllocated(double width, double height)
diff --git a/Xamarin.Forms.Core/VisualElement_StyleSheet.cs b/Xamarin.Forms.Core/VisualElement_StyleSheet.cs
new file mode 100644
index 00000000000..8ea40256eac
--- /dev/null
+++ b/Xamarin.Forms.Core/VisualElement_StyleSheet.cs
@@ -0,0 +1,83 @@
+using System.Collections.Generic;
+using System.IO;
+using System.Reflection;
+using Xamarin.Forms.Internals;
+using Xamarin.Forms.StyleSheets;
+
+
+namespace Xamarin.Forms
+{
+ public partial class Element : IStyleSelectable
+ {
+ IEnumerable IStyleSelectable.Children => LogicalChildrenInternal;
+
+ IList IStyleSelectable.Classes => null;
+
+ string IStyleSelectable.Id => StyleId;
+
+ string _styleSelectableName;
+ string IStyleSelectable.Name => _styleSelectableName ?? (_styleSelectableName = GetType().Name);
+
+ IStyleSelectable IStyleSelectable.Parent => Parent;
+ }
+
+ public partial class VisualElement : IStyleSelectable, IStylable
+ {
+ IList IStyleSelectable.Classes => StyleClass;
+
+ public static readonly BindableProperty StyleSheetProperty =
+ BindableProperty.Create("StyleSheet", typeof(string), typeof(VisualElement), default(string),
+ propertyChanged: (bp, o, n) => ((VisualElement)bp).OnStyleSheetChanged((string)o, (string)n));
+
+ public string StyleSheet {
+ get { return (string)GetValue(StyleSheetProperty); }
+ set { SetValue(StyleSheetProperty, value); }
+ }
+
+ StyleSheet _sheet;
+ void OnStyleSheetChanged(string oldValue, string newValue)
+ {
+ if (_sheet != null)
+ _sheet.UnApply(this);
+ _sheet = Xamarin.Forms.StyleSheets.StyleSheet.Parse(new StringReader(newValue));
+ if (_sheet != null)
+ _sheet.Apply(this);
+ }
+
+ BindableProperty IStylable.GetProperty(string key)
+ {
+ StylePropertyAttribute styleAttribute;
+ if (!Xamarin.Forms.Internals.Registrar.StyleProperties.TryGetValue(key, out styleAttribute))
+ return null;
+
+ if (!styleAttribute.TargetType.GetTypeInfo().IsAssignableFrom(GetType().GetTypeInfo()))
+ return null;
+
+ if (styleAttribute.BindableProperty != null)
+ return styleAttribute.BindableProperty;
+
+ var bpField = GetType().GetField(styleAttribute.BindablePropertyName);
+ if (bpField == null || !bpField.IsStatic)
+ return null;
+
+ return (styleAttribute.BindableProperty = bpField.GetValue(null) as BindableProperty);
+ }
+
+ void ApplyStyleSheetOnParentSet()
+ {
+ var parent = Parent;
+ if (parent == null)
+ return;
+ var sheets = new List();
+ while (parent != null) {
+ var visualParent = parent as VisualElement;
+ var sheet = visualParent?._sheet;
+ if (sheet != null)
+ sheets.Add(sheet);
+ parent = parent.Parent;
+ }
+ for (var i = sheets.Count - 1; i >= 0; i--)
+ sheets[i].Apply(this);
+ }
+ }
+}
\ 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 1580077f0d6..f529235098e 100644
--- a/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
+++ b/Xamarin.Forms.Core/Xamarin.Forms.Core.csproj
@@ -179,7 +179,6 @@
-
@@ -462,6 +461,15 @@
+
+
+
+
+
+
+
+
+
diff --git a/Xamarin.Forms.Xaml.UnitTests/MarkupExtensionTests.cs b/Xamarin.Forms.Xaml.UnitTests/MarkupExtensionTests.cs
index 9d76c016686..26854574c25 100644
--- a/Xamarin.Forms.Xaml.UnitTests/MarkupExtensionTests.cs
+++ b/Xamarin.Forms.Xaml.UnitTests/MarkupExtensionTests.cs
@@ -36,8 +36,6 @@ public object ProvideValue (IServiceProvider serviceProvider)
result += targetValueProvider != null;
var xamlType = serviceProvider.GetService (typeof(IXamlTypeResolver));
result += xamlType != null;
- var rootObject = serviceProvider.GetService (typeof(IRootObjectProvider));
- result += rootObject != null;
}
return result;
}
@@ -132,13 +130,12 @@ public void TestServiceProvider ()
var serviceProvider = new Internals.XamlServiceProvider (null, null) {
IProvideValueTarget = new Internals.XamlValueTargetProvider (null, null, null, null),
IXamlTypeResolver = typeResolver,
- IRootObjectProvider = new Internals.XamlRootObjectProvider(null),
};
var result = (new MarkupExtensionParser ()).ParseExpression (ref markupString, serviceProvider);
Assert.That (result, Is.InstanceOf ());
- Assert.AreEqual ("TrueTrueTrue", result);
+ Assert.AreEqual ("TrueTrue", result);
}
[Test]
diff --git a/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml
new file mode 100644
index 00000000000..0fbfc092689
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml.cs b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml.cs
new file mode 100644
index 00000000000..6a479cbb5c8
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/StyleSheet.xaml.cs
@@ -0,0 +1,49 @@
+using System;
+using NUnit.Framework;
+
+using Xamarin.Forms.Core.UnitTests;
+using NUnit.Framework.Constraints;
+
+namespace Xamarin.Forms.Xaml.UnitTests
+{
+ public partial class StyleSheet : ContentPage
+ {
+ public StyleSheet()
+ {
+ InitializeComponent();
+ }
+
+ public StyleSheet(bool useCompiledXaml)
+ {
+ //this stub will be replaced at compile time
+ }
+
+ [TestFixture]
+ public class Tests
+ {
+ [SetUp]
+ public void SetUp()
+ {
+ Device.PlatformServices = new MockPlatformServices();
+ Xamarin.Forms.Internals.Registrar.RegisterAll(new Type[0]);
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void EmbeddedStyleSheetsAreLoaded(bool useCompiledXaml)
+ {
+ var layout = new StyleSheet(useCompiledXaml);
+ Assert.That(layout.StyleSheet, new SubstringConstraint ("label"));
+ }
+
+ [TestCase(false)]
+ [TestCase(true)]
+ public void StyleSheetsAreApplied(bool useCompiledXaml)
+ {
+ var layout = new StyleSheet(useCompiledXaml);
+ Assert.That(layout.label0.TextColor, Is.EqualTo(Color.Azure));
+ Assert.That(layout.label0.BackgroundColor, Is.EqualTo(Color.AliceBlue));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
index b254de4b6a8..4b5a153a54d 100644
--- a/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
+++ b/Xamarin.Forms.Xaml.UnitTests/Xamarin.Forms.Xaml.UnitTests.csproj
@@ -445,7 +445,6 @@
Bz44216.xaml
- AcceptEmptyServiceProvider.xaml
AcceptEmptyServiceProvider.xaml
@@ -505,6 +504,9 @@
MergedResourceDictionaries.xaml
+
+ StyleSheet.xaml
+
@@ -534,15 +536,6 @@
Xamarin.Forms.Maps
-
-
-
-
-
-
-
-
-
MSBuild:UpdateDesignTimeXaml
@@ -925,6 +918,23 @@
MSBuild:UpdateDesignTimeXaml
+
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
+
+ MSBuild:UpdateDesignTimeXaml
+
@@ -941,23 +951,6 @@
-
- MSBuild:UpdateDesignTimeXaml
-
-
-
-
- MSBuild:UpdateDesignTimeXaml
-
-
-
-
- MSBuild:UpdateDesignTimeXaml
-
-
-
-
- MSBuild:UpdateDesignTimeXaml
-
+
diff --git a/Xamarin.Forms.Xaml.UnitTests/css/bar.css b/Xamarin.Forms.Xaml.UnitTests/css/bar.css
new file mode 100644
index 00000000000..5f282702bb0
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/css/bar.css
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml.UnitTests/css/foo.css b/Xamarin.Forms.Xaml.UnitTests/css/foo.css
new file mode 100644
index 00000000000..9cca27b33d7
--- /dev/null
+++ b/Xamarin.Forms.Xaml.UnitTests/css/foo.css
@@ -0,0 +1,4 @@
+label {
+ color: azure;
+ background-color: aliceblue;
+}
\ No newline at end of file
diff --git a/Xamarin.Forms.Xaml/MarkupExtensions/StaticResourceExtension.cs b/Xamarin.Forms.Xaml/MarkupExtensions/StaticResourceExtension.cs
index 14e0b8be378..f7298e4cf01 100644
--- a/Xamarin.Forms.Xaml/MarkupExtensions/StaticResourceExtension.cs
+++ b/Xamarin.Forms.Xaml/MarkupExtensions/StaticResourceExtension.cs
@@ -1,10 +1,39 @@
-using System;
+using System;
using System.Reflection;
using System.Xml;
+using System.IO;
using Xamarin.Forms.Internals;
+using Xamarin.Forms.Xaml.Internals;
namespace Xamarin.Forms.Xaml
{
+ [ContentProperty("Path")]
+ public sealed class StyleSheetExtension : IMarkupExtension
+ {
+ public string Path { get; set; }
+
+ public object ProvideValue(IServiceProvider serviceProvider)
+ {
+ if (serviceProvider == null)
+ throw new ArgumentNullException(nameof(serviceProvider));
+ if (Path == null)
+ throw new XamlParseException("you must specify a key in {StaticResource}",
+ (serviceProvider.GetService(typeof(IXmlLineInfoProvider)) as IXmlLineInfoProvider)?.XmlLineInfo);
+
+ var currentAssemblyProvider = serviceProvider.GetService(typeof(ICurrentAssemblyProvider)) as ICurrentAssemblyProvider;
+ if (currentAssemblyProvider == null)
+ throw new ArgumentException();
+
+ var asm = currentAssemblyProvider.CurrentAssembly;
+ using (var stream = asm.GetManifestResourceStream(Path))
+ using (var reader = new StreamReader(stream)) {
+ var css = reader.ReadToEnd();
+ var alternateCss = ResourceLoader.ResourceProvider?.Invoke(Path);
+ return alternateCss ?? css;
+ }
+ }
+ }
+
[ContentProperty("Key")]
public sealed class StaticResourceExtension : IMarkupExtension
{
diff --git a/Xamarin.Forms.Xaml/XamlLoader.cs b/Xamarin.Forms.Xaml/XamlLoader.cs
index f74982744bd..16b54e3cfe0 100644
--- a/Xamarin.Forms.Xaml/XamlLoader.cs
+++ b/Xamarin.Forms.Xaml/XamlLoader.cs
@@ -275,4 +275,4 @@ public RuntimeRootNode(XmlType xmlType, object root, IXmlNamespaceResolver resol
public object Root { get; internal set; }
}
}
-}
\ No newline at end of file
+}
diff --git a/Xamarin.Forms.Xaml/XamlServiceProvider.cs b/Xamarin.Forms.Xaml/XamlServiceProvider.cs
index 7a8c4a58077..f7471297222 100644
--- a/Xamarin.Forms.Xaml/XamlServiceProvider.cs
+++ b/Xamarin.Forms.Xaml/XamlServiceProvider.cs
@@ -15,8 +15,6 @@ internal XamlServiceProvider(INode node, HydrationContext context)
object targetObject;
if (node != null && node.Parent != null && context.Values.TryGetValue(node.Parent, out targetObject))
IProvideValueTarget = new XamlValueTargetProvider(targetObject, node, context, null);
- if (context != null)
- IRootObjectProvider = new XamlRootObjectProvider(context.RootElement);
if (node != null)
{
IXamlTypeResolver = new XamlTypeResolver(node.NamespaceResolver, XamlParser.GetElementType,
@@ -34,6 +32,9 @@ internal XamlServiceProvider(INode node, HydrationContext context)
IXmlLineInfoProvider = new XmlLineInfoProvider(xmlLineInfo);
IValueConverterProvider = new ValueConverterProvider();
+
+ if (context != null)
+ services[typeof(ICurrentAssemblyProvider)] = new CurrentAssemblyProvider(context.RootElement.GetType().GetTypeInfo().Assembly);
}
public XamlServiceProvider()
@@ -53,12 +54,6 @@ internal IXamlTypeResolver IXamlTypeResolver
set { services[typeof (IXamlTypeResolver)] = value; }
}
- internal IRootObjectProvider IRootObjectProvider
- {
- get { return (IRootObjectProvider)GetService(typeof (IRootObjectProvider)); }
- set { services[typeof (IRootObjectProvider)] = value; }
- }
-
internal IXmlLineInfoProvider IXmlLineInfoProvider
{
get { return (IXmlLineInfoProvider)GetService(typeof (IXmlLineInfoProvider)); }
@@ -250,16 +245,6 @@ Type Resolve(string qualifiedTypeName, IServiceProvider serviceProvider, out Xam
XmlType xmlType, IXmlLineInfo xmlInfo, Assembly currentAssembly, out XamlParseException exception);
}
- class XamlRootObjectProvider : IRootObjectProvider
- {
- public XamlRootObjectProvider(object rootObject)
- {
- RootObject = rootObject;
- }
-
- public object RootObject { get; }
- }
-
public class XmlLineInfoProvider : IXmlLineInfoProvider
{
public XmlLineInfoProvider(IXmlLineInfo xmlLineInfo)
@@ -280,6 +265,11 @@ public class NameScopeProvider : INameScopeProvider
public INameScope NameScope { get; set; }
}
+ interface ICurrentAssemblyProvider
+ {
+ Assembly CurrentAssembly { get; }
+ }
+
public class XmlNamespaceResolver : IXmlNamespaceResolver
{
readonly Dictionary namespaces = new Dictionary();
@@ -307,4 +297,13 @@ public void Add(string prefix, string ns)
namespaces.Add(prefix, ns);
}
}
+
+ public class CurrentAssemblyProvider : ICurrentAssemblyProvider
+ {
+ public CurrentAssemblyProvider(Assembly currentAssembly)
+ {
+ CurrentAssembly = currentAssembly;
+ }
+ public Assembly CurrentAssembly { get; set; }
+ }
}
\ No newline at end of file