using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.PropertyGrid.Models;
using Avalonia.PropertyGrid.Services;
namespace Avalonia.PropertyGrid.Controls;
///
/// A PropertyGrid control for editing object properties with Xceed-like functionality
///
public class PropertyGrid : TemplatedControl
{
private object? _selectedObject;
private string _searchText = string.Empty;
///
/// Defines the SelectedObject property.
///
public static readonly StyledProperty SelectedObjectProperty =
AvaloniaProperty.Register(nameof(SelectedObject));
///
/// Defines the SearchText property.
///
public static readonly StyledProperty SearchTextProperty =
AvaloniaProperty.Register(nameof(SearchText), string.Empty);
///
/// Defines the ShowCategories property.
///
public static readonly StyledProperty ShowCategoriesProperty =
AvaloniaProperty.Register(nameof(ShowCategories), true);
///
/// Defines the ShowDescriptions property.
///
public static readonly StyledProperty ShowDescriptionsProperty =
AvaloniaProperty.Register(nameof(ShowDescriptions), true);
///
/// Defines the FilteredProperties property.
///
public static readonly StyledProperty> FilteredPropertiesProperty =
AvaloniaProperty.Register>(
nameof(FilteredProperties),
new ObservableCollection());
///
/// Defines the CategoryGroups property.
///
public static readonly StyledProperty> CategoryGroupsProperty =
AvaloniaProperty.Register>(
nameof(CategoryGroups),
new ObservableCollection());
static PropertyGrid()
{
SelectedObjectProperty.Changed.AddClassHandler((grid, args) =>
grid.OnSelectedObjectChanged(args));
SearchTextProperty.Changed.AddClassHandler((grid, args) =>
grid.OnSearchTextChanged(args));
}
///
/// Initializes a new instance of the PropertyGrid class.
///
public PropertyGrid()
{
FilteredProperties = new ObservableCollection();
}
///
/// The object whose properties are being edited
///
public object? SelectedObject
{
get => GetValue(SelectedObjectProperty);
set => SetValue(SelectedObjectProperty, value);
}
///
/// The search/filter text
///
public string SearchText
{
get => GetValue(SearchTextProperty);
set => SetValue(SearchTextProperty, value);
}
///
/// Whether to show properties grouped by categories
///
public bool ShowCategories
{
get => GetValue(ShowCategoriesProperty);
set => SetValue(ShowCategoriesProperty, value);
}
///
/// Whether to show property descriptions
///
public bool ShowDescriptions
{
get => GetValue(ShowDescriptionsProperty);
set => SetValue(ShowDescriptionsProperty, value);
}
///
/// The filtered list of properties to display
///
public ObservableCollection FilteredProperties
{
get => GetValue(FilteredPropertiesProperty);
set => SetValue(FilteredPropertiesProperty, value);
}
///
/// The category groups to display
///
public ObservableCollection CategoryGroups
{
get => GetValue(CategoryGroupsProperty);
set => SetValue(CategoryGroupsProperty, value);
}
private List _allProperties = new();
private void OnSelectedObjectChanged(AvaloniaPropertyChangedEventArgs args)
{
_selectedObject = args.NewValue;
System.Diagnostics.Debug.WriteLine($"PropertyGrid: SelectedObject changed to {_selectedObject?.GetType().Name ?? "null"}");
RefreshProperties();
}
private void OnSearchTextChanged(AvaloniaPropertyChangedEventArgs args)
{
_searchText = args.NewValue as string ?? string.Empty;
FilterProperties();
}
///
/// Refreshes the property list from the selected object
///
public void RefreshProperties()
{
_allProperties.Clear();
FilteredProperties.Clear();
if (_selectedObject == null)
{
System.Diagnostics.Debug.WriteLine("PropertyGrid: RefreshProperties - SelectedObject is null");
return;
}
_allProperties = PropertyItemFactory.CreatePropertyItems(_selectedObject);
System.Diagnostics.Debug.WriteLine($"PropertyGrid: Created {_allProperties.Count} property items");
// Subscribe to property changes
foreach (var item in _allProperties)
{
item.PropertyChanged += OnPropertyItemChanged;
}
FilterProperties();
System.Diagnostics.Debug.WriteLine($"PropertyGrid: FilteredProperties count = {FilteredProperties.Count}");
}
private void OnPropertyItemChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PropertyItem.IsExpanded) && sender is PropertyItem item)
{
HandlePropertyExpansion(item);
}
}
private void HandlePropertyExpansion(PropertyItem item)
{
System.Diagnostics.Debug.WriteLine($"HandlePropertyExpansion: {item.DisplayName}, IsExpanded={item.IsExpanded}, Children.Count={item.Children.Count}");
if (item.IsExpanded && item.Value != null)
{
// Load child properties if not already loaded
if (item.Children.Count == 0)
{
item.Children = PropertyItemFactory.CreatePropertyItems(item.Value, item.IndentLevel + 1);
System.Diagnostics.Debug.WriteLine($" Loaded {item.Children.Count} children for {item.DisplayName}");
// Subscribe to property changes for children
foreach (var child in item.Children)
{
child.PropertyChanged += OnPropertyItemChanged;
}
}
// Insert children after the parent in the filtered list
var parentIndex = FilteredProperties.IndexOf(item);
if (parentIndex >= 0)
{
int insertedCount = 0;
for (int i = 0; i < item.Children.Count; i++)
{
// Only insert if not already in the list
if (!FilteredProperties.Contains(item.Children[i]))
{
FilteredProperties.Insert(parentIndex + i + 1, item.Children[i]);
insertedCount++;
}
}
System.Diagnostics.Debug.WriteLine($" Inserted {insertedCount} children into FilteredProperties at index {parentIndex + 1}");
}
// Also insert into CategoryGroups if ShowCategories is enabled
if (ShowCategories)
{
foreach (var categoryGroup in CategoryGroups)
{
var itemIndexInCategory = categoryGroup.Properties.IndexOf(item);
if (itemIndexInCategory >= 0)
{
// Insert children after the parent in the category
for (int i = 0; i < item.Children.Count; i++)
{
if (!categoryGroup.Properties.Contains(item.Children[i]))
{
categoryGroup.Properties.Insert(itemIndexInCategory + i + 1, item.Children[i]);
}
}
System.Diagnostics.Debug.WriteLine($" Inserted {item.Children.Count} children into category '{categoryGroup.CategoryName}'");
break;
}
}
}
}
else if (!item.IsExpanded)
{
// Remove children from the filtered list (but keep them in item.Children for re-expansion)
int removedCount = 0;
foreach (var child in item.Children)
{
if (FilteredProperties.Remove(child))
{
removedCount++;
}
// If child is also expanded, recursively collapse it
if (child.IsExpanded)
{
child.IsExpanded = false;
}
}
System.Diagnostics.Debug.WriteLine($" Removed {removedCount} children from FilteredProperties");
// Also remove from CategoryGroups if ShowCategories is enabled
if (ShowCategories)
{
foreach (var categoryGroup in CategoryGroups)
{
if (categoryGroup.Properties.Contains(item))
{
int categoryRemovedCount = 0;
foreach (var child in item.Children)
{
if (categoryGroup.Properties.Remove(child))
{
categoryRemovedCount++;
}
}
System.Diagnostics.Debug.WriteLine($" Removed {categoryRemovedCount} children from category '{categoryGroup.CategoryName}'");
break;
}
}
}
}
}
private void FilterProperties()
{
FilteredProperties.Clear();
CategoryGroups.Clear();
var filtered = _allProperties.AsEnumerable();
// Apply search filter
if (!string.IsNullOrWhiteSpace(_searchText))
{
var searchLower = _searchText.ToLowerInvariant();
filtered = filtered.Where(p =>
p.DisplayName.ToLowerInvariant().Contains(searchLower) ||
(p.Category?.ToLowerInvariant().Contains(searchLower) ?? false) ||
(p.Description?.ToLowerInvariant().Contains(searchLower) ?? false));
}
// Populate flat list
foreach (var item in filtered)
{
FilteredProperties.Add(item);
}
System.Diagnostics.Debug.WriteLine($"PropertyGrid: FilterProperties - ShowCategories={ShowCategories}, filtered count={filtered.Count()}");
// Group by category for category view
if (ShowCategories)
{
var groupedByCategory = filtered
.GroupBy(p => p.Category ?? "Misc")
.OrderBy(g => g.Key);
foreach (var group in groupedByCategory)
{
var categoryGroup = new CategoryGroup
{
CategoryName = group.Key
};
foreach (var item in group.OrderBy(p => p.Order).ThenBy(p => p.DisplayName))
{
categoryGroup.Properties.Add(item);
}
CategoryGroups.Add(categoryGroup);
System.Diagnostics.Debug.WriteLine($"PropertyGrid: Added category '{categoryGroup.CategoryName}' with {categoryGroup.PropertyCount} properties");
}
System.Diagnostics.Debug.WriteLine($"PropertyGrid: Total CategoryGroups={CategoryGroups.Count}");
}
}
///
/// Refreshes all property values from the source object
///
public void RefreshValues()
{
foreach (var item in _allProperties)
{
item.RefreshValue();
}
}
}