A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

313 lines
11 KiB

// 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
{
/// <summary>
/// Maintains a list of prioritized bindings together with a current value.
/// </summary>
/// <remarks>
/// Bindings, in the form of <see cref="IObservable{Object}"/>s are added to the object using
/// the <see cref="Add"/> method. With the observable is passed a priority, where lower values
/// represent higher priorities. The current <see cref="Value"/> is selected from the highest
/// priority binding that doesn't return <see cref="AvaloniaProperty.UnsetValue"/>. 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
/// <see cref="IPriorityValueOwner.Changed"/> method on the
/// owner object is fired with the old and new values.
/// </remarks>
internal class PriorityValue
{
private readonly Type _valueType;
private readonly SingleOrDictionary<int, PriorityLevel> _levels = new SingleOrDictionary<int, PriorityLevel>();
private readonly Func<object, object> _validate;
private readonly SetAndNotifyCallback<(object, int)> _setAndNotifyCallback;
private (object value, int priority) _value;
private DeferredSetter<object> _setter;
/// <summary>
/// Initializes a new instance of the <see cref="PriorityValue"/> class.
/// </summary>
/// <param name="owner">The owner of the object.</param>
/// <param name="property">The property that the value represents.</param>
/// <param name="valueType">The value type.</param>
/// <param name="validate">An optional validation function.</param>
public PriorityValue(
IPriorityValueOwner owner,
AvaloniaProperty property,
Type valueType,
Func<object, object> validate = null)
{
Owner = owner;
Property = property;
_valueType = valueType;
_value = (AvaloniaProperty.UnsetValue, int.MaxValue);
_validate = validate;
_setAndNotifyCallback = SetAndNotify;
}
/// <summary>
/// Gets a value indicating whether the property is animating.
/// </summary>
public bool IsAnimating
{
get
{
return ValuePriority <= (int)BindingPriority.Animation &&
GetLevel(ValuePriority).ActiveBindingIndex != -1;
}
}
/// <summary>
/// Gets the owner of the value.
/// </summary>
public IPriorityValueOwner Owner { get; }
/// <summary>
/// Gets the property that the value represents.
/// </summary>
public AvaloniaProperty Property { get; }
/// <summary>
/// Gets the current value.
/// </summary>
public object Value => _value.value;
/// <summary>
/// Gets the priority of the binding that is currently active.
/// </summary>
public int ValuePriority => _value.priority;
/// <summary>
/// Adds a new binding.
/// </summary>
/// <param name="binding">The binding.</param>
/// <param name="priority">The binding priority.</param>
/// <returns>
/// A disposable that will remove the binding.
/// </returns>
public IDisposable Add(IObservable<object> binding, int priority)
{
return GetLevel(priority).Add(binding);
}
/// <summary>
/// Sets the value for a specified priority.
/// </summary>
/// <param name="value">The value.</param>
/// <param name="priority">The priority</param>
public void SetValue(object value, int priority)
{
GetLevel(priority).DirectValue = value;
}
/// <summary>
/// Gets the currently active bindings on this object.
/// </summary>
/// <returns>An enumerable collection of bindings.</returns>
public IEnumerable<PriorityBindingEntry> GetBindings()
{
foreach (var level in _levels)
{
foreach (var binding in level.Value.Bindings)
{
yield return binding;
}
}
}
/// <summary>
/// Returns diagnostic string that can help the user debug the bindings in effect on
/// this object.
/// </summary>
/// <returns>A diagnostic string.</returns>
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();
}
/// <summary>
/// Called when the value for a priority level changes.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
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);
}
}
}
/// <summary>
/// Called when a priority level encounters an error.
/// </summary>
/// <param name="level">The priority level of the changed entry.</param>
/// <param name="error">The binding error.</param>
public void LevelError(PriorityLevel level, BindingNotification error)
{
Owner.LogError(Property, error.Error);
}
/// <summary>
/// Causes a revalidation of the value.
/// </summary>
public void Revalidate()
{
if (_validate != null)
{
PriorityLevel level;
if (_levels.TryGetValue(ValuePriority, out level))
{
UpdateValue(level.Value, level.Priority);
}
}
}
/// <summary>
/// Gets the <see cref="PriorityLevel"/> with the specified priority, creating it if it
/// doesn't already exist.
/// </summary>
/// <param name="priority">The priority.</param>
/// <returns>The priority level.</returns>
private PriorityLevel GetLevel(int priority)
{
PriorityLevel result;
if (!_levels.TryGetValue(priority, out result))
{
result = new PriorityLevel(this, priority);
_levels.Add(priority, result);
}
return result;
}
/// <summary>
/// Updates the current <see cref="Value"/> and notifies all subscribers.
/// </summary>
/// <param name="value">The value to set.</param>
/// <param name="priority">The priority level that the value came from.</param>
private void UpdateValue(object value, int priority)
{
var newValue = (value, priority);
if (newValue == _value)
{
return;
}
if (_setter == null)
{
_setter = Owner.GetNonDirectDeferredSetter(Property);
}
_setter.SetAndNotifyCallback(Property, _setAndNotifyCallback, ref _value, newValue);
}
private void SetAndNotify(AvaloniaProperty property, ref (object value, int priority) backing, (object value, int priority) update)
{
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)
{
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());
}
}
}
}