diff --git a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs index 190524402a..b7f1f08e89 100644 --- a/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs +++ b/src/Avalonia.Base/Animation/AnimatorKeyFrame.cs @@ -1,6 +1,7 @@ using System; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; +using System.Text; using Avalonia.Animation.Animators; using Avalonia.Data; using Avalonia.Reactive; @@ -75,5 +76,13 @@ namespace Avalonia.Animation return (T)typeConv.ConvertTo(Value, typeof(T))!; } + + internal override void BuildDebugDisplay(StringBuilder builder, bool includeContent) + { + base.BuildDebugDisplay(builder, includeContent); + DebugDisplayHelper.AppendOptionalValue(builder, nameof(Property), Property, includeContent); + DebugDisplayHelper.AppendOptionalValue(builder, nameof(Cue), Cue, includeContent); + DebugDisplayHelper.AppendOptionalValue(builder, nameof(Value), Value, includeContent); + } } } diff --git a/src/Avalonia.Base/Animation/Animators/TransformOperationsAnimator.cs b/src/Avalonia.Base/Animation/Animators/TransformOperationsAnimator.cs index bf88da04c1..b0d7484a67 100644 --- a/src/Avalonia.Base/Animation/Animators/TransformOperationsAnimator.cs +++ b/src/Avalonia.Base/Animation/Animators/TransformOperationsAnimator.cs @@ -1,14 +1,15 @@ using System; +using Avalonia.Collections; using Avalonia.Media; using Avalonia.Media.Transformation; namespace Avalonia.Animation.Animators { - internal class TransformOperationsAnimator : Animator + internal class TransformOperationsAnimator : Animator, IAvaloniaListItemValidator { public TransformOperationsAnimator() { - Validate = ValidateTransform; + Validator = this; } public override TransformOperations Interpolate(double progress, TransformOperations oldValue, TransformOperations newValue) @@ -24,11 +25,11 @@ namespace Avalonia.Animation.Animators return value as TransformOperations ?? TransformOperations.Identity; } - private void ValidateTransform(AnimatorKeyFrame kf) + void IAvaloniaListItemValidator.Validate(AnimatorKeyFrame item) { - if (!(kf.Value is TransformOperations)) + if (item.Value is not TransformOperations) { - throw new InvalidOperationException($"All keyframes must be of type {typeof(TransformOperations)}."); + throw new InvalidOperationException($"{item.DebugDisplay} must have a value of type {typeof(TransformOperations)}."); } } } diff --git a/src/Avalonia.Base/Animation/KeyFrame.cs b/src/Avalonia.Base/Animation/KeyFrame.cs index 9188167d0e..6a196ad88d 100644 --- a/src/Avalonia.Base/Animation/KeyFrame.cs +++ b/src/Avalonia.Base/Animation/KeyFrame.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text; using Avalonia.Collections; using Avalonia.Metadata; @@ -95,6 +96,20 @@ namespace Avalonia.Animation } } + internal override void BuildDebugDisplay(StringBuilder builder, bool includeContent) + { + base.BuildDebugDisplay(builder, includeContent); + + switch (TimingMode) + { + case KeyFrameTimingMode.TimeSpan: + DebugDisplayHelper.AppendOptionalValue(builder, nameof(KeyTime), KeyTime, includeContent); + break; + case KeyFrameTimingMode.Cue: + DebugDisplayHelper.AppendOptionalValue(builder, nameof(Cue), Cue, includeContent); + break; + } + } } } diff --git a/src/Avalonia.Base/Animation/TransitionBase.cs b/src/Avalonia.Base/Animation/TransitionBase.cs index e5ae073779..86d997a7d0 100644 --- a/src/Avalonia.Base/Animation/TransitionBase.cs +++ b/src/Avalonia.Base/Animation/TransitionBase.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Text; using Avalonia.Animation.Easings; namespace Avalonia.Animation @@ -96,5 +97,12 @@ namespace Avalonia.Animation => Apply(control, clock, oldValue, newValue); internal abstract IDisposable Apply(Animatable control, IClock clock, object? oldValue, object? newValue); + + internal override void BuildDebugDisplay(StringBuilder builder, bool includeContent) + { + base.BuildDebugDisplay(builder, includeContent); + DebugDisplayHelper.AppendOptionalValue(builder, nameof(Property), Property, includeContent); + DebugDisplayHelper.AppendOptionalValue(builder, nameof(Duration), Duration, includeContent); + } } } diff --git a/src/Avalonia.Base/Animation/Transitions.cs b/src/Avalonia.Base/Animation/Transitions.cs index d94ecdc886..bea5120f78 100644 --- a/src/Avalonia.Base/Animation/Transitions.cs +++ b/src/Avalonia.Base/Animation/Transitions.cs @@ -7,7 +7,7 @@ namespace Avalonia.Animation /// /// A collection of definitions. /// - public sealed class Transitions : AvaloniaList + public sealed class Transitions : AvaloniaList, IAvaloniaListItemValidator { /// /// Initializes a new instance of the class. @@ -15,16 +15,18 @@ namespace Avalonia.Animation public Transitions() { ResetBehavior = ResetBehavior.Remove; - Validate = ValidateTransition; + Validator = this; } - private void ValidateTransition(ITransition obj) + void IAvaloniaListItemValidator.Validate(ITransition item) { Dispatcher.UIThread.VerifyAccess(); - if (obj.Property.IsDirect) + var property = item.Property; + if (property.IsDirect) { - throw new InvalidOperationException("Cannot animate a direct property."); + var display = item is TransitionBase transition ? transition.DebugDisplay : item.ToString(); + throw new InvalidOperationException($"Cannot animate direct property {property} on {display}."); } } } diff --git a/src/Avalonia.Base/AvaloniaObject.cs b/src/Avalonia.Base/AvaloniaObject.cs index ee036b904f..28fa4d5d0c 100644 --- a/src/Avalonia.Base/AvaloniaObject.cs +++ b/src/Avalonia.Base/AvaloniaObject.cs @@ -1,7 +1,9 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.Runtime.CompilerServices; +using System.Text; using Avalonia.Data; using Avalonia.Data.Core; using Avalonia.Diagnostics; @@ -18,6 +20,7 @@ namespace Avalonia /// /// This class is analogous to DependencyObject in WPF. /// + [DebuggerDisplay("{DebugDisplay}")] public class AvaloniaObject : IAvaloniaObjectDebug, INotifyPropertyChanged { private readonly ValueStore _values; @@ -101,6 +104,11 @@ namespace Avalonia set { this.Bind(binding.Property!, value); } } + /// + /// Gets a string to display inside the debugger for this object. + /// + internal string DebugDisplay => GetDebugDisplay(true); + /// /// Returns a value indicating whether the current thread is the UI thread. /// @@ -860,6 +868,28 @@ namespace Avalonia priority); } + internal string GetDebugDisplay(bool includeContent) + { + var builder = new StringBuilder(); + BuildDebugDisplay(builder, includeContent); + return builder.ToString(); + } + + internal virtual void BuildDebugDisplay(StringBuilder builder, bool includeContent) + { + var type = GetType(); + + if (type.Namespace is { } ns && + (ns == "Avalonia" || ns.StartsWith("Avalonia.", StringComparison.Ordinal))) + { + builder.Append(type.Name); + } + else + { + builder.Append(ToString()); + } + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ValidatePriority(BindingPriority priority) { diff --git a/src/Avalonia.Base/Collections/AvaloniaList.cs b/src/Avalonia.Base/Collections/AvaloniaList.cs index 47b67459e1..84a77810da 100644 --- a/src/Avalonia.Base/Collections/AvaloniaList.cs +++ b/src/Avalonia.Base/Collections/AvaloniaList.cs @@ -115,7 +115,34 @@ namespace Avalonia.Collections /// Gets or sets a validation routine that can be used to validate items before they are /// added. /// - public Action? Validate { get; set; } + public Action? Validate + { + get => Validator switch + { + null => null, + ItemValidator itemValidator => itemValidator.Validate, + { } other => other.Validate + }; + set + { + if (value is null) + { + Validator = null; + return; + } + + if (Validator is ItemValidator itemValidator) + { + itemValidator.Validate = value; + } + else + { + Validator = new ItemValidator(value); + } + } + } + + internal IAvaloniaListItemValidator? Validator { get; set; } /// bool IList.IsFixedSize => false; @@ -149,7 +176,7 @@ namespace Avalonia.Collections set { - Validate?.Invoke(value); + Validator?.Validate(value); T old = _inner[index]; @@ -196,7 +223,7 @@ namespace Avalonia.Collections /// The item. public virtual void Add(T item) { - Validate?.Invoke(item); + Validator?.Validate(item); int index = _inner.Count; _inner.Add(item); NotifyAdd(item, index); @@ -303,7 +330,7 @@ namespace Avalonia.Collections /// The item. public virtual void Insert(int index, T item) { - Validate?.Invoke(item); + Validator?.Validate(item); _inner.Insert(index, item); NotifyAdd(item, index); } @@ -318,7 +345,7 @@ namespace Avalonia.Collections _ = items ?? throw new ArgumentNullException(nameof(items)); bool willRaiseCollectionChanged = _collectionChanged != null; - bool hasValidation = Validate != null; + bool hasValidation = Validator is not null; if (items is IList list) { @@ -330,7 +357,7 @@ namespace Avalonia.Collections { foreach (T item in collection) { - Validate!(item); + Validator!.Validate(item); } } @@ -351,7 +378,7 @@ namespace Avalonia.Collections if (hasValidation) { - Validate!(item); + Validator!.Validate(item); } _inner.Insert(insertIndex++, item); @@ -381,7 +408,7 @@ namespace Avalonia.Collections if (hasValidation) { - Validate!(item); + Validator!.Validate(item); } _inner.Insert(insertIndex++, item); @@ -772,6 +799,17 @@ namespace Avalonia.Collections _innerEnumerator.Dispose(); } } + + private sealed class ItemValidator : IAvaloniaListItemValidator + { + public ItemValidator(Action validate) + => Validate = validate; + + public Action Validate { get; set; } + + void IAvaloniaListItemValidator.Validate(T item) + => Validate(item); + } } internal static class EventArgsCache diff --git a/src/Avalonia.Base/Collections/IAvaloniaListItemValidator.cs b/src/Avalonia.Base/Collections/IAvaloniaListItemValidator.cs new file mode 100644 index 0000000000..3b4eb5730c --- /dev/null +++ b/src/Avalonia.Base/Collections/IAvaloniaListItemValidator.cs @@ -0,0 +1,6 @@ +namespace Avalonia.Collections; + +internal interface IAvaloniaListItemValidator +{ + void Validate(T item); +} diff --git a/src/Avalonia.Base/Diagnostics/DebugDisplayHelper.cs b/src/Avalonia.Base/Diagnostics/DebugDisplayHelper.cs new file mode 100644 index 0000000000..27c41e8000 --- /dev/null +++ b/src/Avalonia.Base/Diagnostics/DebugDisplayHelper.cs @@ -0,0 +1,49 @@ +using System.Text; + +namespace Avalonia; + +internal static class DebugDisplayHelper +{ + public static void AppendOptionalValue(StringBuilder builder, string name, object? value, bool includeContent) + { + const int maxValueLength = 50; + + if (value is null or string { Length: 0 }) + { + return; + } + + if (builder.Length > 0 && builder[builder.Length - 1] == ')') + { + --builder.Length; + builder.Append(", "); + } + else + { + builder.Append(" ("); + } + + builder.Append(name); + builder.Append(" = "); + + if (value is AvaloniaObject avaloniaObject) + { + avaloniaObject.BuildDebugDisplay(builder, includeContent); + } + else + { + var stringValue = value.ToString(); + if (stringValue?.Length > maxValueLength) + { + builder.Append(stringValue, 0, maxValueLength - 1); + builder.Append('…'); + } + else + { + builder.Append(stringValue); + } + } + + builder.Append(')'); + } +} diff --git a/src/Avalonia.Base/StyledElement.cs b/src/Avalonia.Base/StyledElement.cs index 7f99b6eac2..07ea2f69bb 100644 --- a/src/Avalonia.Base/StyledElement.cs +++ b/src/Avalonia.Base/StyledElement.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics; -using System.Linq; +using System.Text; using Avalonia.Animation; using Avalonia.Collections; using Avalonia.Controls; @@ -34,6 +34,7 @@ namespace Avalonia ISetInheritanceParent, ISupportInitialize, INamed, + IAvaloniaListItemValidator, #pragma warning disable CS0618 // Type or member is obsolete IStyleable #pragma warning restore CS0618 // Type or member is obsolete @@ -277,7 +278,7 @@ namespace Avalonia var list = new AvaloniaList { ResetBehavior = ResetBehavior.Remove, - Validate = logical => ValidateLogicalChild(logical) + Validator = this }; list.CollectionChanged += LogicalChildrenCollectionChanged; _logicalChildren = list; @@ -787,11 +788,11 @@ namespace Avalonia return null; } - private static void ValidateLogicalChild(ILogical c) + void IAvaloniaListItemValidator.Validate(ILogical item) { - if (c == null) + if (item is null) { - throw new ArgumentException("Cannot add null to LogicalChildren."); + throw new ArgumentException($"Cannot add null to {nameof(LogicalChildren)}."); } } @@ -1018,6 +1019,12 @@ namespace Avalonia } } + internal override void BuildDebugDisplay(StringBuilder builder, bool includeContent) + { + base.BuildDebugDisplay(builder, includeContent); + DebugDisplayHelper.AppendOptionalValue(builder, nameof(Name), Name, includeContent); + } + private static IReadOnlyList