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() {