using System; using System.Collections.Generic; using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Reflection; using Avalonia.PropertyGrid.Models; namespace Avalonia.PropertyGrid.Services; /// /// Service for creating PropertyItem collections from objects /// public class PropertyItemFactory { private static readonly HashSet SimpleTypes = new() { typeof(string), typeof(int), typeof(long), typeof(short), typeof(byte), typeof(uint), typeof(ulong), typeof(ushort), typeof(sbyte), typeof(float), typeof(double), typeof(decimal), typeof(bool), typeof(char), typeof(DateTime), typeof(DateTimeOffset), typeof(TimeSpan), typeof(Guid) }; /// /// Creates a collection of PropertyItems from an object /// public static List CreatePropertyItems(object? obj, int indentLevel = 0) { if (obj == null) return new List(); var properties = new List(); var type = obj.GetType(); // Check if this is a collection (but not a string or dictionary) if (IsCollection(obj)) { return CreateCollectionItems(obj, indentLevel); } foreach (var prop in type.GetProperties(BindingFlags.Public | BindingFlags.Instance)) { // Skip indexed properties if (prop.GetIndexParameters().Length > 0) continue; var item = CreatePropertyItem(obj, prop, indentLevel); if (item != null && item.IsBrowsable) { properties.Add(item); } } // Sort by order, then by category, then by name return properties.OrderBy(p => p.Order) .ThenBy(p => p.Category) .ThenBy(p => p.DisplayName) .ToList(); } /// /// Creates PropertyItems for collection elements /// private static List CreateCollectionItems(object collection, int indentLevel) { var items = new List(); if (collection is System.Collections.IEnumerable enumerable) { int index = 0; foreach (var element in enumerable) { if (element == null) { index++; continue; } var elementType = element.GetType(); var isSimple = IsSimpleType(elementType); // For simple types, show the value in the display name string displayName = $"[{index}]"; if (isSimple || elementType == typeof(string)) { var valueStr = element.ToString(); if (!string.IsNullOrEmpty(valueStr) && valueStr.Length <= 50) { displayName = $"[{index}] = {valueStr}"; } } var item = new PropertyItem { Name = $"[{index}]", DisplayName = displayName, Description = $"Item at index {index}", Category = "Items", PropertyType = elementType, IsReadOnly = true, // Collection items are read-only for now Instance = collection, IndentLevel = indentLevel, Value = element, Converter = TypeDescriptor.GetConverter(elementType), Order = index, IsBrowsable = true, IsExpandable = !isSimple && elementType.IsClass && elementType != typeof(string) }; items.Add(item); index++; } } return items; } /// /// Checks if an object is a collection (excluding strings and dictionaries) /// private static bool IsCollection(object obj) { if (obj == null) return false; var type = obj.GetType(); // Exclude strings and dictionaries if (type == typeof(string)) return false; if (typeof(System.Collections.IDictionary).IsAssignableFrom(type)) return false; // Check if it implements IEnumerable return typeof(System.Collections.IEnumerable).IsAssignableFrom(type); } /// /// Creates a single PropertyItem from a PropertyInfo /// public static PropertyItem? CreatePropertyItem(object instance, PropertyInfo propertyInfo, int indentLevel = 0) { // Check if browsable var browsableAttr = propertyInfo.GetCustomAttribute(); if (browsableAttr != null && !browsableAttr.Browsable) return null; var item = new PropertyItem { Name = propertyInfo.Name, DisplayName = GetDisplayName(propertyInfo), Description = GetDescription(propertyInfo), Category = GetCategory(propertyInfo), PropertyType = propertyInfo.PropertyType, IsReadOnly = !propertyInfo.CanWrite || propertyInfo.GetCustomAttribute()?.IsReadOnly == true, PropertyInfo = propertyInfo, Instance = instance, IndentLevel = indentLevel, Converter = TypeDescriptor.GetConverter(propertyInfo.PropertyType), Order = GetOrder(propertyInfo), IsBrowsable = true }; // Get the current value try { item.Value = propertyInfo.GetValue(instance); } catch { item.Value = null; } // Check if expandable (complex type) var propType = propertyInfo.PropertyType; item.IsExpandable = !IsSimpleType(propType) && propType.IsClass && propType != typeof(string); return item; } private static string GetDisplayName(PropertyInfo prop) { var displayAttr = prop.GetCustomAttribute(); if (displayAttr?.Name != null) return displayAttr.Name; var displayNameAttr = prop.GetCustomAttribute(); if (displayNameAttr?.DisplayName != null) return displayNameAttr.DisplayName; // Add spaces before capital letters return AddSpacesToCamelCase(prop.Name); } private static string? GetDescription(PropertyInfo prop) { var displayAttr = prop.GetCustomAttribute(); if (displayAttr?.Description != null) return displayAttr.Description; var descriptionAttr = prop.GetCustomAttribute(); return descriptionAttr?.Description; } private static string GetCategory(PropertyInfo prop) { var categoryAttr = prop.GetCustomAttribute(); if (categoryAttr?.Category != null && categoryAttr.Category != CategoryAttribute.Default.Category) return categoryAttr.Category; var displayAttr = prop.GetCustomAttribute(); if (displayAttr?.GroupName != null) return displayAttr.GroupName; return "Misc"; } private static int GetOrder(PropertyInfo prop) { var displayAttr = prop.GetCustomAttribute(); return displayAttr?.GetOrder() ?? int.MaxValue; } private static bool IsSimpleType(Type type) { type = Nullable.GetUnderlyingType(type) ?? type; return SimpleTypes.Contains(type) || type.IsEnum; } private static string AddSpacesToCamelCase(string text) { if (string.IsNullOrWhiteSpace(text)) return string.Empty; var result = new System.Text.StringBuilder(); result.Append(text[0]); for (int i = 1; i < text.Length; i++) { if (char.IsUpper(text[i]) && !char.IsUpper(text[i - 1])) { result.Append(' '); } result.Append(text[i]); } return result.ToString(); } }