// 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; } } }