diff --git a/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs b/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs index 4a4ec5002a..3b08b06812 100644 --- a/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs +++ b/src/Gtk/Perspex.Cairo/Media/FormattedTextImpl.cs @@ -24,8 +24,8 @@ namespace Perspex.Cairo.Media TextAlignment textAlignment, FontWeight fontWeight) { - Contract.Requires(context != null); - Contract.Requires (text != null); + Contract.Requires(context != null); + Contract.Requires (text != null); Layout = new Pango.Layout(context); _text = text; Layout.SetText(text); diff --git a/src/Perspex.Base/BindingPriority.cs b/src/Perspex.Base/BindingPriority.cs new file mode 100644 index 0000000000..e26898d202 --- /dev/null +++ b/src/Perspex.Base/BindingPriority.cs @@ -0,0 +1,47 @@ +// 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. + +namespace Perspex +{ + /// + /// The priority of a binding. + /// + public enum BindingPriority + { + /// + /// A value that comes from an animation. + /// + Animation = -1, + + /// + /// A local value. + /// + LocalValue = 0, + + /// + /// A triggered style binding. + /// + /// + /// A style trigger is a selector such as .class which overrides a + /// binding. In this way, a basic control can have + /// for example a Background from the templated parent which changes when the + /// control has the :pointerover class. + /// + StyleTrigger, + + /// + /// A binding to a property on the templated parent. + /// + TemplatedParent, + + /// + /// A style binding. + /// + Style, + + /// + /// The binding is uninitialized. + /// + Unset = int.MaxValue, + } +} \ No newline at end of file diff --git a/src/Perspex.Base/Perspex.Base.csproj b/src/Perspex.Base/Perspex.Base.csproj index 258afdc1b4..abe7582049 100644 --- a/src/Perspex.Base/Perspex.Base.csproj +++ b/src/Perspex.Base/Perspex.Base.csproj @@ -51,6 +51,7 @@ + diff --git a/src/Perspex.Base/PerspexObject.cs b/src/Perspex.Base/PerspexObject.cs index 8a1d05fcb7..7a0e211f2c 100644 --- a/src/Perspex.Base/PerspexObject.cs +++ b/src/Perspex.Base/PerspexObject.cs @@ -15,48 +15,6 @@ using Serilog.Core.Enrichers; namespace Perspex { - /// - /// The priority of a binding. - /// - public enum BindingPriority - { - /// - /// A value that comes from an animation. - /// - Animation = -1, - - /// - /// A local value. - /// - LocalValue = 0, - - /// - /// A triggered style binding. - /// - /// - /// A style trigger is a selector such as .class which overrides a - /// binding. In this way, a basic control can have - /// for example a Background from the templated parent which changes when the - /// control has the :pointerover class. - /// - StyleTrigger, - - /// - /// A binding to a property on the templated parent. - /// - TemplatedParent, - - /// - /// A style binding. - /// - Style, - - /// - /// The binding is uninitialized. - /// - Unset = int.MaxValue, - } - /// /// An object with support. /// @@ -112,11 +70,15 @@ namespace Perspex foreach (var property in GetRegisteredProperties()) { + object value = property.IsDirect ? + property.Getter(this) : + property.GetDefaultValue(GetType()); + var e = new PerspexPropertyChangedEventArgs( this, property, PerspexProperty.UnsetValue, - property.GetDefaultValue(GetType()), + value, BindingPriority.Unset); property.NotifyInitialized(e); @@ -222,7 +184,7 @@ namespace Perspex binding.Mode; var sourceBinding = value as BindingDescriptor; - if (sourceBinding == null && mode != BindingMode.OneWay) + if (sourceBinding == null && mode > BindingMode.OneWay) { throw new InvalidOperationException("Can only bind OneWay to plain IObservable."); } @@ -253,7 +215,7 @@ namespace Perspex /// A collection of definitions. public static IEnumerable GetRegisteredProperties(Type type) { - Contract.Requires(type != null); + Contract.Requires(type != null); TypeInfo i = type.GetTypeInfo(); @@ -301,8 +263,8 @@ namespace Perspex /// public static void Register(Type type, PerspexProperty property) { - Contract.Requires(type != null); - Contract.Requires(property != null); + Contract.Requires(type != null); + Contract.Requires(property != null); List list; @@ -338,7 +300,7 @@ namespace Perspex /// The property. public void ClearValue(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); SetValue(property, PerspexProperty.UnsetValue); } @@ -350,7 +312,7 @@ namespace Perspex /// An observable. public IObservable GetObservable(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); return new PerspexObservable( observer => @@ -383,7 +345,7 @@ namespace Perspex /// An observable. public IObservable GetObservable(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); return GetObservable((PerspexProperty)property).Cast(); } @@ -425,27 +387,34 @@ namespace Perspex /// The value. public object GetValue(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); - object result; - - PriorityValue value; - - if (_values.TryGetValue(property, out value)) + if (property.IsDirect) { - result = value.Value; + return GetRegistered(property).Getter(this); } else { - result = PerspexProperty.UnsetValue; - } + object result = PerspexProperty.UnsetValue; + PriorityValue value; - if (result == PerspexProperty.UnsetValue) - { - result = GetDefaultValue(property); - } + if (!IsRegistered(property)) + { + ThrowNotRegistered(property); + } - return result; + if (_values.TryGetValue(property, out value)) + { + result = value.Value; + } + + if (result == PerspexProperty.UnsetValue) + { + result = GetDefaultValue(property); + } + + return result; + } } /// @@ -456,9 +425,16 @@ namespace Perspex /// The value. public T GetValue(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); - return (T)GetValue((PerspexProperty)property); + if (property.IsDirect) + { + return ((PerspexProperty)GetRegistered(property)).Getter(this); + } + else + { + return (T)GetValue((PerspexProperty)property); + } } /// @@ -479,7 +455,7 @@ namespace Perspex /// True if the property is set, otherwise false. public bool IsSet(PerspexProperty property) { - Contract.Requires(property != null); + Contract.Requires(property != null); return _values.ContainsKey(property); } @@ -491,24 +467,7 @@ namespace Perspex /// True if the property is registered, otherwise false. public bool IsRegistered(PerspexProperty property) { - Type type = GetType(); - - while (type != null) - { - List list; - - if (s_registered.TryGetValue(type, out list)) - { - if (list.Contains(property)) - { - return true; - } - } - - type = type.GetTypeInfo().BaseType; - } - - return false; + return FindRegistered(property) != null; } /// @@ -522,37 +481,50 @@ namespace Perspex object value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - - PriorityValue v; - var originalValue = value; + Contract.Requires(property != null); - if (!IsRegistered(property)) + if (property.IsDirect) { - throw new InvalidOperationException(string.Format( - "Property '{0}' not registered on '{1}'", - property.Name, - GetType())); - } + property = GetRegistered(property); - if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) - { - throw new InvalidOperationException(string.Format( - "Invalid value for Property '{0}': '{1}' ({2})", - property.Name, - originalValue, - originalValue?.GetType().FullName ?? "(null)")); - } + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } - if (!_values.TryGetValue(property, out v)) + property.Setter(this, value); + } + else { - if (value == PerspexProperty.UnsetValue) + PriorityValue v; + var originalValue = value; + + if (!IsRegistered(property)) + { + ThrowNotRegistered(property); + } + + if (!TypeUtilities.TryCast(property.PropertyType, value, out value)) { - return; + throw new ArgumentException(string.Format( + "Invalid value for Property '{0}': '{1}' ({2})", + property.Name, + originalValue, + originalValue?.GetType().FullName ?? "(null)")); } - v = CreatePriorityValue(property); - _values.Add(property, v); + if (!_values.TryGetValue(property, out v)) + { + if (value == PerspexProperty.UnsetValue) + { + return; + } + + v = CreatePriorityValue(property); + _values.Add(property, v); + } + + v.SetValue(value, (int)priority); } _propertyLog.Verbose( @@ -560,7 +532,6 @@ namespace Perspex property, value, priority); - v.SetDirectValue(value, (int)priority); } /// @@ -575,9 +546,23 @@ namespace Perspex T value, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + Contract.Requires(property != null); + + if (property.IsDirect) + { + property = (PerspexProperty)GetRegistered(property); + + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } - SetValue((PerspexProperty)property, value, priority); + property.Setter(this, value); + } + else + { + SetValue((PerspexProperty)property, value, priority); + } } /// @@ -594,32 +579,48 @@ namespace Perspex IObservable source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); - - PriorityValue v; - IDescription description = source as IDescription; + Contract.Requires(property != null); - if (!IsRegistered(property)) + if (property.IsDirect) { - throw new InvalidOperationException(string.Format( - "Property '{0}' not registered on '{1}'", - property.Name, - GetType())); - } + property = GetRegistered(property); - if (!_values.TryGetValue(property, out v)) - { - v = CreatePriorityValue(property); - _values.Add(property, v); + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + _propertyLog.Verbose( + "Bound {Property} to {Binding} with priority LocalValue", + property, + source); + + return source.Subscribe(x => SetValue(property, x)); } + else + { + PriorityValue v; + IDescription description = source as IDescription; - _propertyLog.Verbose( - "Bound {Property} to {Binding} with priority {Priority}", - property, - source, - priority); + if (!IsRegistered(property)) + { + ThrowNotRegistered(property); + } + + if (!_values.TryGetValue(property, out v)) + { + v = CreatePriorityValue(property); + _values.Add(property, v); + } - return v.Add(source, (int)priority); + _propertyLog.Verbose( + "Bound {Property} to {Binding} with priority {Priority}", + property, + source, + priority); + + return v.Add(source, (int)priority); + } } /// @@ -637,9 +638,23 @@ namespace Perspex IObservable source, BindingPriority priority = BindingPriority.LocalValue) { - Contract.Requires(property != null); + Contract.Requires(property != null); - return Bind((PerspexProperty)property, source.Select(x => (object)x), priority); + if (property.IsDirect) + { + property = (PerspexProperty)GetRegistered(property); + + if (property.Setter == null) + { + throw new ArgumentException($"The property {property.Name} is readonly."); + } + + return source.Subscribe(x => SetValue(property, x)); + } + else + { + return Bind((PerspexProperty)property, source.Select(x => (object)x), priority); + } } /// @@ -713,6 +728,69 @@ namespace Perspex { } + /// + /// Raises the event. + /// + /// The property that has changed. + /// The old property value. + /// The new property value. + /// The priority of the binding that produced the value. + protected void RaisePropertyChanged( + PerspexProperty property, + object oldValue, + object newValue, + BindingPriority priority) + { + Contract.Requires(property != null); + + PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( + this, + property, + oldValue, + newValue, + priority); + + OnPropertyChanged(e); + property.NotifyChanged(e); + + if (PropertyChanged != null) + { + PropertyChanged(this, e); + } + + if (_inpcChanged != null) + { + PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); + _inpcChanged(this, e2); + } + } + + /// + /// Sets the backing field for a direct perspex property, raising the + /// event if the value has changed. + /// + /// The type of the property. + /// The property. + /// The backing field. + /// The value. + /// + /// True if the value changed, otherwise false. + /// + protected bool SetAndRaise(PerspexProperty property, ref T field, T value) + { + if (!object.Equals(field, value)) + { + var old = field; + field = value; + RaisePropertyChanged(property, old, value, BindingPriority.LocalValue); + return true; + } + else + { + return false; + } + } + /// /// Creates a for a . /// @@ -772,6 +850,59 @@ namespace Perspex } } + /// + /// Given a returns a registered perspex property that is + /// equal. + /// + /// The property. + /// The registered property or null if not found. + /// + /// Calling AddOwner on a direct PerspexProperty creates new new PerspexProperty with + /// an overridden getter and setter. This property is a different object but is equal + /// according to . + /// + public PerspexProperty FindRegistered(PerspexProperty property) + { + Type type = GetType(); + + while (type != null) + { + List list; + + if (s_registered.TryGetValue(type, out list)) + { + var index = list.IndexOf(property); + + if (index != -1) + { + return list[index]; + } + } + + type = type.GetTypeInfo().BaseType; + } + + return null; + } + + /// + /// Given a returns a registered perspex property that is + /// equal or throws if not found. + /// + /// The property. + /// The registered property. + public PerspexProperty GetRegistered(PerspexProperty property) + { + var result = FindRegistered(property); + + if (result == null) + { + ThrowNotRegistered(property); + } + + return result; + } + /// /// Called when a property is changed on the current . /// @@ -801,40 +932,13 @@ namespace Perspex } /// - /// Raises the event. + /// Throws an exception indicating that the specified property is not registered on this + /// object. /// - /// The property that has changed. - /// The old property value. - /// The new property value. - /// The priority of the binding that produced the value. - private void RaisePropertyChanged( - PerspexProperty property, - object oldValue, - object newValue, - BindingPriority priority) + /// The property + private void ThrowNotRegistered(PerspexProperty p) { - Contract.Requires(property != null); - - PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs( - this, - property, - oldValue, - newValue, - priority); - - OnPropertyChanged(e); - property.NotifyChanged(e); - - if (PropertyChanged != null) - { - PropertyChanged(this, e); - } - - if (_inpcChanged != null) - { - PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name); - _inpcChanged(this, e2); - } + throw new ArgumentException($"Property '{p.Name} not registered on '{this.GetType()}"); } } } diff --git a/src/Perspex.Base/PerspexProperty.cs b/src/Perspex.Base/PerspexProperty.cs index 9950ec9028..3bda662038 100644 --- a/src/Perspex.Base/PerspexProperty.cs +++ b/src/Perspex.Base/PerspexProperty.cs @@ -15,13 +15,18 @@ namespace Perspex /// /// This class is analogous to DependencyProperty in WPF. /// - public class PerspexProperty + public class PerspexProperty : IEquatable { /// /// Represents an unset property value. /// public static readonly object UnsetValue = new Unset(); + /// + /// Gets the next ID that will be allocated to a property. + /// + private static int s_nextId = 1; + /// /// The default values for the property, by type. /// @@ -43,6 +48,11 @@ namespace Perspex private readonly Dictionary> _validation = new Dictionary>(); + /// + /// Gets the ID of the property. + /// + private int _id; + /// /// Initializes a new instance of the class. /// @@ -64,9 +74,9 @@ namespace Perspex Func validate = null, bool isAttached = false) { - Contract.Requires(name != null); - Contract.Requires(valueType != null); - Contract.Requires(ownerType != null); + Contract.Requires(name != null); + Contract.Requires(valueType != null); + Contract.Requires(ownerType != null); if (name.Contains(".")) { @@ -80,6 +90,7 @@ namespace Perspex Inherits = inherits; DefaultBindingMode = defaultBindingMode; IsAttached = isAttached; + _id = s_nextId++; if (validate != null) { @@ -87,6 +98,69 @@ namespace Perspex } } + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The type of the property's value. + /// The type of the class that registers the property. + /// Gets the current value of the property. + /// Sets the value of the property. + public PerspexProperty( + string name, + Type valueType, + Type ownerType, + Func getter, + Action setter) + { + Contract.Requires(name != null); + Contract.Requires(valueType != null); + Contract.Requires(ownerType != null); + Contract.Requires(getter != null); + + if (name.Contains(".")) + { + throw new ArgumentException("'name' may not contain periods."); + } + + Name = name; + PropertyType = valueType; + OwnerType = ownerType; + Getter = getter; + Setter = setter; + IsDirect = true; + _id = s_nextId++; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// A new getter. + /// A new setter. + protected PerspexProperty( + PerspexProperty source, + Func getter, + Action setter) + { + Contract.Requires(source != null); + Contract.Requires(getter != null); + + if (!source.IsDirect) + { + throw new InvalidOperationException( + "This method can only be called on direct PerspexProperties."); + } + + Name = source.Name; + PropertyType = source.PropertyType; + OwnerType = source.OwnerType; + Getter = getter; + Setter = setter; + IsDirect = true; + _id = source._id; + } + /// /// Gets the name of the property. /// @@ -135,6 +209,11 @@ namespace Perspex /// public bool IsAttached { get; } + /// + /// Gets a value indicating whether this is a direct property. + /// + public bool IsDirect { get; } + /// /// Gets an observable that is fired when this property is initialized on a /// new instance. @@ -191,6 +270,49 @@ namespace Perspex }; } + /// + /// Gets the getter function for direct properties. + /// + internal Func Getter { get; } + + /// + /// Gets the etter function for direct properties. + /// + internal Action Setter { get; } + + /// + /// Tests two s for equality. + /// + /// The first property. + /// The second property. + /// True if the properties are equal, otherwise false. + public static bool operator ==(PerspexProperty a, PerspexProperty b) + { + if (object.ReferenceEquals(a, b)) + { + return true; + } + else if (((object)a == null) || ((object)b == null)) + { + return false; + } + else + { + return a.Equals(b); + } + } + + /// + /// Tests two s for unequality. + /// + /// The first property. + /// The second property. + /// True if the properties are equal, otherwise false. + public static bool operator !=(PerspexProperty a, PerspexProperty b) + { + return !(a == b); + } + /// /// Registers a . /// @@ -210,7 +332,7 @@ namespace Perspex Func validate = null) where TOwner : PerspexObject { - Contract.Requires(name != null); + Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, @@ -226,6 +348,34 @@ namespace Perspex return result; } + /// + /// Registers a direct . + /// + /// The type of the class that is registering the property. + /// The type of the property's value. + /// The name of the property. + /// Gets the current value of the property. + /// Sets the value of the property. + /// A + public static PerspexProperty RegisterDirect( + string name, + Func getter, + Action setter = null) + where TOwner : PerspexObject + { + Contract.Requires(name != null); + + PerspexProperty result = new PerspexProperty( + name, + typeof(TOwner), + Cast(getter), + Cast(setter)); + + PerspexObject.Register(typeof(TOwner), result); + + return result; + } + /// /// Registers an attached . /// @@ -245,7 +395,7 @@ namespace Perspex BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null) { - Contract.Requires(name != null); + Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, @@ -281,7 +431,7 @@ namespace Perspex BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null) { - Contract.Requires(name != null); + Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, @@ -297,6 +447,25 @@ namespace Perspex return result; } + /// + public override bool Equals(object obj) + { + var p = obj as PerspexProperty; + return p != null ? Equals(p) : false; + } + + /// + public bool Equals(PerspexProperty other) + { + return other != null && _id == other._id; + } + + /// + public override int GetHashCode() + { + return _id; + } + /// /// Returns a binding accessor that can be passed to 's [] /// operator to initiate a binding. @@ -320,7 +489,7 @@ namespace Perspex /// The default value. public object GetDefaultValue(Type type) { - Contract.Requires(type != null); + Contract.Requires(type != null); while (type != null) { @@ -346,7 +515,7 @@ namespace Perspex /// public Func GetValidationFunc(Type type) { - Contract.Requires(type != null); + Contract.Requires(type != null); while (type != null) { @@ -390,7 +559,7 @@ namespace Perspex /// The default value. public void OverrideDefaultValue(Type type, object defaultValue) { - Contract.Requires(type != null); + Contract.Requires(type != null); if (!TypeUtilities.TryCast(PropertyType, defaultValue, out defaultValue)) { @@ -416,7 +585,7 @@ namespace Perspex /// The validation function. public void OverrideValidation(Type type, Func validation) { - Contract.Requires(type != null); + Contract.Requires(type != null); if (_validation.ContainsKey(type)) { @@ -453,9 +622,37 @@ namespace Perspex _changed.OnNext(e); } + /// + /// Casts a getter function accepting a typed owner to one accepting a + /// . + /// + /// The owner type. + /// The property value type. + /// The typed function. + /// The untyped function. + private static Func Cast(Func f) + where TOwner : PerspexObject + { + return (f != null) ? o => f((TOwner)o) : (Func)null; + } + + /// + /// Casts a setter action accepting a typed owner to one accepting a + /// . + /// + /// The owner type. + /// The property value type. + /// The typed action. + /// The untyped action. + private static Action Cast(Action f) + where TOwner : PerspexObject + { + return f != null ? (o, v) => f((TOwner)o, v) : (Action)null; + } + /// /// Casts a validation function accepting a typed owner to one accepting a - /// . + /// . /// /// The owner type. /// The property value type. diff --git a/src/Perspex.Base/PerspexProperty`1.cs b/src/Perspex.Base/PerspexProperty`1.cs index 14fd84f528..3c81b5e91b 100644 --- a/src/Perspex.Base/PerspexProperty`1.cs +++ b/src/Perspex.Base/PerspexProperty`1.cs @@ -36,24 +36,87 @@ namespace Perspex defaultValue, inherits, defaultBindingMode, - Convert(validate), + Cast(validate), isAttached) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); } + /// + /// Initializes a new instance of the class. + /// + /// The name of the property. + /// The type of the class that registers the property. + /// Gets the current value of the property. + /// Sets the value of the property. + public PerspexProperty( + string name, + Type ownerType, + Func getter, + Action setter) + : base(name, typeof(TValue), ownerType, CastParamReturn(getter), CastParams(setter)) + { + Getter = getter; + Setter = setter; + } + + /// + /// Initializes a new instance of the class. + /// + /// The direct property to copy. + /// A new getter. + /// A new setter. + private PerspexProperty( + PerspexProperty source, + Func getter, + Action setter) + : base(source, CastParamReturn(getter), CastParams(setter)) + { + Getter = getter; + Setter = setter; + } + + /// + /// Gets the getter function for direct properties. + /// + internal new Func Getter { get; } + + /// + /// Gets the etter function for direct properties. + /// + internal new Action Setter { get; } + /// /// Registers the property on another type. /// /// The type of the additional owner. /// The property. - public PerspexProperty AddOwner() + public PerspexProperty AddOwner() where TOwner : PerspexObject { + if (IsDirect) + { + throw new InvalidOperationException( + "You must provide a new getter and setter when calling AddOwner on a direct PerspexProperty."); + } + PerspexObject.Register(typeof(TOwner), this); return this; } + /// + /// Registers the direct property on another type. + /// + /// The type of the additional owner. + /// The property. + public PerspexProperty AddOwner( + Func getter, + Action setter = null) + where TOwner : PerspexObject + { + var result = new PerspexProperty(this, CastReturn(getter), CastParam1(setter)); + PerspexObject.Register(typeof(TOwner), result); + return result; + } + /// /// Gets the default value for the property on the specified type. /// @@ -78,11 +141,59 @@ namespace Perspex } /// - /// Converts from a typed validation function to an untyped. + /// Casts a typed getter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Func CastParamReturn(Func f) + where TOwner : PerspexObject + { + return (f != null) ? o => f((TOwner)o) : (Func)null; + } + + /// + /// Casts a typed getter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Func CastReturn(Func f) + where TOwner : PerspexObject + { + return (f != null) ? o => f((TOwner)o) : (Func)null; + } + + /// + /// Casts a typed setter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Action CastParams(Action f) + where TOwner : PerspexObject + { + return (f != null) ? (o, v) => f((TOwner)o, (TValue)v) : (Action)null; + } + + /// + /// Casts a typed setter function to an untyped. + /// + /// The owner type. + /// The typed function. + /// The untyped function. + private static Action CastParam1(Action f) + where TOwner : PerspexObject + { + return (f != null) ? (o, v) => f((TOwner)o, v) : (Action)null; + } + + /// + /// Casts a typed validation function to an untyped. /// /// The typed validation function. /// The untyped validation function. - private static Func Convert(Func f) + private static Func Cast(Func f) { return f != null ? (o, v) => f(o, (TValue)v) : (Func)null; } diff --git a/src/Perspex.Base/PriorityValue.cs b/src/Perspex.Base/PriorityValue.cs index 4be6b88861..53ecd8da99 100644 --- a/src/Perspex.Base/PriorityValue.cs +++ b/src/Perspex.Base/PriorityValue.cs @@ -105,11 +105,11 @@ namespace Perspex } /// - /// Sets the direct value for a specified priority. + /// Sets the value for a specified priority. /// /// The value. /// The priority - public void SetDirectValue(object value, int priority) + public void SetValue(object value, int priority) { GetLevel(priority).DirectValue = value; } diff --git a/src/Perspex.Base/Utilities/TypeUtilities.cs b/src/Perspex.Base/Utilities/TypeUtilities.cs index 81bd5ad250..98909d9801 100644 --- a/src/Perspex.Base/Utilities/TypeUtilities.cs +++ b/src/Perspex.Base/Utilities/TypeUtilities.cs @@ -35,7 +35,7 @@ namespace Perspex.Utilities /// True if the cast was sucessful, otherwise false. public static bool TryCast(Type to, object value, out object result) { - Contract.Requires(to != null); + Contract.Requires(to != null); if (value == null) { diff --git a/src/Perspex.Controls/Control.cs b/src/Perspex.Controls/Control.cs index 901b744451..81015e196b 100644 --- a/src/Perspex.Controls/Control.cs +++ b/src/Perspex.Controls/Control.cs @@ -2,14 +2,12 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; -using System.Linq; using System.Reactive.Linq; using Perspex.Collections; using Perspex.Controls.Primitives; using Perspex.Controls.Templates; using Perspex.Input; using Perspex.Interactivity; -using Perspex.LogicalTree; using Perspex.Rendering; using Perspex.Styling; @@ -46,7 +44,7 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty ParentProperty = - PerspexProperty.Register(nameof(Parent)); + PerspexProperty.RegisterDirect(nameof(Parent), o => o.Parent); /// /// Defines the property. @@ -66,16 +64,12 @@ namespace Perspex.Controls public static readonly RoutedEvent RequestBringIntoViewEvent = RoutedEvent.Register("RequestBringIntoView", RoutingStrategies.Bubble); + private IControl _parent; private readonly Classes _classes = new Classes(); - private DataTemplates _dataTemplates; - private IControl _focusAdorner; - private string _id; - private IPerspexList _logicalChildren; - private Styles _styles; /// @@ -227,7 +221,7 @@ namespace Perspex.Controls /// /// Gets the control's logical parent. /// - public IControl Parent => GetValue(ParentProperty); + public IControl Parent => _parent; /// /// Gets or sets a user-defined object attached to the control. @@ -297,7 +291,7 @@ namespace Perspex.Controls throw new InvalidOperationException("The Control already has a parent."); } - SetValue(ParentProperty, parent); + SetAndRaise(ParentProperty, ref _parent, (IControl)parent); } /// diff --git a/src/Perspex.Controls/DropDown.cs b/src/Perspex.Controls/DropDown.cs index eb78e13cbc..c2ac601bc3 100644 --- a/src/Perspex.Controls/DropDown.cs +++ b/src/Perspex.Controls/DropDown.cs @@ -26,11 +26,15 @@ namespace Perspex.Controls ContentControl.VerticalContentAlignmentProperty.AddOwner(); public static readonly PerspexProperty IsDropDownOpenProperty = - PerspexProperty.Register("IsDropDownOpen"); + PerspexProperty.RegisterDirect( + nameof(IsDropDownOpen), + o => o.IsDropDownOpen, + (o, v) => o.IsDropDownOpen = v); public static readonly PerspexProperty SelectionBoxItemProperty = PerspexProperty.Register("SelectionBoxItem"); + private bool _isDropDownOpen; private Popup _popup; static DropDown() @@ -64,8 +68,8 @@ namespace Perspex.Controls public bool IsDropDownOpen { - get { return GetValue(IsDropDownOpenProperty); } - set { SetValue(IsDropDownOpenProperty, value); } + get { return _isDropDownOpen; } + set { SetAndRaise(IsDropDownOpenProperty, ref _isDropDownOpen, value); } } public object SelectionBoxItem diff --git a/src/Perspex.Controls/ItemsControl.cs b/src/Perspex.Controls/ItemsControl.cs index c1e303147c..6649b43cb0 100644 --- a/src/Perspex.Controls/ItemsControl.cs +++ b/src/Perspex.Controls/ItemsControl.cs @@ -33,7 +33,7 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty ItemsProperty = - PerspexProperty.Register(nameof(Items)); + PerspexProperty.RegisterDirect(nameof(Items), o => o.Items, (o, v) => o.Items = v); /// /// Defines the property. @@ -47,6 +47,7 @@ namespace Perspex.Controls public static readonly PerspexProperty MemberSelectorProperty = PerspexProperty.Register(nameof(MemberSelector)); + private IEnumerable _items = new PerspexList(); private IItemContainerGenerator _itemContainerGenerator; /// @@ -63,7 +64,7 @@ namespace Perspex.Controls public ItemsControl() { Classes.Add(":empty"); - Items = new PerspexList(); + SubscribeToItems(_items); } /// @@ -87,8 +88,8 @@ namespace Perspex.Controls /// public IEnumerable Items { - get { return GetValue(ItemsProperty); } - set { SetValue(ItemsProperty, value); } + get { return _items; } + set { SetAndRaise(ItemsProperty, ref _items, value); } } /// @@ -161,26 +162,11 @@ namespace Perspex.Controls if (incc != null) { - incc.CollectionChanged += ItemsCollectionChanged; + incc.CollectionChanged -= ItemsCollectionChanged; } var newValue = e.NewValue as IEnumerable; - - if (newValue == null || newValue.Count() == 0) - { - Classes.Add(":empty"); - } - else - { - Classes.Remove(":empty"); - } - - incc = newValue as INotifyCollectionChanged; - - if (incc != null) - { - incc.CollectionChanged += ItemsCollectionChanged; - } + SubscribeToItems(newValue); } /// @@ -202,5 +188,28 @@ namespace Perspex.Controls Classes.Remove(":empty"); } } + + /// + /// Subscribes to an collection. + /// + /// + private void SubscribeToItems(IEnumerable items) + { + if (items == null || items.Count() == 0) + { + Classes.Add(":empty"); + } + else + { + Classes.Remove(":empty"); + } + + var incc = items as INotifyCollectionChanged; + + if (incc != null) + { + incc.CollectionChanged += ItemsCollectionChanged; + } + } } } diff --git a/src/Perspex.Controls/Menu.cs b/src/Perspex.Controls/Menu.cs index db424ddcf8..3513843c37 100644 --- a/src/Perspex.Controls/Menu.cs +++ b/src/Perspex.Controls/Menu.cs @@ -28,7 +28,11 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty IsOpenProperty = - PerspexProperty.Register(nameof(IsOpen)); + PerspexProperty.RegisterDirect( + nameof(IsOpen), + o => o.IsOpen); + + private bool _isOpen; /// /// Tracks event handlers added to the root of the visual tree. @@ -50,8 +54,8 @@ namespace Perspex.Controls /// public bool IsOpen { - get { return GetValue(IsOpenProperty); } - private set { SetValue(IsOpenProperty, value); } + get { return _isOpen; } + private set { SetAndRaise(IsOpenProperty, ref _isOpen, value); } } /// diff --git a/src/Perspex.Controls/Presenters/ContentPresenter.cs b/src/Perspex.Controls/Presenters/ContentPresenter.cs index 6425d51294..492279e25f 100644 --- a/src/Perspex.Controls/Presenters/ContentPresenter.cs +++ b/src/Perspex.Controls/Presenters/ContentPresenter.cs @@ -15,7 +15,9 @@ namespace Perspex.Controls.Presenters /// Defines the property. /// public static readonly PerspexProperty ChildProperty = - PerspexProperty.Register("Child"); + PerspexProperty.RegisterDirect( + nameof(Child), + o => o.Child); /// /// Defines the property. @@ -23,6 +25,7 @@ namespace Perspex.Controls.Presenters public static readonly PerspexProperty ContentProperty = ContentControl.ContentProperty.AddOwner(); + private IControl _child; private bool _createdChild; /// @@ -38,8 +41,8 @@ namespace Perspex.Controls.Presenters /// public IControl Child { - get { return GetValue(ChildProperty); } - private set { SetValue(ChildProperty, value); } + get { return _child; } + private set { SetAndRaise(ChildProperty, ref _child, value); } } /// @@ -101,8 +104,12 @@ namespace Perspex.Controls.Presenters var logicalHost = this.FindReparentingHost(); var logicalChildren = logicalHost?.LogicalChildren ?? LogicalChildren; - logicalChildren.Remove(old); - ClearVisualChildren(); + if (old != null) + { + logicalChildren.Remove(old); + ((ISetLogicalParent)old).SetParent(null); + ClearVisualChildren(); + } Child = result; diff --git a/src/Perspex.Controls/Presenters/DeckPresenter.cs b/src/Perspex.Controls/Presenters/DeckPresenter.cs index 526f37e7b9..3a4c844d2a 100644 --- a/src/Perspex.Controls/Presenters/DeckPresenter.cs +++ b/src/Perspex.Controls/Presenters/DeckPresenter.cs @@ -23,7 +23,7 @@ namespace Perspex.Controls.Presenters /// Defines the property. /// public static readonly PerspexProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(); + ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); /// /// Defines the property. @@ -41,7 +41,9 @@ namespace Perspex.Controls.Presenters /// Defines the property. /// public static readonly PerspexProperty SelectedIndexProperty = - SelectingItemsControl.SelectedIndexProperty.AddOwner(); + SelectingItemsControl.SelectedIndexProperty.AddOwner( + o => o.SelectedIndex, + (o, v) => o.SelectedIndex = v); /// /// Defines the property. @@ -49,8 +51,9 @@ namespace Perspex.Controls.Presenters public static readonly PerspexProperty TransitionProperty = Deck.TransitionProperty.AddOwner(); + private IEnumerable _items; + private int _selectedIndex = -1; private bool _createdPanel; - private IItemContainerGenerator _generator; /// @@ -94,8 +97,8 @@ namespace Perspex.Controls.Presenters /// public IEnumerable Items { - get { return GetValue(ItemsProperty); } - set { SetValue(ItemsProperty, value); } + get { return _items; } + set { SetAndRaise(ItemsProperty, ref _items, value); } } /// @@ -121,8 +124,8 @@ namespace Perspex.Controls.Presenters /// public int SelectedIndex { - get { return GetValue(SelectedIndexProperty); } - set { SetValue(SelectedIndexProperty, value); } + get { return _selectedIndex; } + set { SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); } } /// @@ -204,7 +207,7 @@ namespace Perspex.Controls.Presenters } } - if (Transition != null) + if (Transition != null && (from != null || to != null)) { await Transition.Start((Visual)from, (Visual)to, fromIndex < toIndex); } diff --git a/src/Perspex.Controls/Presenters/ItemsPresenter.cs b/src/Perspex.Controls/Presenters/ItemsPresenter.cs index 15f73f6822..5c52f70df4 100644 --- a/src/Perspex.Controls/Presenters/ItemsPresenter.cs +++ b/src/Perspex.Controls/Presenters/ItemsPresenter.cs @@ -20,7 +20,7 @@ namespace Perspex.Controls.Presenters /// Defines the property. /// public static readonly PerspexProperty ItemsProperty = - ItemsControl.ItemsProperty.AddOwner(); + ItemsControl.ItemsProperty.AddOwner(o => o.Items, (o, v) => o.Items = v); /// /// Defines the property. @@ -34,8 +34,8 @@ namespace Perspex.Controls.Presenters public static readonly PerspexProperty MemberSelectorProperty = ItemsControl.MemberSelectorProperty.AddOwner(); + private IEnumerable _items; private bool _createdPanel; - private IItemContainerGenerator _generator; /// @@ -89,8 +89,8 @@ namespace Perspex.Controls.Presenters /// public IEnumerable Items { - get { return GetValue(ItemsProperty); } - set { SetValue(ItemsProperty, value); } + get { return _items; } + set { SetAndRaise(ItemsProperty, ref _items, value); } } /// diff --git a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs index 811cc7bb3e..139b861dc1 100644 --- a/src/Perspex.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Perspex.Controls/Primitives/SelectingItemsControl.cs @@ -31,18 +31,19 @@ namespace Perspex.Controls.Primitives /// Defines the property. /// public static readonly PerspexProperty SelectedIndexProperty = - PerspexProperty.Register( + PerspexProperty.RegisterDirect( nameof(SelectedIndex), - defaultValue: -1, - validate: ValidateSelectedIndex); + o => o.SelectedIndex, + (o, v) => o.SelectedIndex = v); /// /// Defines the property. /// public static readonly PerspexProperty SelectedItemProperty = - PerspexProperty.Register( + PerspexProperty.RegisterDirect( nameof(SelectedItem), - validate: ValidateSelectedItem); + o => o.SelectedItem, + (o, v) => o.SelectedItem = v); /// /// Event that should be raised by items that implement to @@ -52,6 +53,9 @@ namespace Perspex.Controls.Primitives public static readonly RoutedEvent IsSelectedChangedEvent = RoutedEvent.Register("IsSelectedChanged", RoutingStrategies.Bubble); + private int _selectedIndex = -1; + private object _selectedItem; + /// /// Initializes static members of the class. /// @@ -85,8 +89,16 @@ namespace Perspex.Controls.Primitives /// public int SelectedIndex { - get { return GetValue(SelectedIndexProperty); } - set { SetValue(SelectedIndexProperty, value); } + get + { + return _selectedIndex; + } + + set + { + value = (value >= 0 && value < Items?.Cast().Count()) ? value : -1; + SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value); + } } /// @@ -94,8 +106,16 @@ namespace Perspex.Controls.Primitives /// public object SelectedItem { - get { return GetValue(SelectedItemProperty); } - set { SetValue(SelectedItemProperty, value); } + get + { + return _selectedItem; + } + + set + { + value = Items?.Cast().Contains(value) == true ? value : null; + SetAndRaise(SelectedItemProperty, ref _selectedItem, value); + } } /// @@ -234,30 +254,6 @@ namespace Perspex.Controls.Primitives } } - /// - /// Coerces the property. - /// - /// The object with the property. - /// The proposed value of the property. - /// The final value of the property. - private static int ValidateSelectedIndex(SelectingItemsControl sender, int index) - { - var items = sender.Items; - return (index >= 0 && index < items?.Cast().Count()) ? index : -1; - } - - /// - /// Coerces the property. - /// - /// The object with the property. - /// The proposed value of the property. - /// The final value of the property. - private static object ValidateSelectedItem(SelectingItemsControl sender, object item) - { - var items = sender.Items; - return items?.Cast().Contains(item) == true ? item : null; - } - /// /// Called when new containers are initialized by the . /// diff --git a/src/Perspex.Controls/TopLevel.cs b/src/Perspex.Controls/TopLevel.cs index 82cfc0e03e..5936e08404 100644 --- a/src/Perspex.Controls/TopLevel.cs +++ b/src/Perspex.Controls/TopLevel.cs @@ -29,13 +29,13 @@ namespace Perspex.Controls /// Defines the property. /// public static readonly PerspexProperty ClientSizeProperty = - PerspexProperty.Register("ClientSize"); + PerspexProperty.RegisterDirect(nameof(ClientSize), o => o.ClientSize); /// /// Defines the property. /// public static readonly PerspexProperty IsActiveProperty = - PerspexProperty.Register("IsActive"); + PerspexProperty.RegisterDirect(nameof(IsActive), o => o.IsActive); /// /// Defines the property. @@ -43,30 +43,13 @@ namespace Perspex.Controls public static readonly PerspexProperty PointerOverElementProperty = PerspexProperty.Register(nameof(IInputRoot.PointerOverElement)); - /// - /// The render manager for the window.s - /// private readonly IRenderManager _renderManager; - - /// - /// The window renderer. - /// private readonly IRenderer _renderer; - - /// - /// The input manager for the window. - /// private readonly IInputManager _inputManager; - - /// - /// The access key handler for the window. - /// private readonly IAccessKeyHandler _accessKeyHandler; - - /// - /// The access keyboard navigation handler for the window. - /// private readonly IKeyboardNavigationHandler _keyboardNavigationHandler; + private Size _clientSize; + private bool _isActive; /// /// Initializes static members of the class. @@ -177,8 +160,8 @@ namespace Perspex.Controls /// public Size ClientSize { - get { return GetValue(ClientSizeProperty); } - private set { SetValue(ClientSizeProperty, value); } + get { return _clientSize; } + private set { SetAndRaise(ClientSizeProperty, ref _clientSize, value); } } /// @@ -186,8 +169,8 @@ namespace Perspex.Controls /// public bool IsActive { - get { return GetValue(IsActiveProperty); } - private set { SetValue(IsActiveProperty, value); } + get { return _isActive; } + private set { SetAndRaise(IsActiveProperty, ref _isActive, value); } } /// @@ -195,7 +178,8 @@ namespace Perspex.Controls /// public ILayoutManager LayoutManager { - get; } + get; + } /// /// Gets the platform-specific window implementation. diff --git a/src/Perspex.Input/InputElement.cs b/src/Perspex.Input/InputElement.cs index 1cba2af3c6..43cfc840c6 100644 --- a/src/Perspex.Input/InputElement.cs +++ b/src/Perspex.Input/InputElement.cs @@ -43,7 +43,7 @@ namespace Perspex.Input /// Defines the property. /// public static readonly PerspexProperty IsFocusedProperty = - PerspexProperty.Register("IsFocused"); + PerspexProperty.RegisterDirect("IsFocused", o => o.IsFocused); /// /// Defines the property. @@ -55,7 +55,7 @@ namespace Perspex.Input /// Defines the property. /// public static readonly PerspexProperty IsPointerOverProperty = - PerspexProperty.Register("IsPointerOver"); + PerspexProperty.RegisterDirect("IsPointerOver", o => o.IsPointerOver); /// /// Defines the event. @@ -137,6 +137,9 @@ namespace Perspex.Input "PointerWheelChanged", RoutingStrategies.Tunnel | RoutingStrategies.Bubble); + private bool _isFocused; + private bool _isPointerOver; + /// /// Initializes static members of the class. /// @@ -288,8 +291,8 @@ namespace Perspex.Input /// public bool IsFocused { - get { return GetValue(IsFocusedProperty); } - private set { SetValue(IsFocusedProperty, value); } + get { return _isFocused; } + private set { SetAndRaise(IsFocusedProperty, ref _isFocused, value); } } /// @@ -306,8 +309,8 @@ namespace Perspex.Input /// public bool IsPointerOver { - get { return GetValue(IsPointerOverProperty); } - internal set { SetValue(IsPointerOverProperty, value); } + get { return _isPointerOver; } + internal set { SetAndRaise(IsPointerOverProperty, ref _isPointerOver, value); } } /// diff --git a/src/Perspex.Input/InputExtensions.cs b/src/Perspex.Input/InputExtensions.cs index f53ecc5524..ef3f848073 100644 --- a/src/Perspex.Input/InputExtensions.cs +++ b/src/Perspex.Input/InputExtensions.cs @@ -11,7 +11,7 @@ namespace Perspex.Input { public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) { - Contract.Requires(element != null); + Contract.Requires(element != null); if (element.Bounds.Contains(p) && element.IsVisible && diff --git a/src/Perspex.Interactivity/Interactive.cs b/src/Perspex.Interactivity/Interactive.cs index 36a2f984ac..a6bfd0d94c 100644 --- a/src/Perspex.Interactivity/Interactive.cs +++ b/src/Perspex.Interactivity/Interactive.cs @@ -4,11 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reactive; using System.Reactive.Disposables; using System.Reactive.Linq; using Perspex.Layout; -using Perspex.VisualTree; namespace Perspex.Interactivity { @@ -39,8 +37,8 @@ namespace Perspex.Interactivity RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble, bool handledEventsToo = false) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + Contract.Requires(routedEvent != null); + Contract.Requires(handler != null); List subscriptions; @@ -87,8 +85,8 @@ namespace Perspex.Interactivity /// The handler. public void RemoveHandler(RoutedEvent routedEvent, Delegate handler) { - Contract.Requires(routedEvent != null); - Contract.Requires(handler != null); + Contract.Requires(routedEvent != null); + Contract.Requires(handler != null); List subscriptions; @@ -116,7 +114,7 @@ namespace Perspex.Interactivity /// The event args. public void RaiseEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + Contract.Requires(e != null); e.Source = e.Source ?? this; @@ -143,7 +141,7 @@ namespace Perspex.Interactivity /// The event args. private void BubbleEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + Contract.Requires(e != null); e.Route = RoutingStrategies.Bubble; @@ -159,7 +157,7 @@ namespace Perspex.Interactivity /// The event args. private void TunnelEvent(RoutedEventArgs e) { - Contract.Requires(e != null); + Contract.Requires(e != null); e.Route = RoutingStrategies.Tunnel; @@ -175,7 +173,7 @@ namespace Perspex.Interactivity /// The event args. private void RaiseEventImpl(RoutedEventArgs e) { - Contract.Requires(e != null); + Contract.Requires(e != null); e.RoutedEvent.InvokeClassHandlers(this, e); diff --git a/src/Perspex.Interactivity/RoutedEvent.cs b/src/Perspex.Interactivity/RoutedEvent.cs index 0b387821a4..ca865f74d0 100644 --- a/src/Perspex.Interactivity/RoutedEvent.cs +++ b/src/Perspex.Interactivity/RoutedEvent.cs @@ -26,9 +26,9 @@ namespace Perspex.Interactivity Type eventArgsType, Type ownerType) { - Contract.Requires(name != null); - Contract.Requires(eventArgsType != null); - Contract.Requires(ownerType != null); + Contract.Requires(name != null); + Contract.Requires(eventArgsType != null); + Contract.Requires(ownerType != null); Contract.Requires(typeof(RoutedEventArgs).GetTypeInfo().IsAssignableFrom(eventArgsType.GetTypeInfo())); EventArgsType = eventArgsType; @@ -66,7 +66,7 @@ namespace Perspex.Interactivity RoutingStrategies routingStrategy) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + Contract.Requires(name != null); return new RoutedEvent(name, routingStrategy, typeof(TOwner)); } @@ -77,7 +77,7 @@ namespace Perspex.Interactivity Type ownerType) where TEventArgs : RoutedEventArgs { - Contract.Requires(name != null); + Contract.Requires(name != null); return new RoutedEvent(name, routingStrategy, ownerType); } @@ -117,8 +117,8 @@ namespace Perspex.Interactivity public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType) : base(name, routingStrategies, typeof(TEventArgs), ownerType) { - Contract.Requires(name != null); - Contract.Requires(ownerType != null); + Contract.Requires(name != null); + Contract.Requires(ownerType != null); } public void AddClassHandler( diff --git a/src/Perspex.SceneGraph/Visual.cs b/src/Perspex.SceneGraph/Visual.cs index 64eae2ff21..46020d8e07 100644 --- a/src/Perspex.SceneGraph/Visual.cs +++ b/src/Perspex.SceneGraph/Visual.cs @@ -32,7 +32,7 @@ namespace Perspex /// Defines the property. /// public static readonly PerspexProperty BoundsProperty = - PerspexProperty.Register(nameof(Bounds)); + PerspexProperty.RegisterDirect(nameof(Bounds), o => o.Bounds); /// /// Defines the property. @@ -64,6 +64,12 @@ namespace Perspex public static readonly PerspexProperty TransformOriginProperty = PerspexProperty.Register(nameof(TransformOrigin), defaultValue: RelativePoint.Center); + /// + /// Defines the property. + /// + public static readonly PerspexProperty VisualParentProperty = + PerspexProperty.RegisterDirect("VisualParent", o => o._visualParent); + /// /// Defines the property. /// @@ -75,10 +81,15 @@ namespace Perspex /// private readonly PerspexList _visualChildren; + /// + /// The visual's bounds relative to its parent. + /// + private Rect _bounds; + /// /// Holds the parent of the visual. /// - private Visual _visualParent; + private IVisual _visualParent; /// /// Whether the element is attached to the visual tree. @@ -121,8 +132,8 @@ namespace Perspex /// public Rect Bounds { - get { return GetValue(BoundsProperty); } - protected set { SetValue(BoundsProperty, value); } + get { return _bounds; } + protected set { SetAndRaise(BoundsProperty, ref _bounds, value); } } /// @@ -439,6 +450,8 @@ namespace Perspex { NotifyAttachedToVisualTree(newRoot); } + + RaisePropertyChanged(VisualParentProperty, old, value, BindingPriority.LocalValue); } } diff --git a/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs b/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs index 14b24ba041..901bf678f1 100644 --- a/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs +++ b/src/Perspex.SceneGraph/VisualTree/VisualExtensions.cs @@ -19,7 +19,7 @@ namespace Perspex.VisualTree /// The visual's ancestors. public static IEnumerable GetVisualAncestors(this IVisual visual) { - Contract.Requires(visual != null); + Contract.Requires(visual != null); visual = visual.VisualParent; @@ -53,7 +53,7 @@ namespace Perspex.VisualTree /// The visuals at the requested point. public static IVisual GetVisualAt(this IVisual visual, Point p) { - Contract.Requires(visual != null); + Contract.Requires(visual != null); return visual.GetVisualsAt(p).FirstOrDefault(); } @@ -66,7 +66,7 @@ namespace Perspex.VisualTree /// The visuals at the requested point. public static IEnumerable GetVisualsAt(this IVisual visual, Point p) { - Contract.Requires(visual != null); + Contract.Requires(visual != null); if (visual.Bounds.Contains(p)) { @@ -162,7 +162,7 @@ namespace Perspex.VisualTree /// public static IVisual GetVisualRoot(this IVisual visual) { - Contract.Requires(visual != null); + Contract.Requires(visual != null); var parent = visual.VisualParent; diff --git a/src/Perspex.Styling/LogicalTree/LogicalExtensions.cs b/src/Perspex.Styling/LogicalTree/LogicalExtensions.cs index d134330036..e36b78d42c 100644 --- a/src/Perspex.Styling/LogicalTree/LogicalExtensions.cs +++ b/src/Perspex.Styling/LogicalTree/LogicalExtensions.cs @@ -11,7 +11,7 @@ namespace Perspex.LogicalTree { public static IEnumerable GetLogicalAncestors(this ILogical logical) { - Contract.Requires(logical != null); + Contract.Requires(logical != null); logical = logical.LogicalParent; diff --git a/src/Perspex.Styling/Styling/StyleBinding.cs b/src/Perspex.Styling/Styling/StyleBinding.cs index 61da30a842..c0235161db 100644 --- a/src/Perspex.Styling/Styling/StyleBinding.cs +++ b/src/Perspex.Styling/Styling/StyleBinding.cs @@ -86,7 +86,7 @@ namespace Perspex.Styling /// IDisposable object used to unsubscribe from the observable sequence. protected override IDisposable SubscribeCore(IObserver observer) { - Contract.Requires(observer != null); + Contract.Requires(observer != null); if (Source == null) { diff --git a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj index fe60b3f768..e343d3e512 100644 --- a/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj +++ b/tests/Perspex.Base.UnitTests/Perspex.Base.UnitTests.csproj @@ -73,6 +73,7 @@ + diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs index 20002d362a..7c8e60543d 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Binding.cs @@ -39,7 +39,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target.Bind(Class2.BarProperty, Observable.Return("foo")); }); @@ -212,7 +212,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target[Class1.FooProperty] = Observable.Return("newvalue"); }); diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs new file mode 100644 index 0000000000..c8b4470d13 --- /dev/null +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_Direct.cs @@ -0,0 +1,332 @@ +// 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.Collections.Generic; +using System.Reactive.Subjects; +using Xunit; + +namespace Perspex.Base.UnitTests +{ + public class PerspexObjectTests_Direct + { + [Fact] + public void GetValue_Gets_Value() + { + var target = new Class1(); + + Assert.Equal("initial", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void GetValue_Gets_Value_NonGeneric() + { + var target = new Class1(); + + Assert.Equal("initial", target.GetValue((PerspexProperty)Class1.FooProperty)); + } + + [Fact] + public void GetValue_On_Unregistered_Property_Throws_Exception() + { + var target = new Class2(); + + Assert.Throws(() => target.GetValue(Class1.BarProperty)); + } + + [Fact] + public void SetValue_Sets_Value() + { + var target = new Class1(); + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.Foo); + } + + [Fact] + public void SetValue_Sets_Value_NonGeneric() + { + var target = new Class1(); + + target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.Foo); + } + + [Fact] + public void SetValue_Raises_PropertyChanged() + { + var target = new Class1(); + bool raised = false; + + target.PropertyChanged += (s, e) => + raised = e.Property == Class1.FooProperty && + (string)e.OldValue == "initial" && + (string)e.NewValue == "newvalue" && + e.Priority == BindingPriority.LocalValue; + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.True(raised); + } + + [Fact] + public void SetValue_Raises_Changed() + { + var target = new Class1(); + bool raised = false; + + Class1.FooProperty.Changed.Subscribe(e => + raised = e.Property == Class1.FooProperty && + (string)e.OldValue == "initial" && + (string)e.NewValue == "newvalue" && + e.Priority == BindingPriority.LocalValue); + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.True(raised); + } + + [Fact] + public void SetValue_On_Unregistered_Property_Throws_Exception() + { + var target = new Class2(); + + Assert.Throws(() => target.SetValue(Class1.BarProperty, "value")); + } + + [Fact] + public void GetObservable_Returns_Values() + { + var target = new Class1(); + List values = new List(); + + target.GetObservable(Class1.FooProperty).Subscribe(x => values.Add(x)); + target.Foo = "newvalue"; + + Assert.Equal(new[] { "initial", "newvalue" }, values); + } + + [Fact] + public void Bind_Binds_Property_Value() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind(Class1.FooProperty, source); + + Assert.Equal("initial", target.Foo); + source.OnNext("first"); + Assert.Equal("first", target.Foo); + source.OnNext("second"); + Assert.Equal("second", target.Foo); + + sub.Dispose(); + + source.OnNext("third"); + Assert.Equal("second", target.Foo); + } + + [Fact] + public void Bind_Binds_Property_Value_NonGeneric() + { + var target = new Class1(); + var source = new Subject(); + + var sub = target.Bind((PerspexProperty)Class1.FooProperty, source); + + Assert.Equal("initial", target.Foo); + source.OnNext("first"); + Assert.Equal("first", target.Foo); + source.OnNext("second"); + Assert.Equal("second", target.Foo); + + sub.Dispose(); + + source.OnNext("third"); + Assert.Equal("second", target.Foo); + } + + [Fact] + public void ReadOnly_Property_Cannot_Be_Set() + { + var target = new Class1(); + + Assert.Throws(() => + target.SetValue(Class1.BarProperty, "newvalue")); + } + + [Fact] + public void ReadOnly_Property_Cannot_Be_Set_NonGeneric() + { + var target = new Class1(); + + Assert.Throws(() => + target.SetValue((PerspexProperty)Class1.BarProperty, "newvalue")); + } + + [Fact] + public void ReadOnly_Property_Cannot_Be_Bound() + { + var target = new Class1(); + var source = new Subject(); + + Assert.Throws(() => + target.Bind(Class1.BarProperty, source)); + } + + [Fact] + public void ReadOnly_Property_Cannot_Be_Bound_NonGeneric() + { + var target = new Class1(); + var source = new Subject(); + + Assert.Throws(() => + target.Bind(Class1.BarProperty, source)); + } + + [Fact] + public void GetValue_Gets_Value_On_AddOwnered_Property() + { + var target = new Class2(); + + Assert.Equal("initial2", target.GetValue(Class2.FooProperty)); + } + + [Fact] + public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original() + { + var target = new Class2(); + + Assert.Equal("initial2", target.GetValue(Class1.FooProperty)); + } + + [Fact] + public void GetValue_Gets_Value_On_AddOwnered_Property_Using_Original_NonGeneric() + { + var target = new Class2(); + + Assert.Equal("initial2", target.GetValue((PerspexProperty)Class1.FooProperty)); + } + + [Fact] + public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original() + { + var target = new Class2(); + + target.SetValue(Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.Foo); + } + + [Fact] + public void SetValue_Sets_Value_On_AddOwnered_Property_Using_Original_NonGeneric() + { + var target = new Class2(); + + target.SetValue((PerspexProperty)Class1.FooProperty, "newvalue"); + + Assert.Equal("newvalue", target.Foo); + } + + [Fact] + public void Bind_Binds_AddOwnered_Property_Value() + { + var target = new Class2(); + var source = new Subject(); + + var sub = target.Bind(Class1.FooProperty, source); + + Assert.Equal("initial2", target.Foo); + source.OnNext("first"); + Assert.Equal("first", target.Foo); + source.OnNext("second"); + Assert.Equal("second", target.Foo); + + sub.Dispose(); + + source.OnNext("third"); + Assert.Equal("second", target.Foo); + } + + [Fact] + public void Bind_Binds_AddOwnered_Property_Value_NonGeneric() + { + var target = new Class2(); + var source = new Subject(); + + var sub = target.Bind((PerspexProperty)Class1.FooProperty, source); + + Assert.Equal("initial2", target.Foo); + source.OnNext("first"); + Assert.Equal("first", target.Foo); + source.OnNext("second"); + Assert.Equal("second", target.Foo); + + sub.Dispose(); + + source.OnNext("third"); + Assert.Equal("second", target.Foo); + } + + [Fact] + public void Property_Notifies_Initialized() + { + Class1 target; + bool raised = false; + + Class1.FooProperty.Initialized.Subscribe(e => + raised = e.Property == Class1.FooProperty && + e.OldValue == PerspexProperty.UnsetValue && + (string)e.NewValue == "initial" && + e.Priority == BindingPriority.Unset); + + target = new Class1(); + + Assert.True(raised); + } + + private class Class1 : PerspexObject + { + public static readonly PerspexProperty FooProperty = + PerspexProperty.RegisterDirect("Foo", o => o.Foo, (o, v) => o.Foo = v); + + public static readonly PerspexProperty BarProperty = + PerspexProperty.RegisterDirect("Bar", o => o.Bar); + + private string _foo = "initial"; + + private string _bar = "bar"; + + public string Foo + { + get { return _foo; } + set { SetAndRaise(FooProperty, ref _foo, value); } + } + + public string Bar + { + get { return _bar; } + } + } + + private class Class2 : PerspexObject + { + public static readonly PerspexProperty FooProperty = + Class1.FooProperty.AddOwner(o => o.Foo, (o, v) => o.Foo = v); + + private string _foo = "initial2"; + + static Class2() + { + } + + public string Foo + { + get { return _foo; } + set { SetAndRaise(FooProperty, ref _foo, value); } + } + } + } +} diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs index 7fe24fc056..f143f56c56 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_GetValue.cs @@ -1,6 +1,7 @@ // 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 Xunit; namespace Perspex.Base.UnitTests @@ -44,6 +45,14 @@ namespace Perspex.Base.UnitTests Assert.Equal("changed", child.GetValue(Class1.BazProperty)); } + [Fact] + public void GetValue_Throws_Exception_For_Unregistered_Property() + { + var target = new Class3(); + + Assert.Throws(() => target.GetValue(Class1.FooProperty)); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = @@ -66,5 +75,9 @@ namespace Perspex.Base.UnitTests set { InheritanceParent = value; } } } + + private class Class3 : PerspexObject + { + } } } diff --git a/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs b/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs index ed79e12b44..7c7dce01aa 100644 --- a/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs +++ b/tests/Perspex.Base.UnitTests/PerspexObjectTests_SetValue.cs @@ -87,7 +87,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target.SetValue(Class2.BarProperty, "invalid"); }); @@ -98,7 +98,7 @@ namespace Perspex.Base.UnitTests { Class1 target = new Class1(); - Assert.Throws(() => + Assert.Throws(() => { target.SetValue(Class1.FooProperty, 123); }); diff --git a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs index 9f4fa62aa1..6c90ac1039 100644 --- a/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs +++ b/tests/Perspex.Base.UnitTests/PerspexPropertyTests.cs @@ -125,6 +125,69 @@ namespace Perspex.Base.UnitTests Assert.Equal("newvalue", value); } + [Fact] + public void IsDirect_Property_Set_On_Direct_PerspexProperty() + { + PerspexProperty target = new PerspexProperty( + "test", + typeof(Class1), + o => null, + (o, v) => { }); + + Assert.True(target.IsDirect); + } + + [Fact] + public void Property_Equals_Should_Handle_Null() + { + var p1 = new PerspexProperty("p1", typeof(Class1)); + + Assert.NotEqual(p1, null); + Assert.NotEqual(null, p1); + Assert.False(p1 == null); + Assert.False(null == p1); + Assert.False(p1.Equals(null)); + Assert.True((PerspexProperty)null == (PerspexProperty)null); + } + + [Fact] + public void AddOwnered_Property_Should_Equal_Original() + { + var p1 = new PerspexProperty("p1", typeof(Class1)); + var p2 = p1.AddOwner(); + + Assert.Equal(p1, p2); + Assert.Equal(p1.GetHashCode(), p2.GetHashCode()); + Assert.True(p1 == p2); + } + + [Fact] + public void AddOwnered_Direct_Property_Should_Equal_Original() + { + var p1 = new PerspexProperty("d1", typeof(Class1), o => null, (o,v) => { }); + var p2 = p1.AddOwner(o => null, (o, v) => { }); + + Assert.Equal(p1, p2); + Assert.Equal(p1.GetHashCode(), p2.GetHashCode()); + Assert.True(p1 == p2); + } + + [Fact] + public void AddOwner_With_Getter_And_Setter_On_Standard_Property_Should_Throw() + { + var p1 = new PerspexProperty("p1", typeof(Class1)); + + Assert.Throws(() => p1.AddOwner(o => null, (o, v) => { })); + } + + [Fact] + public void AddOwner_On_Direct_Property_Without_Getter_Or_Setter_Should_Throw() + { + var p1 = new PerspexProperty("e1", typeof(Class1), o => null, (o, v) => { }); + + Assert.Throws(() => p1.AddOwner()); + } + private class Class1 : PerspexObject { public static readonly PerspexProperty FooProperty = @@ -135,7 +198,7 @@ namespace Perspex.Base.UnitTests { } - private class Class3 + private class Class3 : PerspexObject { } } diff --git a/tests/Perspex.Base.UnitTests/PriorityValueTests.cs b/tests/Perspex.Base.UnitTests/PriorityValueTests.cs index dd69d3eb79..e4cfa408f7 100644 --- a/tests/Perspex.Base.UnitTests/PriorityValueTests.cs +++ b/tests/Perspex.Base.UnitTests/PriorityValueTests.cs @@ -47,7 +47,7 @@ namespace Perspex.Base.UnitTests var target = new PriorityValue("Test", typeof(string)); target.Add(Single("foo"), 0); - target.SetDirectValue("bar", 0); + target.SetValue("bar", 0); Assert.Equal("bar", target.Value); } @@ -60,7 +60,7 @@ namespace Perspex.Base.UnitTests target.Add(source, 0); Assert.Equal("initial", target.Value); - target.SetDirectValue("first", 0); + target.SetValue("first", 0); Assert.Equal("first", target.Value); source.OnNext("second"); Assert.Equal("second", target.Value); @@ -76,7 +76,7 @@ namespace Perspex.Base.UnitTests target.Add(nonActive, 0); target.Add(source, 0); Assert.Equal("initial", target.Value); - target.SetDirectValue("first", 0); + target.SetValue("first", 0); Assert.Equal("first", target.Value); nonActive.OnNext("second"); Assert.Equal("second", target.Value); @@ -92,7 +92,7 @@ namespace Perspex.Base.UnitTests target.Add(nonActive, 1); target.Add(source, 1); Assert.Equal("initial", target.Value); - target.SetDirectValue("first", 1); + target.SetValue("first", 1); Assert.Equal("first", target.Value); nonActive.OnNext("second"); Assert.Equal("first", target.Value); @@ -106,7 +106,7 @@ namespace Perspex.Base.UnitTests target.Add(source, 0); Assert.Equal("initial", target.Value); - target.SetDirectValue("first", 0); + target.SetValue("first", 0); Assert.Equal("first", target.Value); source.OnNext("second"); Assert.Equal("second", target.Value); @@ -267,9 +267,9 @@ namespace Perspex.Base.UnitTests { var target = new PriorityValue("Test", typeof(int), x => Math.Min((int)x, 10)); - target.SetDirectValue(5, 0); + target.SetValue(5, 0); Assert.Equal(5, target.Value); - target.SetDirectValue(15, 0); + target.SetValue(15, 0); Assert.Equal(10, target.Value); } diff --git a/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs b/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs index b2ff5029b2..05b330872f 100644 --- a/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs +++ b/tests/Perspex.Controls.UnitTests/ContentPresenterTests.cs @@ -4,6 +4,7 @@ using System.Collections.Specialized; using System.Linq; using Perspex.Controls.Presenters; +using Perspex.LogicalTree; using Xunit; namespace Perspex.Controls.UnitTests @@ -29,11 +30,28 @@ namespace Perspex.Controls.UnitTests var child = new Control(); target.Content = child; + target.ApplyTemplate(); target.Content = null; + target.ApplyTemplate(); Assert.Equal(new ILogical[0], ((ILogical)target).LogicalChildren.ToList()); } + [Fact] + public void Clearing_Content_Clear_Childs_Parent() + { + var target = new ContentPresenter(); + var child = new Control(); + + target.Content = child; + target.ApplyTemplate(); + target.Content = null; + target.ApplyTemplate(); + + Assert.Null(child.Parent); + Assert.Null(child.GetLogicalParent()); + } + [Fact] public void Changing_Content_Should_Fire_LogicalChildren_CollectionChanged() { diff --git a/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs b/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs index e85d89c22a..f73637fb07 100644 --- a/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs +++ b/tests/Perspex.Controls.UnitTests/Primitives/TabStripTests.cs @@ -33,6 +33,7 @@ namespace Perspex.Controls.UnitTests.Primitives target.ApplyTemplate(); + Assert.Equal(0, target.SelectedIndex); Assert.Equal(target.Items.Cast().First(), target.SelectedItem); Assert.Equal(target.Items.Cast().First(), target.SelectedTab); } diff --git a/tests/Perspex.Controls.UnitTests/Utils/HotKeyManagerTests.cs b/tests/Perspex.Controls.UnitTests/Utils/HotKeyManagerTests.cs index bbc1e78166..fc0c17b9d3 100644 --- a/tests/Perspex.Controls.UnitTests/Utils/HotKeyManagerTests.cs +++ b/tests/Perspex.Controls.UnitTests/Utils/HotKeyManagerTests.cs @@ -43,11 +43,13 @@ namespace Perspex.Controls.UnitTests.Utils HotKeyManager.SetHotKey(button, gesture2); Assert.Equal(gesture2, tl.KeyBindings[0].Gesture); - button.SetValue(Control.ParentProperty, null); + tl.Content = null; + tl.Presenter.ApplyTemplate(); Assert.Empty(tl.KeyBindings); - button.SetValue(Control.ParentProperty, tl); + tl.Content = button; + tl.Presenter.ApplyTemplate(); Assert.Equal(gesture2, tl.KeyBindings[0].Gesture); diff --git a/tests/Perspex.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs b/tests/Perspex.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs index 575bdbad2f..de66909bab 100644 --- a/tests/Perspex.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs +++ b/tests/Perspex.Markup.Xaml.UnitTests/Properties/AssemblyInfo.cs @@ -2,5 +2,9 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System.Reflection; +using Xunit; -[assembly: AssemblyTitle("Perspex.Markup.Xaml.UnitTests")] \ No newline at end of file +[assembly: AssemblyTitle("Perspex.Markup.Xaml.UnitTests")] + +// Don't run tests in parallel. +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs index a4b8c40e52..31acf6f9dc 100644 --- a/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs +++ b/tests/Perspex.SceneGraph.UnitTests/VisualTests.cs @@ -1,6 +1,8 @@ // 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.Collections.Generic; using System.Linq; using Perspex.VisualTree; using Xunit; @@ -31,6 +33,20 @@ namespace Perspex.SceneGraph.UnitTests Assert.Equal(target, child.InheritanceParent); } + [Fact] + public void Added_Child_Should_Notify_VisualParent_Changed() + { + var target = new TestVisual(); + var child = new TestVisual(); + var parents = new List(); + + child.GetObservable(Visual.VisualParentProperty).Subscribe(x => parents.Add(x)); + target.AddChild(child); + target.RemoveChild(child); + + Assert.Equal(new IVisual[] { null, target, null }, parents); + } + [Fact] public void Removed_Child_Should_Have_VisualParent_Cleared() {