// ----------------------------------------------------------------------- // // 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; /// /// 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 currently registered binding entries. /// private LinkedList bindings = new LinkedList(); /// /// The changed observable. /// private Subject> changed = new Subject>(); /// /// The current value. /// private object value; /// /// The priority of the binding that is currently active. /// private int valuePriority = int.MaxValue; /// /// Initializes a new instance of the class. /// public PriorityValue() { this.value = PerspexProperty.UnsetValue; } /// /// 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; } } /// /// 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); }); } /// /// 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; } /// /// Called when an binding's value changes. /// /// The changed entry. private void EntryChanged(BindingEntry changed) { if (changed.Priority <= this.valuePriority) { this.UpdateValue(); } } /// /// Called when an 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) { 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 IObservableDescription) { this.Description = ((IObservableDescription)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(); } } } } }