Browse Source

Improve DevTools previewer of setter values by including all value priorities (#13802)

* Replace "Active Styles" dev tools analysis with "Active Value Frames"

* Remove old diagnostic methods from public API (breaking change)

* Show full style selectors

* Avoid unnecessary value setters by checking if color was actually changed

* Run UpdateStyles from the dispatcher to fix flickering issue

* Fix build
Max Katz 1 year ago
parent
commit
6938183319

+ 18 - 0
api/Avalonia.nupkg.xml

@@ -1015,6 +1015,18 @@
     <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0001</DiagnosticId>
+    <Target>T:Avalonia.Diagnostics.AppliedStyle</Target>
+    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
+    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
+  </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0001</DiagnosticId>
+    <Target>T:Avalonia.Diagnostics.StyleDiagnostics</Target>
+    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
+    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
+  </Suppression>
   <Suppression>
     <DiagnosticId>CP0001</DiagnosticId>
     <Target>T:Avalonia.Input.FocusManager.&lt;GetFocusScopeAncestors&gt;d__18</Target>
@@ -1075,6 +1087,12 @@
     <Left>baseline/netstandard2.0/Avalonia.Dialogs.dll</Left>
     <Right>target/netstandard2.0/Avalonia.Dialogs.dll</Right>
   </Suppression>
+  <Suppression>
+    <DiagnosticId>CP0002</DiagnosticId>
+    <Target>M:Avalonia.Diagnostics.StyledElementExtensions.GetStyleDiagnostics(Avalonia.StyledElement)</Target>
+    <Left>baseline/netstandard2.0/Avalonia.Base.dll</Left>
+    <Right>target/netstandard2.0/Avalonia.Base.dll</Right>
+  </Suppression>
   <Suppression>
     <DiagnosticId>CP0006</DiagnosticId>
     <Target>M:Avalonia.Platform.IAssetLoader.InvalidateAssemblyCache</Target>

+ 0 - 18
src/Avalonia.Base/Diagnostics/AppliedStyle.cs

@@ -1,18 +0,0 @@
-using Avalonia.Styling;
-
-namespace Avalonia.Diagnostics
-{
-    public sealed class AppliedStyle
-    {
-        private readonly IStyleInstance _instance;
-
-        internal AppliedStyle(IStyleInstance instance)
-        {
-            _instance = instance;
-        }
-
-        public bool HasActivator => _instance.HasActivator;
-        public bool IsActive => _instance.IsActive;
-        public StyleBase Style => (StyleBase)_instance.Source;
-    }
-}

+ 27 - 0
src/Avalonia.Base/Diagnostics/IValueFrameDiagnostic.cs

@@ -0,0 +1,27 @@
+using System.Collections.Generic;
+using Avalonia.Data;
+using Avalonia.Metadata;
+
+namespace Avalonia.Diagnostics;
+
+public record ValueEntryDiagnostic(AvaloniaProperty Property, object? Value);
+
+[Unstable]
+[NotClientImplementable]
+public interface IValueFrameDiagnostic
+{
+    public enum FrameType
+    {
+        Unknown = 0,
+        Local,
+        Theme,
+        Style,
+        Template
+    }
+    
+    string? Description { get; }
+    FrameType Type { get; }
+    bool IsActive { get; }
+    BindingPriority Priority { get; }
+    IEnumerable<ValueEntryDiagnostic> Values { get; } 
+}

+ 18 - 0
src/Avalonia.Base/Diagnostics/LocalValueFrameDiagnostic.cs

@@ -0,0 +1,18 @@
+using System.Collections.Generic;
+using Avalonia.Data;
+
+namespace Avalonia.Diagnostics;
+
+internal class LocalValueFrameDiagnostic : IValueFrameDiagnostic
+{
+    public LocalValueFrameDiagnostic(IEnumerable<ValueEntryDiagnostic> values)
+    {
+        Values = values;
+    }
+    
+    public string? Description => null;
+    public IValueFrameDiagnostic.FrameType Type => IValueFrameDiagnostic.FrameType.Local;
+    public bool IsActive => true;
+    public BindingPriority Priority => BindingPriority.LocalValue;
+    public IEnumerable<ValueEntryDiagnostic> Values { get; }
+}

+ 0 - 21
src/Avalonia.Base/Diagnostics/StyleDiagnostics.cs

@@ -1,21 +0,0 @@
-using System.Collections.Generic;
-using Avalonia.Styling;
-
-namespace Avalonia.Diagnostics
-{
-    /// <summary>
-    /// Contains information about style related diagnostics of a control.
-    /// </summary>
-    public class StyleDiagnostics
-    {
-        /// <summary>
-        /// Currently applied styles.
-        /// </summary>
-        public IReadOnlyList<AppliedStyle> AppliedStyles { get; }
-
-        public StyleDiagnostics(IReadOnlyList<AppliedStyle> appliedStyles)
-        {
-            AppliedStyles = appliedStyles;
-        }
-    }
-}

+ 63 - 0
src/Avalonia.Base/Diagnostics/StyleValueFrameDiagnostic.cs

@@ -0,0 +1,63 @@
+using System.Collections.Generic;
+using Avalonia.Data;
+using Avalonia.PropertyStore;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics;
+
+internal class StyleValueFrameDiagnostic : IValueFrameDiagnostic
+{
+    private readonly StyleInstance _styleInstance;
+
+    internal StyleValueFrameDiagnostic(StyleInstance styleInstance)
+    {
+        _styleInstance = styleInstance;
+    }
+
+    public string? Description => _styleInstance.Source switch
+    {
+        Style s => GetFullSelector(s),
+        ControlTheme t => t.TargetType?.Name,
+        _ => null
+    };
+
+    public IValueFrameDiagnostic.FrameType Type => _styleInstance.Source switch
+    {
+        Style => IValueFrameDiagnostic.FrameType.Style,
+        ControlTheme => IValueFrameDiagnostic.FrameType.Theme,
+        _ => IValueFrameDiagnostic.FrameType.Unknown
+    };
+
+    public bool IsActive => _styleInstance.IsActive();
+    public BindingPriority Priority => _styleInstance.FramePriority.ToBindingPriority();
+    public IEnumerable<ValueEntryDiagnostic> Values
+    {
+        get
+        {
+            foreach (var setter in ((StyleBase)_styleInstance.Source!).Setters)
+            {
+                if (setter is Setter { Property: not null } regularSetter)
+                {
+                    yield return new ValueEntryDiagnostic(regularSetter.Property, regularSetter.Value);
+                }
+            }
+        }
+    }
+
+    private string GetFullSelector(Style? style)
+    {
+        var selectors = new Stack<string>();
+
+        while (style is not null)
+        {
+            if (style.Selector is not null)
+            {
+                selectors.Push(style.Selector.ToString());
+            }
+            
+            style = style.Parent as Style;
+        }
+
+        return string.Concat(selectors);
+    }
+}

+ 10 - 11
src/Avalonia.Base/Diagnostics/StyledElementExtensions.cs

@@ -1,17 +1,16 @@
-namespace Avalonia.Diagnostics
+namespace Avalonia.Diagnostics;
+
+/// <summary>
+/// Defines diagnostic extensions on <see cref="StyledElement"/>s.
+/// </summary>
+public static class StyledElementExtensions
 {
     /// <summary>
-    /// Defines diagnostic extensions on <see cref="StyledElement"/>s.
+    /// Gets a style diagnostics for a <see cref="StyledElement"/>.
     /// </summary>
-    public static class StyledElementExtensions
+    /// <param name="styledElement">The element.</param>
+    public static ValueStoreDiagnostic GetValueStoreDiagnostic(this StyledElement styledElement)
     {
-        /// <summary>
-        /// Gets a style diagnostics for a <see cref="StyledElement"/>.
-        /// </summary>
-        /// <param name="styledElement">The element.</param>
-        public static StyleDiagnostics GetStyleDiagnostics(this StyledElement styledElement)
-        {
-            return styledElement.GetStyleDiagnosticsInternal();
-        }
+        return styledElement.GetValueStore().GetStoreDiagnostic();
     }
 }

+ 37 - 0
src/Avalonia.Base/Diagnostics/ValueFrameDiagnostic.cs

@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+using Avalonia.Data;
+using Avalonia.PropertyStore;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics;
+
+internal sealed class ValueFrameDiagnostic : IValueFrameDiagnostic
+{
+    private readonly ValueFrame _valueFrame;
+
+    internal ValueFrameDiagnostic(ValueFrame valueFrame)
+    {
+        _valueFrame = valueFrame;
+    }
+
+    public string? Description => (_valueFrame.Owner?.Owner as StyledElement)?.StyleKey.Name;
+
+    public IValueFrameDiagnostic.FrameType Type => IValueFrameDiagnostic.FrameType.Template;
+
+    public bool IsActive => _valueFrame.IsActive();
+    public BindingPriority Priority => _valueFrame.FramePriority.ToBindingPriority();
+    public IEnumerable<ValueEntryDiagnostic> Values
+    {
+        get
+        {
+            for (var i = 0; i < _valueFrame.EntryCount; i++)
+            {
+                var entry = _valueFrame.GetEntry(i);
+                if (entry.HasValue())
+                {
+                    yield return new ValueEntryDiagnostic(entry.Property, entry.GetValue());
+                }
+            }
+        }
+    }
+}

+ 17 - 0
src/Avalonia.Base/Diagnostics/ValueStoreDiagnostic.cs

@@ -0,0 +1,17 @@
+using System.Collections.Generic;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics;
+
+public class ValueStoreDiagnostic
+{
+    /// <summary>
+    /// Currently applied frames.
+    /// </summary>
+    public IReadOnlyList<IValueFrameDiagnostic> AppliedFrames { get; }
+
+    internal ValueStoreDiagnostic(IReadOnlyList<IValueFrameDiagnostic> appliedFrames)
+    {
+        AppliedFrames = appliedFrames;
+    }
+}

+ 6 - 0
src/Avalonia.Base/PropertyStore/FramePriority.cs

@@ -28,6 +28,12 @@ namespace Avalonia.PropertyStore
             return (FramePriority)(p * 3 + (int)type);
         }
 
+        public static BindingPriority ToBindingPriority(this FramePriority priority)
+        {
+            var p = (int)priority / 3;
+            return p == 0 ? BindingPriority.Animation : (BindingPriority)p;
+        }
+
         public static bool IsType(this FramePriority priority, FrameType type)
         {
             return (FrameType)((int)priority % 3) == type;

+ 34 - 0
src/Avalonia.Base/PropertyStore/ValueStore.cs

@@ -836,6 +836,40 @@ namespace Avalonia.PropertyStore
             }
         }
 
+        public ValueStoreDiagnostic GetStoreDiagnostic()
+        {
+            var frames = new List<IValueFrameDiagnostic>();
+
+            var effectiveLocalValues = new List<ValueEntryDiagnostic>(_effectiveValues.Count);
+            for (var i = 0; i < _effectiveValues.Count; i++)
+            {
+                if (_effectiveValues.GetValue(i) is { } effectiveValue
+                    && effectiveValue.Priority == BindingPriority.LocalValue)
+                {
+                    effectiveLocalValues.Add(new ValueEntryDiagnostic(effectiveValue.Property, effectiveValue.Value));
+                }
+            }
+
+            if (effectiveLocalValues.Count > 0)
+            {
+                frames.Add(new LocalValueFrameDiagnostic(effectiveLocalValues));
+            }
+
+            foreach (var frame in Frames)
+            {
+                if (frame is StyleInstance { Source: StyleBase } styleInstance)
+                {
+                    frames.Add(new StyleValueFrameDiagnostic(styleInstance));
+                }
+                else
+                {
+                    frames.Add(new ValueFrameDiagnostic(frame));   
+                }
+            }
+
+            return new ValueStoreDiagnostic(frames);
+		}
+
         private int InsertFrame(ValueFrame frame)
         {
             Debug.Assert(!_frames.Contains(frame));

+ 0 - 13
src/Avalonia.Base/StyledElement.cs

@@ -420,19 +420,6 @@ namespace Avalonia
             }
         }
 
-        internal StyleDiagnostics GetStyleDiagnosticsInternal()
-        {
-            var styles = new List<AppliedStyle>();
-
-            foreach (var frame in GetValueStore().Frames)
-            {
-                if (frame is IStyleInstance style)
-                    styles.Add(new(style));
-            }
-
-            return new StyleDiagnostics(styles);
-        }
-
         /// <inheritdoc/>
         void ILogical.NotifyAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
         {

+ 8 - 1
src/Avalonia.Diagnostics/Diagnostics/Controls/BrushEditor.axaml.cs

@@ -23,7 +23,14 @@ namespace Avalonia.Diagnostics.Controls
         public BrushEditor()
         {
             FlyoutBase.SetAttachedFlyout(this, new Flyout { Content = _colorView });
-            _colorView.ColorChanged += (_, e) => Brush = new ImmutableSolidColorBrush(e.NewColor);
+            _colorView.ColorChanged += (_, e) =>
+            {
+                // Avoid unnecessary value setters by checking if color was actually changed.
+                if (Brush is null || Brush is not ISolidColorBrush oldSolidBrush || oldSolidBrush.Color != e.NewColor)
+                {
+                    Brush = new ImmutableSolidColorBrush(e.NewColor);
+                }
+            };
             clearHandler = (s, e) => Brush = default;
         }
 

+ 30 - 109
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -11,6 +11,7 @@ using Avalonia.Controls.Metadata;
 using Avalonia.Data;
 using Avalonia.Markup.Xaml.MarkupExtensions;
 using Avalonia.Styling;
+using Avalonia.Threading;
 
 namespace Avalonia.Diagnostics.ViewModels
 {
@@ -21,9 +22,9 @@ namespace Avalonia.Diagnostics.ViewModels
         private IDictionary<object, PropertyViewModel[]>? _propertyIndex;
         private PropertyViewModel? _selectedProperty;
         private DataGridCollectionView? _propertiesView;
-        private bool _snapshotStyles;
-        private bool _showInactiveStyles;
-        private string? _styleStatus;
+        private bool _snapshotFrames;
+        private bool _showInactiveFrames;
+        private string? _framesStatus;
         private object? _selectedEntity;
         private readonly Stack<(string Name, object Entry)> _selectedEntitiesStack = new();
         private string? _selectedEntityName;
@@ -46,12 +47,12 @@ namespace Avalonia.Diagnostics.ViewModels
             _pinnedProperties = pinnedProperties;
             TreePage = treePage;
             Layout = avaloniaObject is Visual visual
-    ? new ControlLayoutViewModel(visual)
-    : default;
+                ? new ControlLayoutViewModel(visual)
+                : default;
 
             NavigateToProperty(_avaloniaObject, (_avaloniaObject as Control)?.Name ?? _avaloniaObject.ToString());
 
-            AppliedStyles = new ObservableCollection<StyleViewModel>();
+            AppliedFrames = new ObservableCollection<ValueFrameViewModel>();
             PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
 
             if (avaloniaObject is StyledElement styledElement)
@@ -68,64 +69,13 @@ namespace Avalonia.Diagnostics.ViewModels
                     }
                 }
 
-                var styleDiagnostics = styledElement.GetStyleDiagnostics();
+                var styleDiagnostics = styledElement.GetValueStoreDiagnostic();
 
                 var clipboard = TopLevel.GetTopLevel(_avaloniaObject as Visual)?.Clipboard;
 
-                // We need to place styles without activator first, such styles will be overwritten by ones with activators.
-                foreach (var appliedStyle in styleDiagnostics.AppliedStyles.OrderBy(s => s.HasActivator))
+                foreach (var appliedStyle in styleDiagnostics.AppliedFrames.OrderBy(s => s.Priority))
                 {
-                    var styleSource = appliedStyle.Style;
-
-                    var setters = new List<SetterViewModel>();
-
-                    if (styleSource is StyleBase style)
-                    {
-                        var selector = style switch
-                        {
-                            Style s => s.Selector?.ToString(),
-                            ControlTheme t => t.TargetType?.Name.ToString(),
-                            _ => null,
-                        };
-
-                        foreach (var setter in style.Setters)
-                        {
-                            if (setter is Setter regularSetter
-                                && regularSetter.Property != null)
-                            {
-                                var setterValue = regularSetter.Value;
-
-                                var resourceInfo = GetResourceInfo(setterValue);
-
-                                SetterViewModel setterVm;
-
-                                if (resourceInfo.HasValue)
-                                {
-                                    var resourceKey = resourceInfo.Value.resourceKey;
-                                    var resourceValue = styledElement.FindResource(resourceKey);
-
-                                    setterVm = new ResourceSetterViewModel(regularSetter.Property, resourceKey, resourceValue, resourceInfo.Value.isDynamic, clipboard);
-                                }
-                                else
-                                {
-                                    var isBinding = IsBinding(setterValue);
-
-                                    if (isBinding)
-                                    {
-                                        setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue, clipboard);
-                                    }
-                                    else
-                                    {
-                                        setterVm = new SetterViewModel(regularSetter.Property, setterValue, clipboard);
-                                    }
-                                }
-
-                                setters.Add(setterVm);
-                            }
-                        }
-
-                        AppliedStyles.Add(new StyleViewModel(appliedStyle, selector ?? "No selector", setters));
-                    }
+                    AppliedFrames.Add(new ValueFrameViewModel(styledElement, appliedStyle, clipboard));
                 }
 
                 UpdateStyles();
@@ -134,35 +84,6 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public bool CanNavigateToParentProperty => _selectedEntitiesStack.Count >= 1;
 
-        private static (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
-        {
-            if (value is StaticResourceExtension staticResource
-                && staticResource.ResourceKey != null)
-            {
-                return (staticResource.ResourceKey, false);
-            }
-            else if (value is DynamicResourceExtension dynamicResource
-                && dynamicResource.ResourceKey != null)
-            {
-                return (dynamicResource.ResourceKey, true);
-            }
-
-            return null;
-        }
-
-        private static bool IsBinding(object? value)
-        {
-            switch (value)
-            {
-                case Binding:
-                case CompiledBindingExtension:
-                case TemplateBinding:
-                    return true;
-            }
-
-            return false;
-        }
-
         public TreePageViewModel TreePage { get; }
 
         public DataGridCollectionView? PropertiesView
@@ -171,7 +92,7 @@ namespace Avalonia.Diagnostics.ViewModels
             private set => RaiseAndSetIfChanged(ref _propertiesView, value);
         }
 
-        public ObservableCollection<StyleViewModel> AppliedStyles { get; }
+        public ObservableCollection<ValueFrameViewModel> AppliedFrames { get; }
 
         public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
 
@@ -199,22 +120,22 @@ namespace Avalonia.Diagnostics.ViewModels
             set => RaiseAndSetIfChanged(ref _selectedProperty, value);
         }
 
-        public bool SnapshotStyles
+        public bool SnapshotFrames
         {
-            get => _snapshotStyles;
-            set => RaiseAndSetIfChanged(ref _snapshotStyles, value);
+            get => _snapshotFrames;
+            set => RaiseAndSetIfChanged(ref _snapshotFrames, value);
         }
 
-        public bool ShowInactiveStyles
+        public bool ShowInactiveFrames
         {
-            get => _showInactiveStyles;
-            set => RaiseAndSetIfChanged(ref _showInactiveStyles, value);
+            get => _showInactiveFrames;
+            set => RaiseAndSetIfChanged(ref _showInactiveFrames, value);
         }
 
-        public string? StyleStatus
+        public string? FramesStatus
         {
-            get => _styleStatus;
-            set => RaiseAndSetIfChanged(ref _styleStatus, value);
+            get => _framesStatus;
+            set => RaiseAndSetIfChanged(ref _framesStatus, value);
         }
 
         public ControlLayoutViewModel? Layout { get; }
@@ -223,9 +144,9 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             base.OnPropertyChanged(e);
 
-            if (e.PropertyName == nameof(SnapshotStyles))
+            if (e.PropertyName == nameof(SnapshotFrames))
             {
-                if (!SnapshotStyles)
+                if (!SnapshotFrames)
                 {
                     UpdateStyles();
                 }
@@ -234,7 +155,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public void UpdateStyleFilters()
         {
-            foreach (var style in AppliedStyles)
+            foreach (var style in AppliedFrames)
             {
                 var hasVisibleSetter = false;
 
@@ -332,17 +253,17 @@ namespace Avalonia.Diagnostics.ViewModels
                 }
             }
 
-            if (!SnapshotStyles)
+            if (!SnapshotFrames)
             {
-                UpdateStyles();
+                Dispatcher.UIThread.Post(UpdateStyles);
             }
         }
 
         void IClassesChangedListener.Changed()
         {
-            if (!SnapshotStyles)
+            if (!SnapshotFrames)
             {
-                UpdateStyles();
+                Dispatcher.UIThread.Post(UpdateStyles);
             }
         }
 
@@ -350,7 +271,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             int activeCount = 0;
 
-            foreach (var style in AppliedStyles)
+            foreach (var style in AppliedFrames)
             {
                 style.Update();
 
@@ -362,7 +283,7 @@ namespace Avalonia.Diagnostics.ViewModels
 
             var propertyBuckets = new Dictionary<AvaloniaProperty, List<SetterViewModel>>();
 
-            foreach (var style in AppliedStyles)
+            foreach (var style in AppliedFrames.Reverse())
             {
                 if (!style.IsActive)
                 {
@@ -398,7 +319,7 @@ namespace Avalonia.Diagnostics.ViewModels
                 pseudoClass.Update();
             }
 
-            StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)";
+            FramesStatus = $"Value Frames ({activeCount}/{AppliedFrames.Count} active)";
         }
 
         private bool FilterProperty(object arg)

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/MainViewModel.cs

@@ -242,7 +242,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             if (Content is TreePageViewModel treeVm && treeVm.Details != null)
             {
-                treeVm.Details.SnapshotStyles = enable;
+                treeVm.Details.SnapshotFrames = enable;
             }
         }
 

+ 0 - 43
src/Avalonia.Diagnostics/Diagnostics/ViewModels/StyleViewModel.cs

@@ -1,43 +0,0 @@
-using System.Collections.Generic;
-using Avalonia.Styling;
-
-namespace Avalonia.Diagnostics.ViewModels
-{
-    internal class StyleViewModel : ViewModelBase
-    {
-        private readonly AppliedStyle _styleInstance;
-        private bool _isActive;
-        private bool _isVisible;
-
-        public StyleViewModel(AppliedStyle styleInstance, string name, List<SetterViewModel> setters)
-        {
-            _styleInstance = styleInstance;
-            IsVisible = true;
-            Name = name;
-            Setters = setters;
-
-            Update();
-        }
-
-        public bool IsActive
-        {
-            get => _isActive;
-            set => RaiseAndSetIfChanged(ref _isActive, value);
-        }
-
-        public bool IsVisible
-        {
-            get => _isVisible;
-            set => RaiseAndSetIfChanged(ref _isVisible, value);
-        }
-
-        public string Name { get; }
-
-        public List<SetterViewModel> Setters { get; }
-
-        public void Update()
-        {
-            IsActive = _styleInstance.IsActive;
-        }
-    }
-}

+ 117 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ValueFrameViewModel.cs

@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Data;
+using Avalonia.Input.Platform;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Styling;
+
+namespace Avalonia.Diagnostics.ViewModels
+{
+    internal class ValueFrameViewModel : ViewModelBase
+    {
+        private readonly IValueFrameDiagnostic _valueFrame;
+        private bool _isActive;
+        private bool _isVisible;
+
+        public ValueFrameViewModel(StyledElement styledElement, IValueFrameDiagnostic valueFrame, IClipboard? clipboard)
+        {
+            _valueFrame = valueFrame;
+            IsVisible = true;
+
+            Description = (_valueFrame.Type, _valueFrame.Description) switch
+            {
+                (IValueFrameDiagnostic.FrameType.Local, _) => "Local Values " + _valueFrame.Description,
+                (IValueFrameDiagnostic.FrameType.Template, _) => "Template " + _valueFrame.Description,
+                (IValueFrameDiagnostic.FrameType.Theme, _) => "Theme " + _valueFrame.Description,
+                (_, {Length:>0}) => _valueFrame.Description,
+                _ => _valueFrame.Priority.ToString()
+            };
+
+            Setters = new List<SetterViewModel>();
+
+            foreach (var (setterProperty, setterValue) in valueFrame.Values)
+            {
+                var resourceInfo = GetResourceInfo(setterValue);
+
+                SetterViewModel setterVm;
+
+                if (resourceInfo.HasValue)
+                {
+                    var resourceKey = resourceInfo.Value.resourceKey;
+                    var resourceValue = styledElement.FindResource(resourceKey);
+
+                    setterVm = new ResourceSetterViewModel(setterProperty, resourceKey, resourceValue,
+                        resourceInfo.Value.isDynamic, clipboard);
+                }
+                else
+                {
+                    var isBinding = IsBinding(setterValue);
+
+                    if (isBinding)
+                    {
+                        setterVm = new BindingSetterViewModel(setterProperty, setterValue, clipboard);
+                    }
+                    else
+                    {
+                        setterVm = new SetterViewModel(setterProperty, setterValue, clipboard);
+                    }
+                }
+                Setters.Add(setterVm);
+            }
+
+            Update();
+        }
+
+        public bool IsActive
+        {
+            get => _isActive;
+            set => RaiseAndSetIfChanged(ref _isActive, value);
+        }
+
+        public bool IsVisible
+        {
+            get => _isVisible;
+            set => RaiseAndSetIfChanged(ref _isVisible, value);
+        }
+
+        public string? Description { get; }
+
+        public List<SetterViewModel> Setters { get; }
+
+        public void Update()
+        {
+            IsActive = _valueFrame.IsActive;
+        }
+        
+        private static (object resourceKey, bool isDynamic)? GetResourceInfo(object? value)
+        {
+            if (value is StaticResourceExtension staticResource
+                && staticResource.ResourceKey != null)
+            {
+                return (staticResource.ResourceKey, false);
+            }
+            else if (value is DynamicResourceExtension dynamicResource
+                     && dynamicResource.ResourceKey != null)
+            {
+                return (dynamicResource.ResourceKey, true);
+            }
+
+            return null;
+        }
+
+        private static bool IsBinding(object? value)
+        {
+            switch (value)
+            {
+                case Binding:
+                case CompiledBindingExtension:
+                case TemplateBinding:
+                    return true;
+            }
+
+            return false;
+        }
+    }
+}

+ 22 - 9
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -5,6 +5,7 @@
              xmlns:controls="using:Avalonia.Diagnostics.Controls"
              xmlns:vm="using:Avalonia.Diagnostics.ViewModels"
              xmlns:lb="using:Avalonia.Diagnostics.Behaviors"
+             xmlns:system="clr-namespace:System;assembly=System.Runtime"
              x:Class="Avalonia.Diagnostics.Views.ControlDetailsView"
              x:Name="Main"
              x:DataType="vm:ControlDetailsViewModel">
@@ -153,22 +154,22 @@
         <Grid Grid.Row="0" Margin="4" RowDefinitions="Auto,Auto">
 
           <Grid Grid.Row="0" Margin="2" ColumnDefinitions="Auto,*,Auto,Auto">
-            <TextBlock FontWeight="Bold" Grid.Column="0" Text="{Binding StyleStatus}" VerticalAlignment="Center" />
-            <CheckBox Margin="2,0,0,0" Grid.Column="2" Content="Show inactive" IsChecked="{Binding ShowInactiveStyles}" ToolTip.Tip="Show styles that are currently inactive" />
-            <ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current styles (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotStyles}" />
+            <TextBlock FontWeight="Bold" Grid.Column="0" Text="{Binding FramesStatus}" VerticalAlignment="Center" />
+            <CheckBox Margin="2,0,0,0" Grid.Column="2" Content="Show inactive" IsChecked="{Binding ShowInactiveFrames}" ToolTip.Tip="Show values that are currently inactive" />
+            <ToggleButton Margin="2,0,0,0" Grid.Column="3" ToolTip.Tip="Snapshot current values (Alt+S/Alt+D to enable/disable within debugged window)" Content="Snapshot" IsChecked="{Binding SnapshotFrames}" />
           </Grid>
 
           <controls:FilterTextBox Grid.Row="1" Margin="2" Grid.Column="0"
                                   DataContext="{Binding TreePage.SettersFilter}"
                                   Text="{Binding FilterString}"
-                                  Watermark="Filter setters"
+                                  Watermark="Filter values"
                                   UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
                                   UseWholeWordFilter="{Binding UseWholeWordFilter}"
                                   UseRegexFilter="{Binding UseRegexFilter}" />
         </Grid>
 
         <ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled">
-          <ItemsControl ItemsSource="{Binding AppliedStyles}" >
+          <ItemsControl ItemsSource="{Binding AppliedFrames}" >
             <ItemsControl.ItemTemplate>
               <DataTemplate>
                 <Border BorderThickness="0,0,0,1" BorderBrush="#6C6C6C" Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacity}}">
@@ -176,14 +177,14 @@
                     <MultiBinding Converter="{x:Static BoolConverters.And}">
                       <MultiBinding Converter="{x:Static BoolConverters.Or}" >
                         <Binding Path="IsActive" />
-                        <Binding Path="#Main.((vm:ControlDetailsViewModel)DataContext).ShowInactiveStyles" />
+                        <Binding Path="#Main.((vm:ControlDetailsViewModel)DataContext).ShowInactiveFrames" />
                       </MultiBinding>
                       <Binding Path="IsVisible" />
                     </MultiBinding>
                   </Border.IsVisible>
                   <Expander IsExpanded="True" Margin="0" Padding="8,0" ContentTransition="{x:Null}" >
                     <Expander.Header>
-                      <TextBlock Grid.Row="0" Text="{Binding Name}" />
+                      <TextBlock Grid.Row="0" Text="{Binding Description}" />
                     </Expander.Header>
 
                     <ItemsControl Margin="20,0,0,0" Grid.Row="1" ItemsSource="{Binding Setters}">
@@ -259,7 +260,13 @@
                             <StackPanel Orientation="Horizontal" Spacing="2" HorizontalAlignment="Left">
                               <TextBlock Classes="property-name" PointerPressed="PropertyNamePressed" Text="{Binding Name}" />
                               <TextBlock Text=":" />
-                              <ContentControl Content="{Binding Value}"/>
+                              <ContentControl Content="{Binding Value}">
+                                <ContentControl.ContentTemplate>
+                                  <DataTemplate x:DataType="system:Object">
+                                    <TextBlock Text="{Binding}" />
+                                  </DataTemplate>
+                                </ContentControl.ContentTemplate>
+                              </ContentControl>
                               <TextBlock>(</TextBlock>
                               <Ellipse Height="8" Width="8" VerticalAlignment="Center" Fill="{Binding Tint}" ToolTip.Tip="{Binding ValueTypeTooltip}"/>
                               <TextBlock FontStyle="Italic" Text="{Binding Key}" />
@@ -280,7 +287,13 @@
                             <StackPanel Orientation="Horizontal" Spacing="2">
                               <TextBlock Classes="property-name" PointerPressed="PropertyNamePressed" Text="{Binding Name}" />
                               <TextBlock Text=":" />
-                              <ContentControl Content="{Binding Value}"/>
+                              <ContentControl Content="{Binding Value}">
+                                <ContentControl.ContentTemplate>
+                                  <DataTemplate x:DataType="system:Object">
+                                    <TextBlock Text="{Binding}" />
+                                  </DataTemplate>
+                                </ContentControl.ContentTemplate>
+                              </ContentControl>
                             </StackPanel>
                             <Rectangle Classes="property-inactive" IsVisible="{Binding !IsActive}" />
                           </Panel>