// ----------------------------------------------------------------------- // // Copyright 2014 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex { using System; using System.Collections.Generic; using System.Linq; using System.Reactive.Subjects; using System.Reflection; using System.Text; using Perspex.Utilities; /// /// 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, the observable is /// fired with the old and new values. /// internal class PriorityValue { /// /// The name of the property. /// private string name; /// /// The value type. /// private Type valueType; /// /// The currently registered bindings organised by priority. /// private Dictionary levels = new Dictionary(); /// /// The changed observable. /// private Subject> changed = new Subject>(); /// /// The current value. /// private object value; /// /// The function used to validate the value, if any. /// private Func validate; /// /// Initializes a new instance of the class. /// /// The name of the property. /// The value type. /// An optional validation function. public PriorityValue(string name, Type valueType, Func validate = null) { this.name = name; this.valueType = valueType; this.value = PerspexProperty.UnsetValue; this.ValuePriority = int.MaxValue; this.validate = validate; } /// /// Fired whenever the current changes. /// /// /// The old and new values may be the same, this class does not check for distinct values. /// 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; } /// /// Adds a new binding. /// /// The binding. /// The binding priority. /// /// A disposable that will remove the binding. /// public IDisposable Add(IObservable binding, int priority) { return this.GetLevel(priority).Add(binding); } /// /// Sets the direct value for a specified priority. /// /// The value. /// The priority public void SetDirectValue(object value, int priority) { this.GetLevel(priority).DirectValue = value; } /// /// Gets the currently active bindings on this object. /// /// An enumerable collection of bindings. public IEnumerable GetBindings() { foreach (var level in this.levels) { foreach (var binding in level.Value.Bindings) { yield return binding; } } } /// /// Returns diagnostic string that can help the user debug the bindings in effect on /// this object. /// /// A diagnostic string. public string GetDiagnostic() { var b = new StringBuilder(); var first = true; foreach (var level in this.levels) { if (!first) { b.AppendLine(); } b.Append(this.ValuePriority == level.Key ? "*" : string.Empty); b.Append("Priority "); b.Append(level.Key); b.Append(": "); b.AppendLine(level.Value.Value?.ToString() ?? "(null)"); b.AppendLine("--------"); b.Append("Direct: "); b.AppendLine(level.Value.DirectValue?.ToString() ?? "(null)"); foreach (var binding in level.Value.Bindings) { b.Append(level.Value.ActiveBindingIndex == binding.Index ? "*" : string.Empty); b.Append(binding.Description ?? binding.Observable.GetType().Name); b.Append(": "); b.AppendLine(binding.Value?.ToString() ?? "(null)"); } first = false; } return b.ToString(); } /// /// Causes a revalidation of the value. /// public void Revalidate() { if (this.validate != null) { PriorityLevel level; if (this.levels.TryGetValue(this.ValuePriority, out level)) { this.UpdateValue(level.Value, level.Priority); } } } /// /// Gets the with the specified priority, creating it if it /// doesn't already exist. /// /// The priority. /// The priority level. private PriorityLevel GetLevel(int priority) { PriorityLevel result; if (!this.levels.TryGetValue(priority, out result)) { var mode = (LevelPrecedenceMode)(priority % 2); result = new PriorityLevel(priority, mode, this.ValueChanged); this.levels.Add(priority, result); } return result; } /// /// Updates the current and notifies all subscibers. /// /// The value to set. /// The priority level that the value came from. private void UpdateValue(object value, int priority) { if (!TypeUtilities.TryCast(this.valueType, value, out value)) { throw new InvalidOperationException(string.Format( "Invalid value for Property '{0}': {1} ({2})", this.name, value, value?.GetType().FullName ?? "(null)")); } var old = this.value; if (this.validate != null) { value = this.validate(value); } this.ValuePriority = priority; this.value = value; this.changed.OnNext(Tuple.Create(old, this.value)); } /// /// Called when the value for a priority level changes. /// /// The priority level of the changed entry. private void ValueChanged(PriorityLevel level) { if (level.Priority <= this.ValuePriority) { if (level.Value != PerspexProperty.UnsetValue) { this.UpdateValue(level.Value, level.Priority); } else { foreach (var i in this.levels.Values.OrderBy(x => x.Priority)) { if (i.Value != PerspexProperty.UnsetValue) { this.UpdateValue(i.Value, i.Priority); return; } } this.UpdateValue(PerspexProperty.UnsetValue, int.MaxValue); } } } } }