using Avalonia; using Avalonia.Collections; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.VisualTree; using CommunityToolkit.Mvvm.Input; using InABox.Core; using System.Collections; using System.ComponentModel; namespace InABox.Avalonia.Components; public class AvaloniaDataGridSelectionChangedEventArgs(object?[] selection) { public object?[] Selection { get; set; } = selection; } public class AvaloniaDataGridRefreshRequestedEventArgs() { } public partial class AvaloniaDataGrid : UserControl, INotifyPropertyChanged { public static StyledProperty CanSearchProperty = AvaloniaProperty.Register(nameof(CanSearch), true); public static StyledProperty ItemsSourceProperty = AvaloniaProperty.Register(nameof(ItemsSource)); public static StyledProperty LastUpdatedProperty = AvaloniaProperty.Register(nameof(LastUpdated)); public static StyledProperty ShowRecordCountProperty = AvaloniaProperty.Register(nameof(ShowRecordCount)); public static StyledProperty RefreshVisibleProperty = AvaloniaProperty.Register(nameof(RefreshVisible)); public static StyledProperty SelectionModeProperty = AvaloniaProperty.Register(nameof(SelectionMode), SelectionMode.Single); public static StyledProperty RowHeightProperty = AvaloniaProperty.Register(nameof(RowHeight), 30); public static StyledProperty ColumnsProperty = AvaloniaProperty.Register(nameof(Columns), null); public string SearchText { get; set; } = ""; public bool CanSearch { get => GetValue(CanSearchProperty); set => SetValue(CanSearchProperty, value); } public IEnumerable ItemsSource { get => GetValue(ItemsSourceProperty); set => SetValue(ItemsSourceProperty, value); } public bool ShowRecordCount { get => GetValue(ShowRecordCountProperty); set => SetValue(ShowRecordCountProperty, value); } public bool RefreshVisible { get => GetValue(RefreshVisibleProperty); set => SetValue(RefreshVisibleProperty, value); } public SelectionMode SelectionMode { get => GetValue(SelectionModeProperty); set => SetValue(SelectionModeProperty, value); } public double RowHeight { get => GetValue(RowHeightProperty); set => SetValue(RowHeightProperty, value); } public int ItemCount { get; set; } public DateTime LastUpdated { get => GetValue(LastUpdatedProperty); set => SetValue(LastUpdatedProperty, value); } public AvaloniaDataGridColumns Columns { get; private set; } public IEnumerable SelectedItems => Grid.SelectedItems.Cast(); public event EventHandler? SelectionChanged; public event EventHandler? RefreshRequested; #region Static Constructor and Property Changed Handlers static AvaloniaDataGrid() { ItemsSourceProperty.Changed.AddClassHandler(ItemsSource_Changed); LastUpdatedProperty.Changed.AddClassHandler(LastUpdated_Changed); ShowRecordCountProperty.Changed.AddClassHandler(ShowRecordCount_Changed); ColumnsProperty.Changed.AddClassHandler(ColumnsProperty_Changed); } private static void ColumnsProperty_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args) { var columns = grid.GetValue(ColumnsProperty); if(columns is not null) { grid.Columns.BeginUpdate().AddRange(columns).EndUpdate(); } } private static void ShowRecordCount_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args) { grid.UpdateSummaryRow(); } private static void LastUpdated_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args) { grid.UpdateSummaryRow(); } private static void ItemsSource_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args) { grid.Grid.ItemsSource = grid.ItemsSource; if (grid.Grid.CollectionView is not null) { grid.Grid.CollectionView.CollectionChanged -= grid.CollectionView_CollectionChanged; grid.Grid.CollectionView.CollectionChanged += grid.CollectionView_CollectionChanged; } grid.ItemsChanged(); grid.UpdateSummaryRow(); } #endregion public AvaloniaDataGrid() { InitializeComponent(); Columns = new AvaloniaDataGridColumns(); Columns.Changed += Columns_Changed; } private void CollectionView_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e) { ItemsChanged(); } private void ItemsChanged() { ItemCount = (Grid.CollectionView as DataGridCollectionView)?.ItemCount ?? 0; } private void Columns_Changed(AvaloniaDataGridColumns columns) { Grid.Columns.Clear(); foreach(var column in columns) { Grid.Columns.Add(column.CreateColumn()); } // Summaries var searchableColumns = Columns.Any(x => x.Searchable); SearchBar.IsVisible = searchableColumns && CanSearch; } private void UpdateSummaryRow() { _lastUpdated.IsVisible = LastUpdated != DateTime.MinValue; _recordCount.Content = $" {ItemCount} records"; _recordCount.IsVisible = ShowRecordCount && ItemsSource is IEnumerable; _recordCountBox.IsVisible = _recordCount.IsVisible || _lastUpdated.IsVisible; } public void ClearSelection() { Grid.SelectedItem = null; Grid.SelectedItems.Clear(); } private bool DoSearch(object? item) { if (SearchText.IsNullOrWhiteSpace()) return true; if (item is null) return false; foreach(var column in Columns) { if(column.Filter(item, SearchText)) return true; } return false; } [RelayCommand] private void Search() { if (Grid.CollectionView is null) return; Grid.CollectionView.Filter = DoSearch; Grid.CollectionView.Refresh(); UpdateSummaryRow(); } [RelayCommand] private void Refresh() { if (Grid.CollectionView is null) return; RefreshRequested?.Invoke(this, new()); Grid.CollectionView.Refresh(); } private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e) { if (SelectionMode != SelectionMode.Multiple) return; SelectionChanged?.Invoke(this, new AvaloniaDataGridSelectionChangedEventArgs(SelectedItems.ToArray())); } private void DataGrid_Tapped(object sender, TappedEventArgs e) { if (SelectionMode == SelectionMode.Multiple) return; var position = e.GetPosition(Grid); var parent = (e.Source as Visual)?.GetVisualAncestors().Where(x => x is DataGridCell || x is DataGridColumnHeader).FirstOrDefault(); if (parent is null) return; if(parent is DataGridCell cell) { var cellCollection = cell.GetVisualParent(); if (cellCollection is null) return; var colIdx = cellCollection.Children.IndexOf(cell); var row = cellCollection.GetVisualAncestors().OfType().FirstOrDefault(); if (row is null) return; var rowCollection = row.GetVisualParent(); if (rowCollection is null) return; var rowIdx = row.Index; var item = (Grid.CollectionView as DataGridCollectionView)?.GetItemAt(rowIdx); var column = Columns[colIdx]; if(column.Tapped is not null) { column.Tapped?.Invoke(column, item); } else { SelectionChanged?.Invoke(this, new AvaloniaDataGridSelectionChangedEventArgs(new object?[] { item })); } } else if(parent is DataGridColumnHeader header) { var headerCollection = header.GetVisualParent(); if (headerCollection is null) return; var colIdx = headerCollection.Children.IndexOf(header); var column = Columns[colIdx]; if(column.Tapped is not null) { column.Tapped?.Invoke(column, null); } else { SelectionChanged?.Invoke(this, new AvaloniaDataGridSelectionChangedEventArgs(Array.Empty())); } } } }