|
@@ -0,0 +1,238 @@
|
|
|
+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<bool> CanSearchProperty =
|
|
|
+ AvaloniaProperty.Register<AvaloniaDataGrid, bool>(nameof(CanSearch), true);
|
|
|
+ public static StyledProperty<IEnumerable> ItemsSourceProperty =
|
|
|
+ AvaloniaProperty.Register<AvaloniaDataGrid, IEnumerable>(nameof(ItemsSource));
|
|
|
+ public static StyledProperty<DateTime> LastUpdatedProperty =
|
|
|
+ AvaloniaProperty.Register<AvaloniaDataGrid, DateTime>(nameof(LastUpdated));
|
|
|
+ public static StyledProperty<bool> ShowRecordCountProperty =
|
|
|
+ AvaloniaProperty.Register<AvaloniaDataGrid, bool>(nameof(ShowRecordCount));
|
|
|
+ public static StyledProperty<bool> RefreshVisibleProperty =
|
|
|
+ AvaloniaProperty.Register<AvaloniaDataGrid, bool>(nameof(RefreshVisible));
|
|
|
+ public static StyledProperty<SelectionMode> SelectionModeProperty =
|
|
|
+ AvaloniaProperty.Register<AvaloniaDataGrid, SelectionMode>(nameof(SelectionMode), SelectionMode.Single);
|
|
|
+ public static StyledProperty<double> RowHeightProperty =
|
|
|
+ AvaloniaProperty.Register<AvaloniaDataGrid, double>(nameof(RowHeight), 30);
|
|
|
+
|
|
|
+ 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<object?> SelectedItems => Grid.SelectedItems.Cast<object?>();
|
|
|
+
|
|
|
+ public event EventHandler<AvaloniaDataGridSelectionChangedEventArgs>? SelectionChanged;
|
|
|
+ public event EventHandler<AvaloniaDataGridRefreshRequestedEventArgs>? RefreshRequested;
|
|
|
+
|
|
|
+ #region Static Constructor and Property Changed Handlers
|
|
|
+
|
|
|
+ static AvaloniaDataGrid()
|
|
|
+ {
|
|
|
+ ItemsSourceProperty.Changed.AddClassHandler<AvaloniaDataGrid>(ItemsSource_Changed);
|
|
|
+ LastUpdatedProperty.Changed.AddClassHandler<AvaloniaDataGrid>(LastUpdated_Changed);
|
|
|
+ ShowRecordCountProperty.Changed.AddClassHandler<AvaloniaDataGrid>(ShowRecordCount_Changed);
|
|
|
+ }
|
|
|
+
|
|
|
+ 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;
|
|
|
+ grid.Grid.CollectionView.CollectionChanged -= grid.CollectionView_CollectionChanged;
|
|
|
+ grid.Grid.CollectionView.CollectionChanged += grid.CollectionView_CollectionChanged;
|
|
|
+ grid.UpdateSummaryRow();
|
|
|
+ }
|
|
|
+
|
|
|
+ #endregion
|
|
|
+
|
|
|
+ public AvaloniaDataGrid()
|
|
|
+ {
|
|
|
+ InitializeComponent();
|
|
|
+
|
|
|
+ Columns = new AvaloniaDataGridColumns();
|
|
|
+ Columns.Changed += Columns_Changed;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void CollectionView_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
|
|
|
+ {
|
|
|
+ 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()
|
|
|
+ {
|
|
|
+ Grid.CollectionView.Filter = DoSearch;
|
|
|
+ Grid.CollectionView.Refresh();
|
|
|
+ UpdateSummaryRow();
|
|
|
+ }
|
|
|
+
|
|
|
+ [RelayCommand]
|
|
|
+ private void Refresh()
|
|
|
+ {
|
|
|
+ RefreshRequested?.Invoke(this, new());
|
|
|
+ Grid.CollectionView.Refresh();
|
|
|
+ }
|
|
|
+
|
|
|
+ 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<DataGridCellsPresenter>();
|
|
|
+ if (cellCollection is null) return;
|
|
|
+
|
|
|
+ var colIdx = cellCollection.Children.IndexOf(cell);
|
|
|
+
|
|
|
+ var row = cellCollection.GetVisualAncestors().OfType<DataGridRow>().FirstOrDefault();
|
|
|
+ if (row is null) return;
|
|
|
+
|
|
|
+ var rowCollection = row.GetVisualParent<DataGridRowsPresenter>();
|
|
|
+ if (rowCollection is null) return;
|
|
|
+
|
|
|
+ var rowIdx = rowCollection.Children.IndexOf(row);
|
|
|
+
|
|
|
+ var item = (Grid.CollectionView as DataGridCollectionView)?[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<DataGridColumnHeadersPresenter>();
|
|
|
+ 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<object?>()));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|