// -----------------------------------------------------------------------
//
// 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;
///
/// 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 coerce the value, if any.
///
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.
///
///
/// 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;
}
///
/// 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)
{
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 ? "*" : "");
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 ? "*" : "");
b.Append(binding.Description ?? binding.Observable.GetType().Name);
b.Append(": ");
b.AppendLine(binding.Value.ToString());
}
first = false;
}
return b.ToString();
}
///
/// Causes a re-coercion of the value.
///
public void Coerce()
{
if (this.coerce != 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)
{
this.VerifyValidValue(value);
var old = this.value;
if (this.coerce != null)
{
value = this.coerce(value);
}
this.ValuePriority = priority;
this.value = value;
this.changed.OnNext(Tuple.Create(old, this.value));
}
///
/// 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 the value for a priority level changes.
///
/// 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);
}
}
}
}
}