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