// 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 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
/// method on the
/// owner object is fired with the old and new values.
///
internal class PriorityValue
{
private readonly IPriorityValueOwner _owner;
private readonly Type _valueType;
private readonly Dictionary _levels = new Dictionary();
private object _value;
private readonly Func _validate;
///
/// 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;
ValuePriority = int.MaxValue;
_validate = validate;
}
///
/// Gets the property that the value represents.
///
public AvaloniaProperty Property { get; }
///
/// Gets the current value.
///
public object Value => _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 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)
{
Logger.Log(
LogEventLevel.Error,
LogArea.Binding,
_owner,
"Error binding to {Target}.{Property}: {Message}",
_owner,
Property,
error.Error.Message);
}
///
/// 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 subscibers.
///
/// The value to set.
/// The priority level that the value came from.
private void UpdateValue(object value, int priority)
{
var notification = value as BindingNotification;
object castValue;
if (notification != null)
{
value = (notification.HasValue) ? notification.Value : null;
}
if (TypeUtilities.TryCast(_valueType, value, out castValue))
{
var old = _value;
if (_validate != null && castValue != AvaloniaProperty.UnsetValue)
{
castValue = _validate(castValue);
}
ValuePriority = priority;
_value = castValue;
if (notification?.HasValue == true)
{
notification.SetValue(castValue);
}
if (notification == null || notification.HasValue)
{
_owner?.Changed(this, old, _value);
}
if (notification != null)
{
_owner?.BindingNotificationReceived(this, notification);
}
}
else
{
Logger.Error(
LogArea.Binding,
_owner,
"Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
Property.Name,
_valueType,
value,
value.GetType());
}
}
}
}