Files
PropertyGrid/Avalonia.PropertyGrid/Services/PropertyItemFactory.cs
T

253 lines
8.1 KiB
C#
Raw Normal View History

2026-08-10 14:45:47 +02:00
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;
/// <summary>
/// Service for creating PropertyItem collections from objects
/// </summary>
public class PropertyItemFactory
{
private static readonly HashSet<Type> 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)
};
/// <summary>
/// Creates a collection of PropertyItems from an object
/// </summary>
public static List<PropertyItem> CreatePropertyItems(object? obj, int indentLevel = 0)
{
if (obj == null)
return new List<PropertyItem>();
var properties = new List<PropertyItem>();
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();
}
/// <summary>
/// Creates PropertyItems for collection elements
/// </summary>
private static List<PropertyItem> CreateCollectionItems(object collection, int indentLevel)
{
var items = new List<PropertyItem>();
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;
}
/// <summary>
/// Checks if an object is a collection (excluding strings and dictionaries)
/// </summary>
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);
}
/// <summary>
/// Creates a single PropertyItem from a PropertyInfo
/// </summary>
public static PropertyItem? CreatePropertyItem(object instance, PropertyInfo propertyInfo, int indentLevel = 0)
{
// Check if browsable
var browsableAttr = propertyInfo.GetCustomAttribute<BrowsableAttribute>();
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<ReadOnlyAttribute>()?.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<DisplayAttribute>();
if (displayAttr?.Name != null)
return displayAttr.Name;
var displayNameAttr = prop.GetCustomAttribute<DisplayNameAttribute>();
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<DisplayAttribute>();
if (displayAttr?.Description != null)
return displayAttr.Description;
var descriptionAttr = prop.GetCustomAttribute<DescriptionAttribute>();
return descriptionAttr?.Description;
}
private static string GetCategory(PropertyInfo prop)
{
var categoryAttr = prop.GetCustomAttribute<CategoryAttribute>();
if (categoryAttr?.Category != null && categoryAttr.Category != CategoryAttribute.Default.Category)
return categoryAttr.Category;
var displayAttr = prop.GetCustomAttribute<DisplayAttribute>();
if (displayAttr?.GroupName != null)
return displayAttr.GroupName;
return "Misc";
}
private static int GetOrder(PropertyInfo prop)
{
var displayAttr = prop.GetCustomAttribute<DisplayAttribute>();
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();
}
}