AvaloniaDataGrid.axaml.cs 12 KB

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