// 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 System.Reflection; using Perspex.Utilities; namespace Perspex { /// /// A perspex property. /// /// /// This class is analogous to DependencyProperty in WPF. /// 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. /// private readonly Dictionary _defaultValues = new Dictionary(); /// /// Observable fired when this property changes on any . /// private readonly Subject _initialized = new Subject(); /// /// Observable fired when this property changes on any . /// private readonly Subject _changed = new Subject(); /// /// The validation functions for the property, by type. /// private readonly Dictionary> _validation = new Dictionary>(); /// /// Gets the ID of the property. /// private int _id; /// /// 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. /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. /// A validation function. /// Whether the property is an attached property. public PerspexProperty( string name, Type valueType, Type ownerType, object defaultValue, bool inherits = false, BindingMode defaultBindingMode = BindingMode.Default, Func validate = null, bool isAttached = false) { Contract.Requires(name != null); 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; _defaultValues.Add(ownerType, defaultValue); Inherits = inherits; DefaultBindingMode = defaultBindingMode; IsAttached = isAttached; _id = s_nextId++; if (validate != null) { _validation.Add(ownerType, validate); } } /// /// 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. /// /// /// The name of the property. /// public string Name { get; } /// /// Gets the type of the property's value. /// /// /// The type of the property's value. /// public Type PropertyType { get; } /// /// Gets the type of the class that registers the property. /// /// /// The type of the class that registers the property. /// public Type OwnerType { get; } /// /// Gets a value indicating whether the property inherits its value. /// /// /// A value indicating whether the property inherits its value. /// public bool Inherits { get; } /// /// Gets the default binding mode for the property. /// /// /// The default binding mode for the property. /// public BindingMode DefaultBindingMode { get; } /// /// Gets a value indicating whether this is an attached property. /// /// /// A value indicating whether this is an attached property. /// 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. /// /// /// This observable is fired each time a new is constructed /// for all properties registered on the object's type. The default value of the property /// for the object is passed in the args' NewValue (OldValue will always be /// . /// /// /// An observable that is fired when this property is initialized on a new /// instance. /// public IObservable Initialized => _initialized; /// /// Gets an observable that is fired when this property changes on any /// instance. /// /// /// An observable that is fired when this property changes on any /// instance. /// public IObservable Changed => _changed; /// /// Provides access to a property's binding via the /// indexer. /// /// The property. /// A describing the binding. public static BindingDescriptor operator !(PerspexProperty property) { return new BindingDescriptor { Priority = BindingPriority.LocalValue, Property = property, }; } /// /// Provides access to a property's template binding via the /// indexer. /// /// The property. /// A describing the binding. public static BindingDescriptor operator ~(PerspexProperty property) { return new BindingDescriptor { Priority = BindingPriority.TemplatedParent, Property = property, }; } /// /// 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) { return a?.Equals(b) ?? false; } /// /// 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?.Equals(b) ?? false; } /// /// Registers a . /// /// The type of the class that is registering the property. /// The type of the property's value. /// The name of the property. /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. /// A validation function. /// A public static PerspexProperty Register( string name, TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null) where TOwner : PerspexObject { Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, typeof(TOwner), defaultValue, inherits, defaultBindingMode, Cast(validate), false); PerspexObject.Register(typeof(TOwner), result); 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 . /// /// The type of the class that is registering the property. /// The type of the class that the property is to be registered on. /// The type of the property's value. /// The name of the property. /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. /// A validation function. /// A public static PerspexProperty RegisterAttached( string name, TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null) { Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, typeof(TOwner), defaultValue, inherits, defaultBindingMode, validate, true); PerspexObject.Register(typeof(THost), result); return result; } /// /// Registers an attached . /// /// The type of the class that the property is to be registered on. /// The type of the property's value. /// The name of the property. /// The type of the class that is registering the property. /// The default value of the property. /// Whether the property inherits its value. /// The default binding mode for the property. /// A validation function. /// A public static PerspexProperty RegisterAttached( string name, Type ownerType, TValue defaultValue = default(TValue), bool inherits = false, BindingMode defaultBindingMode = BindingMode.OneWay, Func validate = null) { Contract.Requires(name != null); PerspexProperty result = new PerspexProperty( name, ownerType, defaultValue, inherits, defaultBindingMode, validate, true); PerspexObject.Register(typeof(THost), result); 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. /// /// A . /// /// The ! and ~ operators are short forms of this. /// public BindingDescriptor Bind() { return new BindingDescriptor { Property = this, }; } /// /// Gets the default value for the property on the specified type. /// /// The type. /// The default value. public object GetDefaultValue(Type type) { Contract.Requires(type != null); while (type != null) { object result; if (_defaultValues.TryGetValue(type, out result)) { return result; } type = type.GetTypeInfo().BaseType; } return _defaultValues[OwnerType]; } /// /// Gets the validation function for the property on the specified type. /// /// The type. /// /// The validation function, or null if no validation function registered for this type. /// public Func GetValidationFunc(Type type) { Contract.Requires(type != null); while (type != null) { Func result; if (_validation.TryGetValue(type, out result)) { return result; } type = type.GetTypeInfo().BaseType; } return null; } /// /// Checks whether the is valid for the property. /// /// The value. /// True if the value is valid, otherwise false. public bool IsValidValue(object value) { return TypeUtilities.TryCast(PropertyType, value, out value); } /// /// Overrides the default value for the property on the specified type. /// /// The type. /// The default value. public void OverrideDefaultValue(object defaultValue) { OverrideDefaultValue(typeof(T), defaultValue); } /// /// Overrides the default value for the property on the specified type. /// /// The type. /// The default value. public void OverrideDefaultValue(Type type, object defaultValue) { Contract.Requires(type != null); if (!TypeUtilities.TryCast(PropertyType, defaultValue, out defaultValue)) { throw new InvalidOperationException(string.Format( "Invalid value for Property '{0}': {1} ({2})", Name, defaultValue, defaultValue.GetType().FullName)); } if (_defaultValues.ContainsKey(type)) { throw new InvalidOperationException("Default value is already set for this property."); } _defaultValues.Add(type, defaultValue); } /// /// Overrides the validation function for the property on the specified type. /// /// The type. /// The validation function. public void OverrideValidation(Type type, Func validation) { Contract.Requires(type != null); if (_validation.ContainsKey(type)) { throw new InvalidOperationException("Validation is already set for this property."); } _validation.Add(type, validation); } /// /// Gets the string representation of the property. /// /// The property's string representation. public override string ToString() { return Name; } /// /// Notifies the observable. /// /// The observable arguments. internal void NotifyInitialized(PerspexPropertyChangedEventArgs e) { _initialized.OnNext(e); } /// /// Notifies the observable. /// /// The observable arguments. internal void NotifyChanged(PerspexPropertyChangedEventArgs e) { _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. /// The typed function. /// The untyped function. private static Func Cast(Func f) where TOwner : PerspexObject { return f != null ? (o, v) => f((TOwner)o, v) : (Func)null; } /// /// Class representing the . /// private class Unset { /// /// Returns the string representation of the . /// /// The string "(unset)". public override string ToString() { return "(unset)"; } } } }