// -----------------------------------------------------------------------
//
// Copyright 2014 MIT Licence. See licence.md for more information.
//
// -----------------------------------------------------------------------
namespace Perspex
{
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Reactive.Subjects;
using System.Reflection;
///
/// Maintains a list of prioritised bindings together with a current value.
///
///
/// Bindings, in the form of s are added to the object using
/// the method. With the observable is passed a priority, where lower values
/// represent higher priorites. The current is selected from the highest
/// priority binding that doesn't return . Where there
/// are multiple bindings registered with the same priority, the most recently added binding
/// has a higher priority. Each time the value changes to a distinct new value, the
/// observable is fired with the old and new values.
///
public class PriorityValue
{
///
/// The name of the property.
///
private string name;
///
/// The value type.
///
private Type valueType;
///
/// The currently registered binding entries.
///
private LinkedList bindings = new LinkedList();
///
/// The changed observable.
///
private Subject> changed = new Subject>();
///
/// The current value.
///
private object value;
private Func coerce;
///
/// Initializes a new instance of the class.
///
/// The name of the property.
/// The value type.
/// An optional coercion function.
public PriorityValue(string name, Type valueType, Func coerce = null)
{
this.name = name;
this.valueType = valueType;
this.value = PerspexProperty.UnsetValue;
this.ValuePriority = int.MaxValue;
this.coerce = coerce;
}
///
/// Fired whenever the current changes to a new distinct value.
///
public IObservable> Changed
{
get { return this.changed; }
}
///
/// Gets the current value.
///
public object Value
{
get { return this.value; }
}
///
/// Gets the priority of the binding that is currently active.
///
public int ValuePriority
{
get;
private set;
}
///
/// Checks whether a value is valid for a type.
///
///
///
///
public static bool IsValidValue(object value, Type propertyType)
{
TypeInfo type = propertyType.GetTypeInfo();
if (value == PerspexProperty.UnsetValue)
{
return true;
}
else if (value == null)
{
if (type.IsValueType &&
(!type.IsGenericType || !(type.GetGenericTypeDefinition() == typeof(Nullable<>))))
{
return false;
}
}
else
{
if (!type.IsAssignableFrom(value.GetType().GetTypeInfo()))
{
return false;
}
}
return true;
}
///
/// Adds a new binding.
///
/// The binding.
/// The binding priority.
///
/// A disposable that will remove the binding.
///
public IDisposable Add(IObservable binding, int priority)
{
BindingEntry entry = new BindingEntry();
LinkedListNode insert = this.bindings.First;
while (insert != null && insert.Value.Priority < priority)
{
insert = insert.Next;
}
if (insert == null)
{
this.bindings.AddLast(entry);
}
else
{
this.bindings.AddBefore(insert, entry);
}
entry.Start(binding, priority, this.EntryChanged, this.EntryCompleted);
return Disposable.Create(() =>
{
this.Remove(entry);
});
}
///
/// Adds a new binding, replacing all those of the same priority.
///
/// The binding.
/// The binding priority.
///
/// A disposable that will remove the binding.
///
public IDisposable Replace(IObservable binding, int priority)
{
BindingEntry entry = new BindingEntry();
LinkedListNode insert = this.bindings.First;
while (insert != null && insert.Value.Priority < priority)
{
insert = insert.Next;
}
while (insert != null && insert.Value.Priority == priority)
{
LinkedListNode next = insert.Next;
insert.Value.Dispose();
this.bindings.Remove(insert);
insert = next;
}
if (insert == null)
{
this.bindings.AddLast(entry);
}
else
{
this.bindings.AddBefore(insert, entry);
}
entry.Start(binding, priority, this.EntryChanged, this.EntryCompleted);
return Disposable.Create(() =>
{
this.Remove(entry);
});
}
///
/// Removes all bindings with the specified priority.
///
/// The priority.
public void Clear(int priority)
{
LinkedListNode item = this.bindings.First;
bool removed = false;
while (item != null && item.Value.Priority <= priority)
{
LinkedListNode next = item.Next;
if (item.Value.Priority == priority)
{
item.Value.Dispose();
this.bindings.Remove(item);
removed = true;
}
item = next;
}
if (removed && priority <= this.ValuePriority)
{
this.UpdateValue();
}
}
///
/// Gets the currently active bindings on this object.
///
/// An enumerable collection of bindings.
public IEnumerable GetBindings()
{
return this.bindings;
}
///
/// Causes a re-coercion of the value.
///
public void Coerce()
{
if (this.coerce != null)
{
this.SetValue(this.Value, this.ValuePriority);
}
}
///
/// Throws an exception if is invalid.
///
/// The value.
private void VerifyValidValue(object value)
{
if (!IsValidValue(value, this.valueType))
{
throw new InvalidOperationException(string.Format(
"Invalid value for Property '{0}': {1} ({2})",
this.name,
value,
value.GetType().FullName));
}
}
///
/// Called when a binding's value changes.
///
/// The changed entry.
private void EntryChanged(BindingEntry changed)
{
if (changed.Priority <= this.ValuePriority)
{
this.UpdateValue();
}
}
///
/// Called when a binding completes.
///
/// The completed entry.
private void EntryCompleted(BindingEntry entry)
{
this.Remove(entry);
}
///
/// Sets the current value and notifies all observers.
///
/// The new value.
/// The priority of the binding which produced the value.
private void SetValue(object value, int priority)
{
VerifyValidValue(value);
if (this.coerce != null)
{
value = this.coerce(value);
VerifyValidValue(value);
}
object old = this.value;
this.ValuePriority = priority;
if (!EqualityComparer.Default.Equals(old, value))
{
this.value = value;
this.changed.OnNext(Tuple.Create(old, value));
}
}
///
/// Removes the specified binding entry and updates the current value.
///
/// The binding entry to remove.
private void Remove(BindingEntry entry)
{
entry.Dispose();
this.bindings.Remove(entry);
this.UpdateValue();
}
///
/// Updates the current value.
///
private void UpdateValue()
{
foreach (BindingEntry entry in this.bindings)
{
if (entry.Value != PerspexProperty.UnsetValue)
{
this.SetValue(entry.Value, entry.Priority);
return;
}
}
this.SetValue(PerspexProperty.UnsetValue, int.MaxValue);
}
///
/// A registered binding.
///
public class BindingEntry : IDisposable
{
///
/// The binding subscription.
///
private IDisposable subscription;
///
/// Gets a description of the binding.
///
public string Description
{
get;
private set;
}
///
/// The priority of the binding.
///
public int Priority
{
get;
private set;
}
///
/// The current value of the binding.
///
public object Value
{
get;
private set;
}
///
/// Starts listening to the specified binding.
///
/// The binding.
/// The binding priority.
/// Called when the binding changes.
/// Called when the binding completes.
public void Start(
IObservable binding,
int priority,
Action changed,
Action completed)
{
Contract.Requires(binding != null);
Contract.Requires(changed != null);
Contract.Requires(completed != null);
if (this.subscription != null)
{
throw new Exception("PriorityValue.Entry.Start() called more than once.");
}
this.Priority = priority;
this.Value = PerspexProperty.UnsetValue;
if (binding is IDescription)
{
this.Description = ((IDescription)binding).Description;
}
this.subscription = binding.Subscribe(
value =>
{
this.Value = value;
changed(this);
},
() => completed(this));
}
///
/// Ends the binding subscription.
///
public void Dispose()
{
if (this.subscription != null)
{
this.subscription.Dispose();
}
}
}
}
}