From 0e812bb0d67be7ce5f8290c70e2843c834a88d62 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 13 Sep 2015 20:08:48 +0200 Subject: [PATCH 01/11] Whitespace. --- src/Perspex.Controls/Image.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Perspex.Controls/Image.cs b/src/Perspex.Controls/Image.cs index d34948e2ee..0782b51f2a 100644 --- a/src/Perspex.Controls/Image.cs +++ b/src/Perspex.Controls/Image.cs @@ -23,7 +23,7 @@ namespace Perspex.Controls /// public static readonly PerspexProperty StretchProperty = PerspexProperty.Register(nameof(Stretch), Stretch.Uniform); - + /// /// Gets or sets the bitmap image that will be displayed. /// From eb220a9388c3cb3b2db6ab9fbb9802594b486d1d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Sep 2015 02:46:38 +0200 Subject: [PATCH 02/11] Started implementing Selector parser. --- .../Parsers/SelectorGrammarTests.cs | 140 ++++++++++++++ .../Perspex.Markup.Xaml.UnitTests.csproj | 9 +- .../packages.config | 1 + .../Parsers/SelectorGrammar.cs | 176 ++++++++++++++++++ .../Perspex.Markup.Xaml.csproj | 2 + .../Properties/AssemblyInfo.cs | 2 + src/Perspex.Styling/Styling/Selectors.cs | 2 +- 7 files changed, 329 insertions(+), 3 deletions(-) create mode 100644 Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs create mode 100644 src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs diff --git a/Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs b/Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs new file mode 100644 index 0000000000..e6b563160e --- /dev/null +++ b/Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs @@ -0,0 +1,140 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Linq; +using Perspex.Markup.Xaml.Parsers; +using Sprache; +using Xunit; + +namespace Perspex.Xaml.Base.UnitTest.Parsers +{ + public class SelectorGrammarTests + { + [Fact] + public void OfType() + { + var result = SelectorGrammar.Selector.Parse("Button").ToList(); + + Assert.Equal( + new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" } }, + result); + } + + [Fact] + public void Name() + { + var result = SelectorGrammar.Selector.Parse("#foo").ToList(); + + Assert.Equal( + new[] { new SelectorGrammar.NameSyntax { Name = "foo" }, }, + result); + } + + [Fact] + public void OfType_Name() + { + var result = SelectorGrammar.Selector.Parse("Button#foo").ToList(); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.NameSyntax { Name = "foo" }, + }, + result); + } + + [Fact] + public void Class() + { + var result = SelectorGrammar.Selector.Parse(".foo").ToList(); + + Assert.Equal( + new[] { new SelectorGrammar.ClassSyntax { Class = "foo" } }, + result); + } + + [Fact] + public void Pseudoclass() + { + var result = SelectorGrammar.Selector.Parse(":foo").ToList(); + + Assert.Equal( + new[] { new SelectorGrammar.ClassSyntax { Class = ":foo" } }, + result); + } + + [Fact] + public void OfType_Class() + { + var result = SelectorGrammar.Selector.Parse("Button.foo").ToList(); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void OfType_Child_Class() + { + var result = SelectorGrammar.Selector.Parse("Button < .foo").ToList(); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.ChildSyntax { }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void OfType_Descendent_Class() + { + var result = SelectorGrammar.Selector.Parse("Button .foo").ToList(); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.DescendentSyntax { }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void OfType_Template_Class() + { + var result = SelectorGrammar.Selector.Parse("Button /template/ .foo").ToList(); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.TemplateSyntax { }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + + [Fact] + public void OfType_Property() + { + var result = SelectorGrammar.Selector.Parse("Button[Foo=bar]").ToList(); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.PropertySyntax { Property = "Foo", Value = "bar" }, + }, + result); + } + } +} diff --git a/Tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj b/Tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj index 67c0aac88f..d96439d1fe 100644 --- a/Tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj +++ b/Tests/Perspex.Markup.Xaml.UnitTests/Perspex.Markup.Xaml.UnitTests.csproj @@ -8,8 +8,8 @@ {99135EAB-653D-47E4-A378-C96E1278CA44} Library Properties - Perspex.Xaml.Base.UnitTest - Perspex.Xaml.Base.UnitTest + Perspex.Markup.Xaml.UnitTests + Perspex.Markup.Xaml.UnitTests v4.5.1 512 10.0 @@ -61,6 +61,10 @@ ..\..\packages\Splat.1.6.1\lib\Net45\Splat.dll True + + ..\..\packages\Sprache.SuperJMN.2.0.0.50\lib\portable-net451+netcore451+wpa81\Sprache.dll + True + ..\..\packages\Rx-Core.2.2.5\lib\net45\System.Reactive.Core.dll @@ -103,6 +107,7 @@ + diff --git a/Tests/Perspex.Markup.Xaml.UnitTests/packages.config b/Tests/Perspex.Markup.Xaml.UnitTests/packages.config index 8268799d3b..f79ea1b907 100644 --- a/Tests/Perspex.Markup.Xaml.UnitTests/packages.config +++ b/Tests/Perspex.Markup.Xaml.UnitTests/packages.config @@ -8,6 +8,7 @@ + diff --git a/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs new file mode 100644 index 0000000000..f113fa6c20 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs @@ -0,0 +1,176 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using Perspex.Styling; +using Sprache; + +namespace Perspex.Markup.Xaml.Parsers +{ + internal class SelectorGrammar + { + public static readonly Parser CombiningCharacter = Parse.Char( + c => + { + var cat = CharUnicodeInfo.GetUnicodeCategory(c); + return cat == UnicodeCategory.NonSpacingMark || + cat == UnicodeCategory.SpacingCombiningMark; + }, + "Connecting Character"); + + public static readonly Parser ConnectingCharacter = Parse.Char( + c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.ConnectorPunctuation, + "Connecting Character"); + + public static readonly Parser FormattingCharacter = Parse.Char( + c => CharUnicodeInfo.GetUnicodeCategory(c) == UnicodeCategory.Format, + "Connecting Character"); + + public static readonly Parser IdentifierStart = Parse.Letter.Or(Parse.Char('_')); + + public static readonly Parser IdentifierChar = Parse + .LetterOrDigit + .Or(ConnectingCharacter) + .Or(CombiningCharacter) + .Or(FormattingCharacter); + + public static readonly Parser Identifier = + from start in IdentifierStart.Once().Text() + from @char in IdentifierChar.Many().Text() + select start + @char; + + public static readonly Parser OfType = + from identifier in Identifier select new OfTypeSyntax { TypeName = identifier }; + + public static readonly Parser Name = + from hash in Parse.Char('#') + from identifier in Identifier + select new NameSyntax { Name = identifier }; + + public static readonly Parser ClassStart = Parse.Char('_').Or(Parse.Letter); + + public static readonly Parser ClassChar = ClassStart.Or(Parse.Numeric); + + public static readonly Parser ClassIdentifier = + from start in ClassStart.Once().Text() + from @char in ClassChar.Many().Text() + select start + @char; + + public static readonly Parser StandardClass = + from dot in Parse.Char('.').Once() + from identifier in ClassIdentifier + select new ClassSyntax { Class = identifier }; + + public static readonly Parser Pseduoclass = + from colon in Parse.Char(':').Once() + from identifier in ClassIdentifier + select new ClassSyntax { Class = ':' + identifier }; + + public static readonly Parser Class = StandardClass.Or(Pseduoclass); + + public static readonly Parser Property = + from open in Parse.Char('[').Once() + from identifier in Identifier + from eq in Parse.Char('=').Once() + from value in Parse.CharExcept(']').Many().Text() + from close in Parse.Char(']').Once() + select new PropertySyntax { Property = identifier, Value = value }; + + public static readonly Parser Child = Parse.Char('<').Token().Return(new ChildSyntax()); + + public static readonly Parser Descendent = + from child in Parse.WhiteSpace.Many() + select new DescendentSyntax(); + + public static readonly Parser Template = + from template in Parse.String("/template/").Token() + select new TemplateSyntax(); + + //public static readonly + + public static readonly Parser SingleSelector = + OfType + .Or(Name) + .Or(Class) + .Or(Property) + .Or(Child) + .Or(Template) + .Or(Descendent); + + public static readonly Parser> Selector = SingleSelector.Many(); + + public interface ISyntax + { + } + + public class OfTypeSyntax : ISyntax + { + public string TypeName { get; set; } + + public override bool Equals(object obj) + { + return obj is OfTypeSyntax && ((OfTypeSyntax)obj).TypeName == TypeName; + } + } + + public class ClassSyntax : ISyntax + { + public string Class { get; set; } + + public override bool Equals(object obj) + { + return obj is ClassSyntax && ((ClassSyntax)obj).Class == Class; + } + } + + public class NameSyntax : ISyntax + { + public string Name { get; set; } + + public override bool Equals(object obj) + { + return obj is NameSyntax && ((NameSyntax)obj).Name == Name; + } + } + + public class PropertySyntax : ISyntax + { + public string Property { get; set; } + + public string Value { get; set; } + + public override bool Equals(object obj) + { + return obj is PropertySyntax && + ((PropertySyntax)obj).Property == Property && + ((PropertySyntax)obj).Value == Value; + } + } + + public class ChildSyntax : ISyntax + { + public override bool Equals(object obj) + { + return obj is ChildSyntax; + } + } + + public class DescendentSyntax : ISyntax + { + public override bool Equals(object obj) + { + return obj is DescendentSyntax; + } + } + + public class TemplateSyntax : ISyntax + { + public override bool Equals(object obj) + { + return obj is TemplateSyntax; + } + } + } +} diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index e146cac468..069a5b1bbe 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -40,6 +40,7 @@ + @@ -51,6 +52,7 @@ + diff --git a/src/Markup/Perspex.Markup.Xaml/Properties/AssemblyInfo.cs b/src/Markup/Perspex.Markup.Xaml/Properties/AssemblyInfo.cs index eea5e0803d..5f71d84370 100644 --- a/src/Markup/Perspex.Markup.Xaml/Properties/AssemblyInfo.cs +++ b/src/Markup/Perspex.Markup.Xaml/Properties/AssemblyInfo.cs @@ -2,5 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using System.Runtime.CompilerServices; [assembly: AssemblyTitle("Perspex.Markup.Xaml")] +[assembly: InternalsVisibleTo("Perspex.Markup.Xaml.UnitTests")] \ No newline at end of file diff --git a/src/Perspex.Styling/Styling/Selectors.cs b/src/Perspex.Styling/Styling/Selectors.cs index efa1fbd983..fc924632ec 100644 --- a/src/Perspex.Styling/Styling/Selectors.cs +++ b/src/Perspex.Styling/Styling/Selectors.cs @@ -78,7 +78,7 @@ namespace Perspex.Styling return new Selector( previous, x => MatchTemplate(x, previous), - " /deep/ ", + " /template/ ", inTemplate: true, stopTraversal: true); } From 193ec2982452309ba945a54eb311946b349063d4 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Sep 2015 13:32:10 +0200 Subject: [PATCH 03/11] More work on parsing style selectors. --- .../Parsers/SelectorGrammarTests.cs | 51 +++++++++++- .../Parsers/SelectorGrammar.cs | 21 +++-- .../Parsers/SelectorParser.cs | 83 +++++++++++++++++++ .../Perspex.Markup.Xaml.csproj | 1 + 4 files changed, 150 insertions(+), 6 deletions(-) create mode 100644 src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs diff --git a/Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs b/Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs index e6b563160e..58bbb465ac 100644 --- a/Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs +++ b/Tests/Perspex.Markup.Xaml.UnitTests/Parsers/SelectorGrammarTests.cs @@ -16,7 +16,17 @@ namespace Perspex.Xaml.Base.UnitTest.Parsers var result = SelectorGrammar.Selector.Parse("Button").ToList(); Assert.Equal( - new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button" } }, + new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button", Xmlns = null } }, + result); + } + + [Fact] + public void NamespacedOfType() + { + var result = SelectorGrammar.Selector.Parse("x|Button").ToList(); + + Assert.Equal( + new[] { new SelectorGrammar.OfTypeSyntax { TypeName = "Button", Xmlns = "x" } }, result); } @@ -93,6 +103,21 @@ namespace Perspex.Xaml.Base.UnitTest.Parsers result); } + [Fact] + public void OfType_Child_Class_No_Spaces() + { + var result = SelectorGrammar.Selector.Parse("Button<.foo").ToList(); + + Assert.Equal( + new SelectorGrammar.ISyntax[] + { + new SelectorGrammar.OfTypeSyntax { TypeName = "Button" }, + new SelectorGrammar.ChildSyntax { }, + new SelectorGrammar.ClassSyntax { Class = "foo" }, + }, + result); + } + [Fact] public void OfType_Descendent_Class() { @@ -136,5 +161,29 @@ namespace Perspex.Xaml.Base.UnitTest.Parsers }, result); } + + [Fact] + public void Namespace_Alone_Fails() + { + Assert.Throws(() => SelectorGrammar.Selector.Parse("ns|").ToList()); + } + + [Fact] + public void Dot_Alone_Fails() + { + Assert.Throws(() => SelectorGrammar.Selector.Parse(". dot").ToList()); + } + + [Fact] + public void Invalid_Identifier_Fails() + { + Assert.Throws(() => SelectorGrammar.Selector.Parse("%foo").ToList()); + } + + [Fact] + public void Invalid_Class_Fails() + { + Assert.Throws(() => SelectorGrammar.Selector.Parse(".%foo").ToList()); + } } } diff --git a/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs index f113fa6c20..b72f77a82a 100644 --- a/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs +++ b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorGrammar.cs @@ -41,8 +41,19 @@ namespace Perspex.Markup.Xaml.Parsers from @char in IdentifierChar.Many().Text() select start + @char; + public static readonly Parser Namespace = + from ns in Parse.Letter.Many().Text() + from bar in Parse.Char('|') + select ns; + public static readonly Parser OfType = - from identifier in Identifier select new OfTypeSyntax { TypeName = identifier }; + from ns in Namespace.Optional() + from identifier in Identifier + select new OfTypeSyntax + { + TypeName = identifier, + Xmlns = ns.GetOrDefault(), + }; public static readonly Parser Name = from hash in Parse.Char('#') @@ -88,8 +99,6 @@ namespace Perspex.Markup.Xaml.Parsers from template in Parse.String("/template/").Token() select new TemplateSyntax(); - //public static readonly - public static readonly Parser SingleSelector = OfType .Or(Name) @@ -99,8 +108,8 @@ namespace Perspex.Markup.Xaml.Parsers .Or(Template) .Or(Descendent); - public static readonly Parser> Selector = SingleSelector.Many(); - + public static readonly Parser> Selector = SingleSelector.Many().End(); + public interface ISyntax { } @@ -109,6 +118,8 @@ namespace Perspex.Markup.Xaml.Parsers { public string TypeName { get; set; } + public string Xmlns { get; set; } + public override bool Equals(object obj) { return obj is OfTypeSyntax && ((OfTypeSyntax)obj).TypeName == TypeName; diff --git a/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs new file mode 100644 index 0000000000..23a0421efb --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Parsers/SelectorParser.cs @@ -0,0 +1,83 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using Perspex.Styling; +using Sprache; + +namespace Perspex.Markup.Xaml.Parsers +{ + /// + /// Parses a from text. + /// + public class SelectorParser + { + private Func _typeResolver; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The type resolver to use. The type resolver is a function which accepts two strings: + /// a type name and a XML namespace prefix and a type name, and should return the resolved + /// type or throw an exception. + /// + public SelectorParser(Func typeResolver) + { + this._typeResolver = typeResolver; + } + + /// + /// Parses a from a string. + /// + /// The string. + /// The parsed selector. + public Selector Parse(string s) + { + var syntax = SelectorGrammar.Selector.Parse(s); + var result = new Selector(); + + foreach (var i in syntax) + { + var ofType = i as SelectorGrammar.OfTypeSyntax; + var @class = i as SelectorGrammar.ClassSyntax; + var name = i as SelectorGrammar.NameSyntax; + var property = i as SelectorGrammar.PropertySyntax; + var child = i as SelectorGrammar.ChildSyntax; + var descendent = i as SelectorGrammar.DescendentSyntax; + var template = i as SelectorGrammar.TemplateSyntax; + + if (ofType != null) + { + result = result.OfType(_typeResolver(ofType.TypeName, ofType.Xmlns)); + } + else if (@class != null) + { + result = result.Class(@class.Class); + } + else if (name != null) + { + result = result.Name(name.Name); + } + else if (property != null) + { + throw new NotImplementedException(); + } + else if (child != null) + { + result = result.Child(); + } + else if (descendent != null) + { + result = result.Descendent(); + } + else if (template != null) + { + result = result.Template(); + } + } + + return result; + } + } +} diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index 069a5b1bbe..badd0b22a2 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -52,6 +52,7 @@ + From 7e2a28c91f9bb8f1e036f0af2167c4e368fbeb2e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Sep 2015 21:08:16 +0200 Subject: [PATCH 04/11] Initial implementation of PerspexPropertyConverter. --- .../Context/PerspexWiringContext.cs | 10 ++++ .../Converters/PerspexPropertyConverter.cs | 57 +++++++++++++++++++ .../Converters/SelectorConverter.cs | 34 +++++++++++ .../Perspex.Markup.Xaml.csproj | 1 + src/Perspex.Base/PerspexObject.cs | 27 +++------ src/Perspex.Base/PerspexProperty.cs | 5 ++ src/Perspex.Styling/Styling/Selector.cs | 28 +++++++++ .../PerspexObjectTests_Metadata.cs | 19 +++++-- .../PerspexPropertyTests.cs | 12 ++++ 9 files changed, 169 insertions(+), 24 deletions(-) create mode 100644 src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyConverter.cs create mode 100644 src/Markup/Perspex.Markup.Xaml/Converters/SelectorConverter.cs diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs index 7ab5758f41..1a8ef84497 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexWiringContext.cs @@ -16,6 +16,7 @@ using Perspex.Markup.Xaml.DataBinding; using Perspex.Markup.Xaml.MarkupExtensions; using Perspex.Media; using Perspex.Media.Imaging; +using Perspex.Styling; namespace Perspex.Markup.Xaml.Context { @@ -42,6 +43,7 @@ namespace Perspex.Markup.Xaml.Context var rootType = typeof(Control); var bindingType = typeof(BindingExtension); var templateType = typeof(XamlDataTemplate); + var styleType = typeof(Style); var definitionForRoot = XamlNamespace .Map(PerspexNs) @@ -59,6 +61,11 @@ namespace Perspex.Markup.Xaml.Context bindingType.Namespace, }), Route.Assembly(templateType.GetTypeInfo().Assembly).WithNamespaces( + new[] + { + templateType.Namespace, + }), + Route.Assembly(styleType.GetTypeInfo().Assembly).WithNamespaces( new[] { templateType.Namespace, @@ -84,7 +91,9 @@ namespace Perspex.Markup.Xaml.Context new TypeConverterRegistration(typeof(Brush), new BrushConverter()), new TypeConverterRegistration(typeof(ColumnDefinitions), new ColumnDefinitionsTypeConverter()), new TypeConverterRegistration(typeof(GridLength), new GridLengthTypeConverter()), + new TypeConverterRegistration(typeof(PerspexProperty), new PerspexPropertyConverter()), new TypeConverterRegistration(typeof(RowDefinitions), new RowDefinitionsTypeConverter()), + new TypeConverterRegistration(typeof(Selector), new SelectorConverter()), new TypeConverterRegistration(typeof(Thickness), new ThicknessConverter()), }; @@ -101,6 +110,7 @@ namespace Perspex.Markup.Xaml.Context new ContentPropertyDefinition(typeof(Decorator), "Child"), new ContentPropertyDefinition(typeof(ItemsControl), "Items"), new ContentPropertyDefinition(typeof(Panel), "Children"), + new ContentPropertyDefinition(typeof(Style), "Setters"), new ContentPropertyDefinition(typeof(TextBlock), "Text"), new ContentPropertyDefinition(typeof(TextBox), "Text"), new ContentPropertyDefinition(typeof(XamlDataTemplate), "Content"), diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyConverter.cs new file mode 100644 index 0000000000..a835401ea9 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Converters/PerspexPropertyConverter.cs @@ -0,0 +1,57 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using System.Linq; +using OmniXaml.ObjectAssembler; +using OmniXaml.TypeConversion; +using Perspex.Markup.Xaml.Parsers; + +namespace Perspex.Markup.Xaml.Converters +{ + public class PerspexPropertyConverter : ITypeConverter + { + public bool CanConvertFrom(IXamlTypeConverterContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public bool CanConvertTo(IXamlTypeConverterContext context, Type destinationType) + { + return false; + } + + public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value) + { + var s = (string)value; + var lastDot = s.LastIndexOf('.'); + + if (lastDot == -1) + { + throw new NotSupportedException("PerspexProperties must currently be fully qualified."); + } + + var typeName = s.Substring(0, lastDot); + var propertyName = s.Substring(lastDot + 1); + + // TODO: Doesn't handle xml namespaces - use GetByQualifiedName when it works with the + // default namespace. + var type = context.TypeRepository.GetByPrefix("", typeName)?.UnderlyingType; + + if (type == null) + { + throw new InvalidOperationException($"Could not find type '{typeName}'."); + } + + // TODO: Handle attached properties. + // TODO: Give decent error message for not found property. + return PerspexObject.GetRegisteredProperties(type).Single(x => x.Name == propertyName); + } + + public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Converters/SelectorConverter.cs b/src/Markup/Perspex.Markup.Xaml/Converters/SelectorConverter.cs new file mode 100644 index 0000000000..dfe858d065 --- /dev/null +++ b/src/Markup/Perspex.Markup.Xaml/Converters/SelectorConverter.cs @@ -0,0 +1,34 @@ +// Copyright (c) The Perspex Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using System; +using System.Globalization; +using OmniXaml.TypeConversion; +using Perspex.Markup.Xaml.Parsers; + +namespace Perspex.Markup.Xaml.Converters +{ + public class SelectorConverter : ITypeConverter + { + public bool CanConvertFrom(IXamlTypeConverterContext context, Type sourceType) + { + return sourceType == typeof(string); + } + + public bool CanConvertTo(IXamlTypeConverterContext context, Type destinationType) + { + return false; + } + + public object ConvertFrom(IXamlTypeConverterContext context, CultureInfo culture, object value) + { + var parser = new SelectorParser((t, ns) => context.TypeRepository.GetByPrefix(ns ?? "", t).UnderlyingType); + return parser.Parse((string)value); + } + + public object ConvertTo(IXamlTypeConverterContext context, CultureInfo culture, object value, Type destinationType) + { + throw new NotImplementedException(); + } + } +} \ No newline at end of file diff --git a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj index badd0b22a2..9308a92fae 100644 --- a/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj +++ b/src/Markup/Perspex.Markup.Xaml/Perspex.Markup.Xaml.csproj @@ -40,6 +40,7 @@ + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 3763326577..2a0a34e350 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -71,6 +71,12 @@ namespace Perspex private static readonly Dictionary> s_registered = new Dictionary>(); + /// + /// The registered attached properties by owner type. + /// + private static readonly Dictionary> s_attached = + new Dictionary>(); + /// /// The parent object that inherited values are inherited from. /// @@ -154,7 +160,7 @@ namespace Perspex _inheritanceParent.PropertyChanged -= ParentPropertyChanged; } - var inherited = (from property in GetProperties(GetType()) + var inherited = (from property in GetRegisteredProperties(GetType()) where property.Inherits select new { @@ -245,7 +251,7 @@ namespace Perspex /// /// The type. /// A collection of definitions. - public static IEnumerable GetProperties(Type type) + public static IEnumerable GetRegisteredProperties(Type type) { Contract.Requires(type != null); @@ -432,22 +438,7 @@ namespace Perspex /// public IEnumerable GetRegisteredProperties() { - Type type = GetType(); - - while (type != null) - { - List list; - - if (s_registered.TryGetValue(type, out list)) - { - foreach (var p in list) - { - yield return p; - } - } - - type = type.GetTypeInfo().BaseType; - } + return GetRegisteredProperties(GetType()); } /// diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 20bacdb4e9..9950ec9028 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -68,6 +68,11 @@ namespace Perspex Contract.Requires(valueType != null); Contract.Requires(ownerType != null); + if (name.Contains(".")) + { + throw new ArgumentException("'name' may not contain periods."); + } + Name = name; PropertyType = valueType; OwnerType = ownerType; diff --git a/src/Perspex.Styling/Styling/Selector.cs b/src/Perspex.Styling/Styling/Selector.cs index 1f9fb42784..7df3f77512 100644 --- a/src/Perspex.Styling/Styling/Selector.cs +++ b/src/Perspex.Styling/Styling/Selector.cs @@ -6,6 +6,34 @@ using System.Collections.Generic; namespace Perspex.Styling { + /// + /// A selector in a . + /// + /// + /// Selectors represented in markup using a CSS-like syntax, e.g. "Button < .dark" which + /// means "A child of a Button with the 'dark' class applied. The preceeding example would be + /// stored in 3 objects, linked by the property: + /// + /// + /// .dark + /// + /// A selector that selects a control with the 'dark' class applied. + /// + /// + /// + /// < + /// + /// A selector that selects a child of the previous selector. + /// + /// + /// + /// Button + /// + /// A selector that selects a Button type. + /// + /// + /// + /// public class Selector { private readonly Func _evaluate; diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs index 7b778414c0..f92fb12a66 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Metadata.cs @@ -15,22 +15,23 @@ namespace Perspex.Base.UnitTests PerspexProperty p; p = Class1.FooProperty; p = Class2.BarProperty; + p = AttachedOwner.AttachedProperty; } [Fact] - public void GetProperties_Returns_Registered_Properties() + public void GetRegisteredProperties_Returns_Registered_Properties() { - string[] names = PerspexObject.GetProperties(typeof(Class1)).Select(x => x.Name).ToArray(); + string[] names = PerspexObject.GetRegisteredProperties(typeof(Class1)).Select(x => x.Name).ToArray(); - Assert.Equal(new[] { "Foo", "Baz", "Qux" }, names); + Assert.Equal(new[] { "Foo", "Baz", "Qux", "Attached" }, names); } [Fact] - public void GetProperties_Returns_Registered_Properties_For_Base_Types() + public void GetRegisteredProperties_Returns_Registered_Properties_For_Base_Types() { - string[] names = PerspexObject.GetProperties(typeof(Class2)).Select(x => x.Name).ToArray(); + string[] names = PerspexObject.GetRegisteredProperties(typeof(Class2)).Select(x => x.Name).ToArray(); - Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux" }, names); + Assert.Equal(new[] { "Bar", "Flob", "Fred", "Foo", "Baz", "Qux", "Attached" }, names); } private class Class1 : PerspexObject @@ -56,5 +57,11 @@ namespace Perspex.Base.UnitTests public static readonly PerspexProperty FredProperty = PerspexProperty.Register("Fred"); } + + private class AttachedOwner + { + public static readonly PerspexProperty AttachedProperty = + PerspexProperty.RegisterAttached("Attached"); + } } } diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs index d8f2840597..9f4fa62aa1 100644 --- a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs +++ b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs @@ -25,6 +25,18 @@ namespace Perspex.Base.UnitTests Assert.Equal(false, target.Inherits); } + [Fact] + public void Name_Cannot_Contain_Periods() + { + Assert.Throws(() => new PerspexProperty( + "Foo.Bar", + typeof(Class1), + "Foo", + false, + BindingMode.OneWay, + null)); + } + [Fact] public void GetDefaultValue_Returns_Registered_Value() { From 0c21bef168c7172ef96adb07952b16894e4aaf67 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Sep 2015 21:42:14 +0200 Subject: [PATCH 05/11] Fix bug in error message. 'value' was getting overwritten with null. --- src/Perspex.Base/PerspexObject.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 2a0a34e350..83021a62d4 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -494,6 +494,7 @@ namespace Perspex Contract.Requires(property != null); PriorityValue v; + var originalValue = value; if (!IsRegistered(property)) { @@ -506,10 +507,10 @@ namespace Perspex if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) { throw new InvalidOperationException(string.Format( - "Invalid value for Property '{0}': {1} ({2})", + "Invalid value for Property '{0}': '{1}' ({2})", property.Name, - value, - value?.GetType().FullName ?? "(null)")); + originalValue, + originalValue?.GetType().FullName ?? "(null)")); } if (!_values.TryGetValue(property, out v)) From 285bd5f2996dcf36fb1f98f20843cb1a2ea5aace Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Sep 2015 21:42:46 +0200 Subject: [PATCH 06/11] Correctly handle setting Setter.Value. --- .../Context/PerspexXamlMemberValuePlugin.cs | 29 +++++++++---------- 1 file changed, 13 insertions(+), 16 deletions(-) diff --git a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs index d7e33c0fc8..d5d864dc47 100644 --- a/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs +++ b/src/Markup/Perspex.Markup.Xaml/Context/PerspexXamlMemberValuePlugin.cs @@ -4,9 +4,11 @@ using System; using System.Reactive.Linq; using Glass; +using OmniXaml.ObjectAssembler; using OmniXaml.Typing; using Perspex.Controls; using Perspex.Markup.Xaml.DataBinding; +using Perspex.Styling; namespace Perspex.Markup.Xaml.Context { @@ -23,30 +25,25 @@ namespace Perspex.Markup.Xaml.Context public override void SetValue(object instance, object value) { - if (ValueRequiresSpecialHandling(value)) + if (value is XamlBindingDefinition) { - HandleSpecialValue(instance, value); - } - else - { - base.SetValue(instance, value); - } - } - - private void HandleSpecialValue(object instance, object value) - { - var definition = value as XamlBindingDefinition; - if (definition != null) - { - HandleXamlBindingDefinition(definition); + HandleXamlBindingDefinition((XamlBindingDefinition)value); } else if (IsPerspexProperty) { HandlePerspexProperty(instance, value); } + else if (instance is Setter && _xamlMember.Name == "Value") + { + var setter = (Setter)instance; + var targetType = setter.Property.PropertyType; + var valuePipeline = new ValuePipeline(_xamlMember.TypeRepository); + var xamlType = _xamlMember.TypeRepository.GetXamlType(targetType); + base.SetValue(instance, valuePipeline.ConvertValueIfNecessary(value, xamlType)); + } else { - throw new InvalidOperationException($"Cannot handle the value {value} for member {this} and the instance {instance}"); + base.SetValue(instance, value); } } From 5a6cf2ca29c0addc0f4b3f284b3c912b3e1178ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Mon, 14 Sep 2015 21:51:06 +0200 Subject: [PATCH 07/11] Added ClassesConverter. --- .../XamlTestApplication/Views/MainWindow.xaml | 7 +++- .../Context/PerspexWiringContext.cs | 1 + .../Converters/ClassesConverter.cs | 33 +++++++++++++++++++ .../Perspex.Markup.Xaml.csproj | 1 + 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 src/Markup/Perspex.Markup.Xaml/Converters/ClassesConverter.cs diff --git a/samples/XamlTestApplication/Views/MainWindow.xaml b/samples/XamlTestApplication/Views/MainWindow.xaml index da27d04242..55736e00ca 100644 --- a/samples/XamlTestApplication/Views/MainWindow.xaml +++ b/samples/XamlTestApplication/Views/MainWindow.xaml @@ -6,10 +6,15 @@ + + +