using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Reflection; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Data; using Avalonia.Markup.Xaml.MarkupExtensions; using Avalonia.Styling; using Avalonia.VisualTree; namespace Avalonia.Diagnostics.ViewModels { internal class ControlDetailsViewModel : ViewModelBase, IDisposable { private readonly IAvaloniaObject _avaloniaObject; private IDictionary? _propertyIndex; private PropertyViewModel? _selectedProperty; private DataGridCollectionView? _propertiesView; private bool _snapshotStyles; private bool _showInactiveStyles; private string? _styleStatus; private object? _selectedEntity; private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new(); private string? _selectedEntityName; private string? _selectedEntityType; private bool _showImplementedInterfaces; public ControlDetailsViewModel(TreePageViewModel treePage, IAvaloniaObject avaloniaObject) { _avaloniaObject = avaloniaObject; TreePage = treePage; Layout = avaloniaObject is IVisual ? new ControlLayoutViewModel((IVisual)avaloniaObject) : default; NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); AppliedStyles = new ObservableCollection(); PseudoClasses = new ObservableCollection(); if (avaloniaObject is StyledElement styledElement) { styledElement.Classes.CollectionChanged += OnClassesChanged; var pseudoClassAttributes = styledElement.GetType().GetCustomAttributes(true); foreach (var classAttribute in pseudoClassAttributes) { foreach (var className in classAttribute.PseudoClasses) { PseudoClasses.Add(new PseudoClassViewModel(className, styledElement)); } } var styleDiagnostics = styledElement.GetStyleDiagnostics(); // 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)) { var styleSource = appliedStyle.Source; var setters = new List(); if (styleSource is Style style) { 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); } else { var isBinding = IsBinding(setterValue); if (isBinding) { setterVm = new BindingSetterViewModel(regularSetter.Property, setterValue); } else { setterVm = new SetterViewModel(regularSetter.Property, setterValue); } } setters.Add(setterVm); } } AppliedStyles.Add(new StyleViewModel(appliedStyle, style.Selector?.ToString() ?? "No selector", setters)); } } UpdateStyles(); } } private (object resourceKey, bool isDynamic)? GetResourceInfo(object? value) { if (value is StaticResourceExtension staticResource) { return (staticResource.ResourceKey, false); } else if (value is DynamicResourceExtension dynamicResource && dynamicResource.ResourceKey != null) { return (dynamicResource.ResourceKey, true); } return null; } private bool IsBinding(object? value) { switch (value) { case Binding: case CompiledBindingExtension: case TemplateBinding: return true; } return false; } public TreePageViewModel TreePage { get; } public DataGridCollectionView? PropertiesView { get => _propertiesView; private set => RaiseAndSetIfChanged(ref _propertiesView, value); } public ObservableCollection AppliedStyles { get; } public ObservableCollection PseudoClasses { get; } public object? SelectedEntity { get => _selectedEntity; set => RaiseAndSetIfChanged(ref _selectedEntity, value); } public string? SelectedEntityName { get => _selectedEntityName; set => RaiseAndSetIfChanged(ref _selectedEntityName, value); } public string? SelectedEntityType { get => _selectedEntityType; set => RaiseAndSetIfChanged(ref _selectedEntityType, value); } public PropertyViewModel? SelectedProperty { get => _selectedProperty; set => RaiseAndSetIfChanged(ref _selectedProperty, value); } public bool SnapshotStyles { get => _snapshotStyles; set => RaiseAndSetIfChanged(ref _snapshotStyles, value); } public bool ShowInactiveStyles { get => _showInactiveStyles; set => RaiseAndSetIfChanged(ref _showInactiveStyles, value); } public string? StyleStatus { get => _styleStatus; set => RaiseAndSetIfChanged(ref _styleStatus, value); } public ControlLayoutViewModel? Layout { get; } protected override void OnPropertyChanged(PropertyChangedEventArgs e) { base.OnPropertyChanged(e); if (e.PropertyName == nameof(SnapshotStyles)) { if (!SnapshotStyles) { UpdateStyles(); } } } public void UpdateStyleFilters() { foreach (var style in AppliedStyles) { var hasVisibleSetter = false; foreach (var setter in style.Setters) { setter.IsVisible = TreePage.SettersFilter.Filter(setter.Name); hasVisibleSetter |= setter.IsVisible; } style.IsVisible = hasVisibleSetter; } } public void Dispose() { if (_avaloniaObject is INotifyPropertyChanged inpc) { inpc.PropertyChanged -= ControlPropertyChanged; } if (_avaloniaObject is AvaloniaObject ao) { ao.PropertyChanged -= ControlPropertyChanged; } if (_avaloniaObject is StyledElement se) { se.Classes.CollectionChanged -= OnClassesChanged; } } private IEnumerable GetAvaloniaProperties(object o) { if (o is AvaloniaObject ao) { return AvaloniaPropertyRegistry.Instance.GetRegistered(ao) .Union(AvaloniaPropertyRegistry.Instance.GetRegisteredAttached(ao.GetType())) .Select(x => new AvaloniaPropertyViewModel(ao, x)); } else { return Enumerable.Empty(); } } private IEnumerable GetClrProperties(object o, bool showImplementedInterfaces) { foreach (var p in GetClrProperties(o, o.GetType())) { yield return p; } if (showImplementedInterfaces) { foreach (var i in o.GetType().GetInterfaces()) { foreach (var p in GetClrProperties(o, i)) { yield return p; } } } } private IEnumerable GetClrProperties(object o, Type t) { return t.GetProperties() .Where(x => x.GetIndexParameters().Length == 0) .Select(x => new ClrPropertyViewModel(o, x)); } private void ControlPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e) { if (_propertyIndex is { } && _propertyIndex.TryGetValue(e.Property, out var properties)) { foreach (var property in properties) { property.Update(); } } Layout?.ControlPropertyChanged(sender, e); } private void ControlPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (e.PropertyName != null && _propertyIndex is { } && _propertyIndex.TryGetValue(e.PropertyName, out var properties)) { foreach (var property in properties) { property.Update(); } } if (!SnapshotStyles) { UpdateStyles(); } } private void OnClassesChanged(object? sender, NotifyCollectionChangedEventArgs e) { if (!SnapshotStyles) { UpdateStyles(); } } private void UpdateStyles() { int activeCount = 0; foreach (var style in AppliedStyles) { style.Update(); if (style.IsActive) { activeCount++; } } var propertyBuckets = new Dictionary>(); foreach (var style in AppliedStyles) { if (!style.IsActive) { continue; } foreach (var setter in style.Setters) { if (propertyBuckets.TryGetValue(setter.Property, out var setters)) { foreach (var otherSetter in setters) { otherSetter.IsActive = false; } setter.IsActive = true; setters.Add(setter); } else { setter.IsActive = true; setters = new List { setter }; propertyBuckets.Add(setter.Property, setters); } } } foreach (var pseudoClass in PseudoClasses) { pseudoClass.Update(); } StyleStatus = $"Styles ({activeCount}/{AppliedStyles.Count} active)"; } private bool FilterProperty(object arg) { return !(arg is PropertyViewModel property) || TreePage.PropertiesFilter.Filter(property.Name); } private class PropertyComparer : IComparer { public static PropertyComparer Instance { get; } = new PropertyComparer(); public int Compare(PropertyViewModel? x, PropertyViewModel? y) { var groupX = GroupIndex(x?.Group); var groupY = GroupIndex(y?.Group); if (groupX != groupY) { return groupX - groupY; } else { return string.CompareOrdinal(x?.Name, y?.Name); } } private int GroupIndex(string? group) { switch (group) { case "Properties": return 0; case "Attached Properties": return 1; case "CLR Properties": return 2; default: return 3; } } } public void ApplySelectedProperty() { var selectedProperty = SelectedProperty; var selectedEntity = SelectedEntity; var selectedEntityName = SelectedEntityName; if (selectedEntity == null || selectedProperty == null || selectedProperty.PropertyType == typeof(string) || selectedProperty.PropertyType.IsValueType ) return; object? property; if (selectedProperty.Key is AvaloniaProperty avaloniaProperty) { property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty); } else { property = selectedEntity.GetType().GetProperties() .FirstOrDefault(pi => pi.Name == selectedProperty.Name && pi.DeclaringType == selectedProperty.DeclaringType && pi.PropertyType.Name == selectedProperty.PropertyType.Name) ?.GetValue(selectedEntity); } if (property == null) return; _selectedEntitiesStack.Push((Name:selectedEntityName!,Entry:selectedEntity)); NavigateToProperty(property, selectedProperty.Name); } public void ApplyParentProperty() { if (_selectedEntitiesStack.Any()) { var property = _selectedEntitiesStack.Pop(); NavigateToProperty(property.Entry, property.Name); } } protected void NavigateToProperty(object o, string? entityName) { var oldSelectedEntity = SelectedEntity; if (oldSelectedEntity is IAvaloniaObject ao1) { ao1.PropertyChanged -= ControlPropertyChanged; } else if (oldSelectedEntity is INotifyPropertyChanged inpc1) { inpc1.PropertyChanged -= ControlPropertyChanged; } SelectedEntity = o; SelectedEntityName = entityName; SelectedEntityType = o.ToString(); var properties = GetAvaloniaProperties(o) .Concat(GetClrProperties(o, _showImplementedInterfaces)) .OrderBy(x => x, PropertyComparer.Instance) .ThenBy(x => x.Name) .ToArray(); _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToArray()); var view = new DataGridCollectionView(properties); view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group))); view.Filter = FilterProperty; PropertiesView = view; if (o is IAvaloniaObject ao2) { ao2.PropertyChanged += ControlPropertyChanged; } else if (o is INotifyPropertyChanged inpc2) { inpc2.PropertyChanged += ControlPropertyChanged; } } internal void SelectProperty(AvaloniaProperty property) { SelectedProperty = null; if (SelectedEntity != _avaloniaObject) { NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); } if (PropertiesView is null) { return; } foreach (object o in PropertiesView) { if (o is AvaloniaPropertyViewModel propertyVm && propertyVm.Property == property) { SelectedProperty = propertyVm; break; } } } internal void UpdatePropertiesView(bool showImplementedInterfaces) { _showImplementedInterfaces = showImplementedInterfaces; SelectedProperty = null; NavigateToProperty(_avaloniaObject, (_avaloniaObject as IControl)?.Name ?? _avaloniaObject.ToString()); } } }