// -----------------------------------------------------------------------
//
// Copyright 2014 MIT Licence. See licence.md for more information.
//
// -----------------------------------------------------------------------
namespace Perspex
{
using Perspex.Reactive;
using Serilog;
using Serilog.Core.Enrichers;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reflection;
///
/// The priority of a binding.
///
public enum BindingPriority
{
///
/// A value that comes from an animation.
///
Animation = -2,
///
/// 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.
///
///
/// This class is analogous to DependencyObject in WPF.
///
public class PerspexObject : INotifyPropertyChanged
{
///
/// The registered properties by type.
///
private static Dictionary> registered =
new Dictionary>();
///
/// The parent object that inherited values are inherited from.
///
private PerspexObject inheritanceParent;
///
/// The set values/bindings on this object.
///
private Dictionary values =
new Dictionary();
///
/// Event handler for implementation.
///
private PropertyChangedEventHandler inpcChanged;
///
/// A serilog logger for logging property events.
///
private ILogger propertyLog;
///
/// Initializes a new instance of the class.
///
public PerspexObject()
{
this.propertyLog = Log.ForContext(new[]
{
new PropertyEnricher("Area", "Property"),
new PropertyEnricher("SourceContext", this.GetType()),
new PropertyEnricher("Id", this.GetHashCode()),
});
foreach (var property in this.GetRegisteredProperties())
{
var e = new PerspexPropertyChangedEventArgs(
this,
property,
PerspexProperty.UnsetValue,
property.GetDefaultValue(this.GetType()),
BindingPriority.Unset);
property.NotifyInitialized(e);
}
}
///
/// Raised when a value changes on this object.
///
public event EventHandler PropertyChanged;
///
/// Raised when a value changes on this object.
///
event PropertyChangedEventHandler INotifyPropertyChanged.PropertyChanged
{
add { this.inpcChanged += value; }
remove { this.inpcChanged -= value; }
}
///
/// Gets or sets the parent object that inherited values
/// are inherited from.
///
protected PerspexObject InheritanceParent
{
get
{
return this.inheritanceParent;
}
set
{
if (this.inheritanceParent != value)
{
if (this.inheritanceParent != null)
{
this.inheritanceParent.PropertyChanged -= this.ParentPropertyChanged;
}
var inherited = (from property in GetProperties(this.GetType())
where property.Inherits
select new
{
Property = property,
Value = this.GetValue(property),
}).ToList();
this.inheritanceParent = value;
foreach (var i in inherited)
{
object newValue = this.GetValue(i.Property);
if (!object.Equals(i.Value, newValue))
{
this.RaisePropertyChanged(i.Property, i.Value, newValue, BindingPriority.LocalValue);
}
}
if (this.inheritanceParent != null)
{
this.inheritanceParent.PropertyChanged += this.ParentPropertyChanged;
}
}
}
}
///
/// Gets or sets the value of a .
///
/// The property.
public object this[PerspexProperty property]
{
get { return this.GetValue(property); }
set { this.SetValue(property, value); }
}
///
/// Gets or sets a binding for a .
///
/// The binding information.
public IObservable this[Binding binding]
{
get
{
return new Binding
{
Mode = binding.Mode,
Priority = binding.Priority,
Property = binding.Property,
Source = this,
};
}
set
{
BindingMode mode = (binding.Mode == BindingMode.Default) ?
binding.Property.DefaultBindingMode :
binding.Mode;
Binding sourceBinding = value as Binding;
if (sourceBinding == null && mode != BindingMode.OneWay)
{
throw new InvalidOperationException("Can only bind OneWay to plain IObservable.");
}
switch (mode)
{
case BindingMode.Default:
case BindingMode.OneWay:
this.Bind(binding.Property, value, binding.Priority);
break;
case BindingMode.OneTime:
this.SetValue(binding.Property, sourceBinding.Source.GetValue(sourceBinding.Property), binding.Priority);
break;
case BindingMode.OneWayToSource:
sourceBinding.Source.Bind(sourceBinding.Property, this.GetObservable(binding.Property), binding.Priority);
break;
case BindingMode.TwoWay:
this.BindTwoWay(binding.Property, sourceBinding.Source, sourceBinding.Property);
break;
}
}
}
///
/// Gets all s registered on a type.
///
/// The type.
/// A collection of definitions.
public static IEnumerable GetProperties(Type type)
{
Contract.Requires(type != null);
TypeInfo i = type.GetTypeInfo();
while (type != null)
{
List list;
if (registered.TryGetValue(type, out list))
{
foreach (PerspexProperty p in list)
{
yield return p;
}
}
type = type.GetTypeInfo().BaseType;
}
}
///
/// Registers a on a type.
///
/// The type.
/// The property.
///
/// You won't usually want to call this method directly, instead use the
/// method.
///
public static void Register(Type type, PerspexProperty property)
{
Contract.Requires(type != null);
Contract.Requires(property != null);
List list;
if (!registered.TryGetValue(type, out list))
{
list = new List();
registered.Add(type, list);
}
if (!list.Contains(property))
{
list.Add(property);
}
}
///
/// Clears a 's local value.
///
/// The property.
public void ClearValue(PerspexProperty property)
{
Contract.Requires(property != null);
this.SetValue(property, PerspexProperty.UnsetValue);
}
///
/// Gets an observable for a .
///
/// The property.
/// An observable.
public IObservable GetObservable(PerspexProperty property)
{
Contract.Requires(property != null);
return new PerspexObservable(
observer =>
{
EventHandler handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(e.NewValue);
}
};
observer.OnNext(this.GetValue(property));
this.PropertyChanged += handler;
return Disposable.Create(() =>
{
this.PropertyChanged -= handler;
});
},
this.GetObservableDescription(property));
}
///
/// Gets an observable for a .
///
/// The property type.
/// The property.
/// An observable.
public IObservable GetObservable(PerspexProperty property)
{
Contract.Requires(property != null);
return this.GetObservable((PerspexProperty)property).Cast();
}
///
/// Gets an observable for a .
///
///
///
///
public IObservable> GetObservableWithHistory(PerspexProperty property)
{
return new PerspexObservable>(
observer =>
{
EventHandler handler = (s, e) =>
{
if (e.Property == property)
{
observer.OnNext(Tuple.Create((T)e.OldValue, (T)e.NewValue));
}
};
this.PropertyChanged += handler;
return Disposable.Create(() =>
{
this.PropertyChanged -= handler;
});
},
this.GetObservableDescription(property));
}
///
/// Gets a value.
///
/// The property.
/// The value.
public object GetValue(PerspexProperty property)
{
Contract.Requires(property != null);
object result;
PriorityValue value;
if (this.values.TryGetValue(property, out value))
{
result = value.Value;
}
else
{
result = PerspexProperty.UnsetValue;
}
if (result == PerspexProperty.UnsetValue)
{
result = this.GetDefaultValue(property);
}
return result;
}
///
/// Gets a value.
///
/// The property.
/// The value.
public T GetValue(PerspexProperty property)
{
Contract.Requires(property != null);
return (T)this.GetValue((PerspexProperty)property);
}
///
/// Gets all properties that are registered on this object.
///
///
/// A collection of objects.
///
public IEnumerable GetRegisteredProperties()
{
Type type = this.GetType();
while (type != null)
{
List list;
if (registered.TryGetValue(type, out list))
{
foreach (var p in list)
{
yield return p;
}
}
type = type.GetTypeInfo().BaseType;
}
}
///
/// Checks whether a is set on this object.
///
/// The property.
/// True if the property is set, otherwise false.
public bool IsSet(PerspexProperty property)
{
Contract.Requires(property != null);
return this.values.ContainsKey(property);
}
///
/// Checks whether a is registered on this class.
///
/// The property.
/// True if the property is registered, otherwise false.
public bool IsRegistered(PerspexProperty property)
{
Type type = this.GetType();
while (type != null)
{
List list;
if (registered.TryGetValue(type, out list))
{
if (list.Contains(property))
{
return true;
}
}
type = type.GetTypeInfo().BaseType;
}
return false;
}
///
/// Sets a value.
///
/// The property.
/// The value.
/// The priority of the value.
public void SetValue(
PerspexProperty property,
object value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires(property != null);
PriorityValue v;
if (!this.IsRegistered(property))
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
this.GetType()));
}
if (!PriorityValue.IsValidValue(value, property.PropertyType))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
property.Name,
value,
value.GetType().FullName));
}
if (!this.values.TryGetValue(property, out v))
{
if (value == PerspexProperty.UnsetValue)
{
return;
}
v = this.CreatePriorityValue(property);
this.values.Add(property, v);
}
this.propertyLog.Verbose(
"Set {Property} to {$Value} with priority {Priority}",
property,
value,
priority);
v.SetDirectValue(value, (int)priority);
}
///
/// Sets a value.
///
/// The type of the property.
/// The property.
/// The value.
/// The priority of the value.
public void SetValue(
PerspexProperty property,
T value,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires(property != null);
this.SetValue((PerspexProperty)property, value, priority);
}
///
/// Binds a to an observable.
///
/// The type of the property.
/// The property.
/// The observable.
/// The priority of the binding.
///
/// A disposable which can be used to terminate the binding.
///
public IDisposable Bind(
PerspexProperty property,
IObservable source,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires(property != null);
PriorityValue v;
IDescription description = source as IDescription;
if (!this.IsRegistered(property))
{
throw new InvalidOperationException(string.Format(
"Property '{0}' not registered on '{1}'",
property.Name,
this.GetType()));
}
if (!this.values.TryGetValue(property, out v))
{
v = this.CreatePriorityValue(property);
this.values.Add(property, v);
}
this.propertyLog.Verbose(
"Bound {Property} to {Binding} with priority {Priority}",
property,
source,
priority);
return v.Add(source, (int)priority);
}
///
/// Binds a to an observable.
///
/// The type of the property.
/// The property.
/// The observable.
/// The priority of the binding.
///
/// A disposable which can be used to terminate the binding.
///
public IDisposable Bind(
PerspexProperty property,
IObservable source,
BindingPriority priority = BindingPriority.LocalValue)
{
Contract.Requires(property != null);
return this.Bind((PerspexProperty)property, source.Select(x => (object)x), priority);
}
///
/// Initialites a two-way bind between s.
///
/// The property on this object.
/// The source object.
/// The property on the source object.
/// The priority of the binding.
///
/// A disposable which can be used to terminate the binding.
///
///
/// The binding is first carried out from to this.
///
public IDisposable BindTwoWay(
PerspexProperty property,
PerspexObject source,
PerspexProperty sourceProperty,
BindingPriority priority = BindingPriority.LocalValue)
{
return new CompositeDisposable(
this.Bind(property, source.GetObservable(sourceProperty)),
source.Bind(sourceProperty, this.GetObservable(property)));
}
///
/// Forces the specified property to be re-coerced.
///
/// The property.
public void CoerceValue(PerspexProperty property)
{
PriorityValue value;
if (this.values.TryGetValue(property, out value))
{
value.Coerce();
}
}
///
/// Gets all priority values set on the object.
///
/// A collection of property/value tuples.
internal IDictionary GetSetValues()
{
return this.values;
}
///
/// Forces re-coercion of properties when a property value changes.
///
/// The property to that affects coercion.
/// The affected properties.
protected static void AffectsCoercion(PerspexProperty property, params PerspexProperty[] affected)
{
property.Changed.Subscribe(e =>
{
foreach (var p in affected)
{
e.Sender.CoerceValue(p);
}
});
}
///
/// Called when a perspex property changes on the object.
///
/// The event arguments.
protected virtual void OnPropertyChanged(PerspexPropertyChangedEventArgs e)
{
}
///
/// Creates a for a .
///
/// The property.
/// The .
private PriorityValue CreatePriorityValue(PerspexProperty property)
{
Func coerce = null;
if (property.Coerce != null)
{
coerce = v => property.Coerce(this, v);
}
PriorityValue result = new PriorityValue(property.Name, property.PropertyType, coerce);
result.Changed.Subscribe(x =>
{
object oldValue = (x.Item1 == PerspexProperty.UnsetValue) ?
this.GetDefaultValue(property) :
x.Item1;
object newValue = (x.Item2 == PerspexProperty.UnsetValue) ?
this.GetDefaultValue(property) :
x.Item2;
if (!object.Equals(oldValue, newValue))
{
this.RaisePropertyChanged(property, oldValue, newValue, (BindingPriority)result.ValuePriority);
this.propertyLog.Verbose(
"{Property} changed from {$Old} to {$Value} with priority {Priority}",
property,
oldValue,
newValue,
(BindingPriority)result.ValuePriority);
}
});
return result;
}
///
/// Gets the default value for a property.
///
/// The property.
/// The default value.
private object GetDefaultValue(PerspexProperty property)
{
if (property.Inherits && this.inheritanceParent != null)
{
return this.inheritanceParent.GetValue(property);
}
else
{
return property.GetDefaultValue(this.GetType());
}
}
///
/// Called when a property is changed on the current .
///
/// The event sender.
/// The event args.
///
/// Checks for changes in an inherited property value.
///
private void ParentPropertyChanged(object sender, PerspexPropertyChangedEventArgs e)
{
Contract.Requires(e != null);
if (e.Property.Inherits && !this.IsSet(e.Property))
{
this.RaisePropertyChanged(e.Property, e.OldValue, e.NewValue, BindingPriority.LocalValue);
}
}
///
/// Gets a description of a property that van be used in observables.
///
/// The property
/// The description.
private string GetObservableDescription(PerspexProperty property)
{
return string.Format("{0}.{1}", this.GetType().Name, property.Name);
}
///
/// 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.
private void RaisePropertyChanged(
PerspexProperty property,
object oldValue,
object newValue,
BindingPriority priority)
{
Contract.Requires(property != null);
PerspexPropertyChangedEventArgs e = new PerspexPropertyChangedEventArgs(
this,
property,
oldValue,
newValue,
priority);
this.OnPropertyChanged(e);
property.NotifyChanged(e);
if (this.PropertyChanged != null)
{
this.PropertyChanged(this, e);
}
if (this.inpcChanged != null)
{
PropertyChangedEventArgs e2 = new PropertyChangedEventArgs(property.Name);
this.inpcChanged(this, e2);
}
}
}
}