// 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)";
}
}
}
}