// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Avalonia.Data;
using Avalonia.Logging;
using Avalonia.Utilities;
namespace Avalonia
{
///
/// Maintains a list of prioritized 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 priorities. 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
/// method on the
/// owner object is fired with the old and new values.
///
internal class PriorityValue
{
private readonly Type _valueType;
private readonly SingleOrDictionary _levels = new SingleOrDictionary();
private readonly Func _validate;
private (object value, int priority) _value;
///
/// Initializes a new instance of the class.
///
/// The owner of the object.
/// The property that the value represents.
/// The value type.
/// An optional validation function.
public PriorityValue(
IPriorityValueOwner owner,
AvaloniaProperty property,
Type valueType,
Func validate = null)
{
Owner = owner;
Property = property;
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
}
///
/// Gets a value indicating whether the property is animating.
///
public bool IsAnimating
{
get
{
return ValuePriority <= (int)BindingPriority.Animation &&
GetLevel(ValuePriority).ActiveBindingIndex != -1;
}
}
///
/// Gets the owner of the value.
///
public IPriorityValueOwner Owner { get; }
///
/// Gets the property that the value represents.
///
public AvaloniaProperty Property { get; }
///
/// Gets the current value.
///
public object Value => _value.value;
///
/// Gets the priority of the binding that is currently active.
///
public int ValuePriority => _value.priority;
///
/// Adds a new binding.
///
/// The binding.
/// The binding priority.
///
/// A disposable that will remove the binding.
///
public IDisposable Add(IObservable binding, int priority)
{
return GetLevel(priority).Add(binding);
}
///
/// Sets the value for a specified priority.
///
/// The value.
/// The priority
public void SetValue(object value, int priority)
{
GetLevel(priority).DirectValue = value;
}
///
/// Gets the currently active bindings on this object.
///
/// An enumerable collection of bindings.
public IEnumerable GetBindings()
{
foreach (var level in _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 _levels)
{
if (!first)
{
b.AppendLine();
}
b.Append(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();
}
///
/// Called when the value for a priority level changes.
///
/// The priority level of the changed entry.
public void LevelValueChanged(PriorityLevel level)
{
if (level.Priority <= ValuePriority)
{
if (level.Value != AvaloniaProperty.UnsetValue)
{
UpdateValue(level.Value, level.Priority);
}
else
{
foreach (var i in _levels.Values.OrderBy(x => x.Priority))
{
if (i.Value != AvaloniaProperty.UnsetValue)
{
UpdateValue(i.Value, i.Priority);
return;
}
}
UpdateValue(AvaloniaProperty.UnsetValue, int.MaxValue);
}
}
}
///
/// Called when a priority level encounters an error.
///
/// The priority level of the changed entry.
/// The binding error.
public void LevelError(PriorityLevel level, BindingNotification error)
{
error.LogIfError(Owner, Property);
}
///
/// Causes a revalidation of the value.
///
public void Revalidate()
{
if (_validate != null)
{
PriorityLevel level;
if (_levels.TryGetValue(ValuePriority, out level))
{
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 (!_levels.TryGetValue(priority, out result))
{
result = new PriorityLevel(this, priority);
_levels.Add(priority, result);
}
return result;
}
///
/// Updates the current and notifies all subscribers.
///
/// The value to set.
/// The priority level that the value came from.
private void UpdateValue(object value, int priority)
{
Owner.Setter.SetAndNotify(Property,
ref _value,
UpdateCore,
(value, priority));
}
private bool UpdateCore(
object update,
ref (object value, int priority) backing,
Action notify)
=> UpdateCore(((object, int))update, ref backing, notify);
private bool UpdateCore(
(object value, int priority) update,
ref (object value, int priority) backing,
Action notify)
{
var val = update.value;
var notification = val as BindingNotification;
object castValue;
if (notification != null)
{
val = (notification.HasValue) ? notification.Value : null;
}
if (TypeUtilities.TryConvertImplicit(_valueType, val, out castValue))
{
var old = backing.value;
if (_validate != null && castValue != AvaloniaProperty.UnsetValue)
{
castValue = _validate(castValue);
}
backing = (castValue, update.priority);
if (notification?.HasValue == true)
{
notification.SetValue(castValue);
}
if (notification == null || notification.HasValue)
{
notify(() => Owner?.Changed(Property, ValuePriority, old, Value));
}
if (notification != null)
{
Owner?.BindingNotificationReceived(Property, notification);
}
}
else
{
Logger.Error(
LogArea.Binding,
Owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
Property.Name,
_valueType,
val,
val?.GetType());
}
return true;
}
}
}