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