AvaloniaDataGrid.axaml.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  1. using Avalonia;
  2. using Avalonia.Collections;
  3. using Avalonia.Controls;
  4. using Avalonia.Controls.Primitives;
  5. using Avalonia.Data;
  6. using Avalonia.Data.Converters;
  7. using Avalonia.Input;
  8. using Avalonia.VisualTree;
  9. using CommunityToolkit.Mvvm.Input;
  10. using InABox.Core;
  11. using System.Collections;
  12. using System.ComponentModel;
  13. namespace InABox.Avalonia.Components;
  14. public class AvaloniaDataGridSelectionChangedEventArgs(object?[] selection)
  15. {
  16. public object?[] Selection { get; set; } = selection;
  17. }
  18. public class AvaloniaDataGridRefreshRequestedEventArgs()
  19. {
  20. }
  21. public enum AvaloniaDataGridSelectionMode
  22. {
  23. None,
  24. Single,
  25. Multiple
  26. }
  27. public partial class AvaloniaDataGrid : UserControl, INotifyPropertyChanged
  28. {
  29. public static StyledProperty<bool> CanSearchProperty =
  30. AvaloniaProperty.Register<AvaloniaDataGrid, bool>(nameof(CanSearch), true);
  31. public static StyledProperty<IEnumerable> ItemsSourceProperty =
  32. AvaloniaProperty.Register<AvaloniaDataGrid, IEnumerable>(nameof(ItemsSource));
  33. public static StyledProperty<DateTime> LastUpdatedProperty =
  34. AvaloniaProperty.Register<AvaloniaDataGrid, DateTime>(nameof(LastUpdated));
  35. public static StyledProperty<bool> ShowRecordCountProperty =
  36. AvaloniaProperty.Register<AvaloniaDataGrid, bool>(nameof(ShowRecordCount));
  37. public static StyledProperty<bool> RefreshVisibleProperty =
  38. AvaloniaProperty.Register<AvaloniaDataGrid, bool>(nameof(RefreshVisible));
  39. public static StyledProperty<AvaloniaDataGridSelectionMode> SelectionModeProperty =
  40. AvaloniaProperty.Register<AvaloniaDataGrid, AvaloniaDataGridSelectionMode>(nameof(SelectionMode), AvaloniaDataGridSelectionMode.Single);
  41. public static StyledProperty<double> RowHeightProperty =
  42. AvaloniaProperty.Register<AvaloniaDataGrid, double>(nameof(RowHeight), 30);
  43. public static StyledProperty<AvaloniaDataGridColumns?> ColumnsProperty =
  44. AvaloniaProperty.Register<AvaloniaDataGrid, AvaloniaDataGridColumns?>(nameof(Columns), null);
  45. public string SearchText { get; set; } = "";
  46. public bool CanSearch
  47. {
  48. get => GetValue(CanSearchProperty);
  49. set => SetValue(CanSearchProperty, value);
  50. }
  51. public IEnumerable ItemsSource
  52. {
  53. get => GetValue(ItemsSourceProperty);
  54. set => SetValue(ItemsSourceProperty, value);
  55. }
  56. public bool ShowRecordCount
  57. {
  58. get => GetValue(ShowRecordCountProperty);
  59. set => SetValue(ShowRecordCountProperty, value);
  60. }
  61. public bool RefreshVisible
  62. {
  63. get => GetValue(RefreshVisibleProperty);
  64. set => SetValue(RefreshVisibleProperty, value);
  65. }
  66. public AvaloniaDataGridSelectionMode SelectionMode
  67. {
  68. get => GetValue(SelectionModeProperty);
  69. set => SetValue(SelectionModeProperty, value);
  70. }
  71. public double RowHeight
  72. {
  73. get => GetValue(RowHeightProperty);
  74. set => SetValue(RowHeightProperty, value);
  75. }
  76. public int ItemCount { get; set; }
  77. public DateTime LastUpdated
  78. {
  79. get => GetValue(LastUpdatedProperty);
  80. set => SetValue(LastUpdatedProperty, value);
  81. }
  82. public AvaloniaDataGridColumns Columns { get; private set; }
  83. public IEnumerable<object?> SelectedItems => Grid.SelectedItems.Cast<object?>();
  84. public event EventHandler<AvaloniaDataGridSelectionChangedEventArgs>? SelectionChanged;
  85. public event EventHandler<AvaloniaDataGridRefreshRequestedEventArgs>? RefreshRequested;
  86. public event Predicate<object?>? FilterRow;
  87. public event EventHandler<DataGridRowEventArgs>? LoadingRow;
  88. #region Static Constructor and Property Changed Handlers
  89. static AvaloniaDataGrid()
  90. {
  91. ItemsSourceProperty.Changed.AddClassHandler<AvaloniaDataGrid>(ItemsSource_Changed);
  92. LastUpdatedProperty.Changed.AddClassHandler<AvaloniaDataGrid>(LastUpdated_Changed);
  93. ShowRecordCountProperty.Changed.AddClassHandler<AvaloniaDataGrid>(ShowRecordCount_Changed);
  94. ColumnsProperty.Changed.AddClassHandler<AvaloniaDataGrid>(ColumnsProperty_Changed);
  95. }
  96. private static void ColumnsProperty_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args)
  97. {
  98. var columns = grid.GetValue(ColumnsProperty);
  99. if(columns is not null)
  100. {
  101. grid.Columns.BeginUpdate().AddRange(columns).EndUpdate();
  102. }
  103. }
  104. private static void ShowRecordCount_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args)
  105. {
  106. grid.UpdateSummaryRow();
  107. }
  108. private static void LastUpdated_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args)
  109. {
  110. grid.UpdateSummaryRow();
  111. }
  112. private static void ItemsSource_Changed(AvaloniaDataGrid grid, AvaloniaPropertyChangedEventArgs args)
  113. {
  114. grid.Grid.ItemsSource = grid.ItemsSource;
  115. if (grid.Grid.CollectionView is not null)
  116. {
  117. grid.Grid.CollectionView.CollectionChanged -= grid.CollectionView_CollectionChanged;
  118. grid.Grid.CollectionView.CollectionChanged += grid.CollectionView_CollectionChanged;
  119. }
  120. grid.ItemsChanged();
  121. }
  122. #endregion
  123. public AvaloniaDataGrid()
  124. {
  125. InitializeComponent();
  126. Columns = new AvaloniaDataGridColumns();
  127. Columns.Changed += Columns_Changed;
  128. Grid.Bind(DataGrid.SelectionModeProperty, new Binding(nameof(SelectionMode))
  129. {
  130. Source = this,
  131. Converter = new FuncValueConverter<AvaloniaDataGridSelectionMode, DataGridSelectionMode>(x => x switch
  132. {
  133. AvaloniaDataGridSelectionMode.Multiple => DataGridSelectionMode.Extended,
  134. AvaloniaDataGridSelectionMode.Single or AvaloniaDataGridSelectionMode.None or _ => DataGridSelectionMode.Single,
  135. })
  136. });
  137. }
  138. private void DataGrid_LoadingRow(object? sender, DataGridRowEventArgs e)
  139. {
  140. LoadingRow?.Invoke(this, e);
  141. }
  142. private void CollectionView_CollectionChanged(object? sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
  143. {
  144. ItemsChanged();
  145. }
  146. private void ItemsChanged()
  147. {
  148. ItemCount = (Grid.CollectionView as DataGridCollectionView)?.ItemCount ?? 0;
  149. UpdateSummaryRow();
  150. }
  151. private void Columns_Changed(AvaloniaDataGridColumns columns)
  152. {
  153. Grid.Columns.Clear();
  154. foreach(var column in columns)
  155. {
  156. Grid.Columns.Add(column.CreateColumn());
  157. }
  158. // Summaries
  159. var searchableColumns = Columns.Any(x => x.Searchable);
  160. SearchBar.IsVisible = searchableColumns && CanSearch;
  161. }
  162. private void UpdateSummaryRow()
  163. {
  164. _lastUpdated.IsVisible = LastUpdated != DateTime.MinValue;
  165. _recordCount.Content = $" {ItemCount} records";
  166. _recordCount.IsVisible = ShowRecordCount && ItemsSource is IEnumerable;
  167. _recordCountBox.IsVisible = _recordCount.IsVisible || _lastUpdated.IsVisible;
  168. }
  169. public void ClearSelection()
  170. {
  171. Grid.SelectedItem = null;
  172. Grid.SelectedItems.Clear();
  173. }
  174. private bool DoSearch(object? item)
  175. {
  176. if (SearchText.IsNullOrWhiteSpace()) return true;
  177. if (item is null) return false;
  178. foreach(var column in Columns)
  179. {
  180. if(column.Filter(item, SearchText)) return true;
  181. }
  182. return false;
  183. }
  184. private bool DoFilter(object? item)
  185. {
  186. return DoSearch(item) && (FilterRow is null || FilterRow(item));
  187. }
  188. public void InvalidateGrid()
  189. {
  190. if (Grid.CollectionView is null) return;
  191. Grid.CollectionView.Filter = DoFilter;
  192. Grid.CollectionView.Refresh();
  193. UpdateSummaryRow();
  194. }
  195. [RelayCommand]
  196. private void Search()
  197. {
  198. if (Grid.CollectionView is null) return;
  199. Grid.CollectionView.Filter = DoFilter;
  200. Grid.CollectionView.Refresh();
  201. UpdateSummaryRow();
  202. }
  203. [RelayCommand]
  204. private void Refresh()
  205. {
  206. if (Grid.CollectionView is null) return;
  207. RefreshRequested?.Invoke(this, new());
  208. Grid.CollectionView.Refresh();
  209. }
  210. private void DataGrid_SelectionChanged(object sender, SelectionChangedEventArgs e)
  211. {
  212. if(SelectionMode == AvaloniaDataGridSelectionMode.None && Grid.SelectedItems.Count > 0)
  213. {
  214. e.Handled = true;
  215. Grid.SelectedItem = null;
  216. return;
  217. }
  218. if (SelectionMode != AvaloniaDataGridSelectionMode.Multiple) return;
  219. SelectionChanged?.Invoke(this, new AvaloniaDataGridSelectionChangedEventArgs(SelectedItems.ToArray()));
  220. }
  221. private void DataGrid_Tapped(object sender, TappedEventArgs e)
  222. {
  223. if (SelectionMode == AvaloniaDataGridSelectionMode.Multiple) return;
  224. var position = e.GetPosition(Grid);
  225. var parent = (e.Source as Visual)?.GetVisualAncestors().Where(x => x is DataGridCell || x is DataGridColumnHeader).FirstOrDefault();
  226. if (parent is null) return;
  227. if(parent is DataGridCell cell)
  228. {
  229. var cellCollection = cell.GetVisualParent<DataGridCellsPresenter>();
  230. if (cellCollection is null) return;
  231. var colIdx = cellCollection.Children.IndexOf(cell);
  232. var row = cellCollection.GetVisualAncestors().OfType<DataGridRow>().FirstOrDefault();
  233. if (row is null) return;
  234. var rowCollection = row.GetVisualParent<DataGridRowsPresenter>();
  235. if (rowCollection is null) return;
  236. var rowIdx = row.Index;
  237. var item = (Grid.CollectionView as DataGridCollectionView)?.GetItemAt(rowIdx);
  238. var column = Columns[colIdx];
  239. if(column.Tapped is not null)
  240. {
  241. column.Tapped?.Invoke(column, item);
  242. }
  243. else
  244. {
  245. SelectionChanged?.Invoke(this, new AvaloniaDataGridSelectionChangedEventArgs(new object?[] { item }));
  246. }
  247. }
  248. else if(parent is DataGridColumnHeader header)
  249. {
  250. var headerCollection = header.GetVisualParent<DataGridColumnHeadersPresenter>();
  251. if (headerCollection is null) return;
  252. var colIdx = headerCollection.Children.IndexOf(header);
  253. var column = Columns[colIdx];
  254. if(column.Tapped is not null)
  255. {
  256. column.Tapped?.Invoke(column, null);
  257. }
  258. else
  259. {
  260. SelectionChanged?.Invoke(this, new AvaloniaDataGridSelectionChangedEventArgs(Array.Empty<object?>()));
  261. }
  262. }
  263. }
  264. }