Skip to content

Commit 78ebc02

Browse files
authoredMay 14, 2023
Merge pull request #178 from AvaloniaUI/feature/cell-selection
MVP of cell selection
2 parents 51336cf + 9f9ed62 commit 78ebc02

35 files changed

+912
-137
lines changed
 

‎docs/column-types.md

+18
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ This is the signature of the `TextColumn` constructor. There are two most import
2020
**Note**:
2121
The sample above is taken from [this article](https://github.com/AvaloniaUI/Avalonia.Controls.TreeDataGrid/blob/master/docs/get-started-flat.md). If you feel like you need more examples feel free to check it, there is a sample that shows how to use TextColumns and how to run a whole `TreeDataGrid` using them.
2222

23+
## CheckBoxColumn
24+
25+
As its name suggests, `CheckBoxColumn` displays a `CheckBox` in its cells. For a readonly checkbox:
26+
27+
```csharp
28+
new CheckColumn<Person>("Firstborn", x => x.IsFirstborn)
29+
```
30+
31+
The first parameter defines the column header. The second parameter is an expression which gets the value of the property from the model.
32+
33+
For a read/write checkbox:
34+
35+
```csharp
36+
new CheckColumn<Person>("Firstborn", x => x.IsFirstborn, (o, v) => o.IsFirstborn = v)
37+
```
38+
39+
This overload adds a second paramter which is the expression used to set the property in the model.
40+
2341
## HierarchicalExpanderColumn
2442
`HierarchicalExpanderColumn` can be used only with `HierarchicalTreeDataGrid` (a.k.a TreeView) thats what Hierarchical stands for in its name, also it can be used only with `HierarchicalTreeDataGridSource`. This type of columns can be useful when you want cells to show an expander to reveal nested data.
2543

‎docs/selection.md

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Selection
2+
3+
Two selection modes are supported:
4+
5+
- Row selection allows the user to select whole rows
6+
- Cell selection allows the user to select individial cells
7+
8+
Both selection types support either single or multiple selection. The default selection type is single row selection.
9+
10+
## Index Paths
11+
12+
Because `TreeDataGrid` supports hierarchical data, using a simple index to identify a row in the data source isn't enough. Instead indexes are represented using the `IndexPath` struct.
13+
14+
An `IndexPath` is essentially an array of indexes, each element of which specifies the index at a succesively deeper level in the hierarchy of the data.
15+
16+
Consider the following data source:
17+
18+
```
19+
|- A
20+
| |- B
21+
| |- C
22+
| |- D
23+
|- E
24+
```
25+
26+
- `A` has an index path of `0` as it's the first item at the root of the hierarchy
27+
- `B` has an index path of `0,0` as it's the first child of the first item
28+
- `C` has an index path of `0,1` as it's the second child of the first item
29+
- `D` has an index path of `0,1,0` as it's the first child of `C`
30+
- `E` has an index path of `1` as it's the second item in the root
31+
32+
`IndexPath` is an immutable struct which is constructed with an array of integers, e.g.: `new ItemPath(0, 1, 0)`. There is also an implicit conversion from `int` for when working with a flat data source.
33+
34+
## Row Selection
35+
36+
Row selection is the default and is exposed via the `RowSelection` property on the `FlatTreeDataGridSource<TModel>` and `HierarchicalTreeDataGridSource<TModel>` classes when enabled. Row selection is stored in an instance of the `TreeDataGridRowSelectionModel<TModel>` class.
37+
38+
By default is single selection. To enable multiple selection set the the `SingleSelect` property to `false`, e.g.:
39+
40+
```csharp
41+
Source = new FlatTreeDataGridSource<Person>(_people)
42+
{
43+
Columns =
44+
{
45+
new TextColumn<Person, string>("First Name", x => x.FirstName),
46+
new TextColumn<Person, string>("Last Name", x => x.LastName),
47+
new TextColumn<Person, int>("Age", x => x.Age),
48+
},
49+
};
50+
51+
Source.RowSelection!.SingleSelect = false;
52+
```
53+
54+
The properties on `ITreeDataGridRowSelectionModel<TModel>` can be used to manipulate the selection, e.g.:
55+
56+
```csharp
57+
Source.RowSelection!.SelectedIndex = 1;
58+
```
59+
60+
Or
61+
62+
```csharp
63+
Source.RowSelection!.SelectedIndex = new IndexPath(0, 1);
64+
```
65+
66+
## Cell Selection
67+
68+
To enable cell selection for a `TreeDataGridSource`, assign an instance of `TreeDataGridCellSelectionModel<TModel>` to the source's `Selection` property:
69+
70+
```csharp
71+
Source = new FlatTreeDataGridSource<Person>(_people)
72+
{
73+
Columns =
74+
{
75+
new TextColumn<Person, string>("First Name", x => x.FirstName),
76+
new TextColumn<Person, string>("Last Name", x => x.LastName),
77+
new TextColumn<Person, int>("Age", x => x.Age),
78+
},
79+
};
80+
81+
Source.Selection = new TreeDataGridCellSelectionModel<Person>(Source);
82+
```
83+
84+
Or for multiple cell selection:
85+
86+
```csharp
87+
Source.Selection = new TreeDataGridCellSelectionModel<Person>(Source) { SingleSelect = false };
88+
```
89+
90+
Cell selection is is exposed via the `CellSelection` property on the `FlatTreeDataGridSource<TModel>` and `HierarchicalTreeDataGridSource<TModel>` classes when enabled.
91+
92+
The `CellIndex` struct indentifies an individual cell with by combination of an integer column index and an `IndexPath` row index.

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,4 @@ We accept issues and pull requests but we answer and review only pull requests a
3232
- [Creating a flat `TreeDataGrid`](docs/get-started-flat.md)
3333
- [Creating a hierarchical `TreeDataGrid`](docs/get-started-hierarchical.md)
3434
- [Supported column types](docs/column-types.md)
35+
- [Selection](docs/selection.md)

‎samples/TreeDataGridDemo/MainWindow.axaml

+7-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
<TabItem Header="Countries">
1212
<DockPanel>
1313
<TextBlock Classes="realized-count" DockPanel.Dock="Bottom"/>
14-
<StackPanel DockPanel.Dock="Right" Spacing="4">
14+
<StackPanel DockPanel.Dock="Right" Spacing="4" Margin="4 0 0 0">
15+
<CheckBox IsChecked="{Binding Countries.CellSelection}">Cell Selection</CheckBox>
1516
<Label Target="countryTextBox">_Country</Label>
1617
<TextBox Name="countryTextBox">Sealand</TextBox>
1718
<Label Target="regionTextBox">_Region</Label>
@@ -48,6 +49,11 @@
4849
<ComboBox ItemsSource="{Binding Files.Drives}"
4950
SelectedItem="{Binding Files.SelectedDrive}"
5051
DockPanel.Dock="Left"/>
52+
<CheckBox IsChecked="{Binding Files.CellSelection}"
53+
Margin="4 0 0 0"
54+
DockPanel.Dock="Right">
55+
Cell Selection
56+
</CheckBox>
5157
<TextBox Text="{Binding Files.SelectedPath, Mode=OneWay}"
5258
Margin="4 0 0 0"
5359
VerticalContentAlignment="Center"

‎samples/TreeDataGridDemo/ViewModels/CountriesPageViewModel.cs

+22-3
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33
using Avalonia.Controls;
44
using Avalonia.Controls.Models.TreeDataGrid;
55
using Avalonia.Controls.Selection;
6+
using ReactiveUI;
67
using TreeDataGridDemo.Models;
78

89
namespace TreeDataGridDemo.ViewModels
910
{
10-
internal class CountriesPageViewModel
11+
internal class CountriesPageViewModel : ReactiveObject
1112
{
1213
private readonly ObservableCollection<Country> _data;
14+
private bool _cellSelection;
1315

1416
public CountriesPageViewModel()
1517
{
@@ -20,8 +22,8 @@ public CountriesPageViewModel()
2022
Columns =
2123
{
2224
new TextColumn<Country, string>("Country", x => x.Name, (r, v) => r.Name = v, new GridLength(6, GridUnitType.Star), new()
23-
{
24-
IsTextSearchEnabled = true
25+
{
26+
IsTextSearchEnabled = true
2527
}),
2628
new TextColumn<Country, string>("Region", x => x.Region, new GridLength(4, GridUnitType.Star)),
2729
new TextColumn<Country, int>("Population", x => x.Population, new GridLength(3, GridUnitType.Star)),
@@ -35,6 +37,23 @@ public CountriesPageViewModel()
3537
Source.RowSelection!.SingleSelect = false;
3638
}
3739

40+
public bool CellSelection
41+
{
42+
get => _cellSelection;
43+
set
44+
{
45+
if (_cellSelection != value)
46+
{
47+
_cellSelection = value;
48+
if (_cellSelection)
49+
Source.Selection = new TreeDataGridCellSelectionModel<Country>(Source) { SingleSelect = false };
50+
else
51+
Source.Selection = new TreeDataGridRowSelectionModel<Country>(Source) { SingleSelect = false };
52+
this.RaisePropertyChanged();
53+
}
54+
}
55+
}
56+
3857
public FlatTreeDataGridSource<Country> Source { get; }
3958

4059
public void AddCountry(Country country) => _data.Add(country);

‎samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs

+18
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace TreeDataGridDemo.ViewModels
2020
public class FilesPageViewModel : ReactiveObject
2121
{
2222
private static IconConverter? s_iconConverter;
23+
private bool _cellSelection;
2324
private FileTreeNodeModel? _root;
2425
private string _selectedDrive;
2526
private string? _selectedPath;
@@ -94,6 +95,23 @@ public FilesPageViewModel()
9495
});
9596
}
9697

98+
public bool CellSelection
99+
{
100+
get => _cellSelection;
101+
set
102+
{
103+
if (_cellSelection != value)
104+
{
105+
_cellSelection = value;
106+
if (_cellSelection)
107+
Source.Selection = new TreeDataGridCellSelectionModel<FileTreeNodeModel>(Source) { SingleSelect = false };
108+
else
109+
Source.Selection = new TreeDataGridRowSelectionModel<FileTreeNodeModel>(Source) { SingleSelect = false };
110+
this.RaisePropertyChanged();
111+
}
112+
}
113+
}
114+
97115
public IList<string> Drives { get; }
98116

99117
public string SelectedDrive

‎src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<PropertyGroup>
33
<TargetFramework>net5.0</TargetFramework>
44
<IsPackable>True</IsPackable>
5+
<LangVersion>10</LangVersion>
56
<RootNamespace>Avalonia.Controls</RootNamespace>
67
</PropertyGroup>
78
<PropertyGroup>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace Avalonia.Controls
2+
{
3+
/// <summary>
4+
/// Represents a cell in a <see cref="TreeDataGrid"/>.
5+
/// </summary>
6+
/// <param name="ColumnIndex">
7+
/// The index of the cell in the <see cref="TreeDataGrid.Columns"/> collection.
8+
/// </param>
9+
/// <param name="RowIndex">
10+
/// The hierarchical index of the row model in the data source.
11+
/// </param>
12+
public readonly record struct CellIndex(int ColumnIndex, IndexPath RowIndex);
13+
}

‎src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs

+15-6
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Collections.Generic;
33
using System.ComponentModel;
44
using System.Linq;
5+
using Avalonia.Controls.Models;
56
using Avalonia.Controls.Models.TreeDataGrid;
67
using Avalonia.Controls.Selection;
78
using Avalonia.Input;
@@ -12,8 +13,10 @@ namespace Avalonia.Controls
1213
/// A data source for a <see cref="TreeDataGrid"/> which displays a flat grid.
1314
/// </summary>
1415
/// <typeparam name="TModel">The model type.</typeparam>
15-
public class FlatTreeDataGridSource<TModel> : ITreeDataGridSource<TModel>, IDisposable
16-
where TModel: class
16+
public class FlatTreeDataGridSource<TModel> : NotifyingBase,
17+
ITreeDataGridSource<TModel>,
18+
IDisposable
19+
where TModel: class
1720
{
1821
private IEnumerable<TModel> _items;
1922
private TreeDataGridItemsSourceView<TModel> _itemsView;
@@ -45,6 +48,7 @@ public IEnumerable<TModel> Items
4548
_rows?.SetItems(_itemsView);
4649
if (_selection is object)
4750
_selection.Source = value;
51+
RaisePropertyChanged();
4852
}
4953
}
5054
}
@@ -59,13 +63,18 @@ public ITreeDataGridSelection? Selection
5963
}
6064
set
6165
{
62-
if (_selection is object)
63-
throw new InvalidOperationException("Selection is already initialized.");
64-
_selection = value;
65-
_isSelectionSet = true;
66+
if (_selection != value)
67+
{
68+
if (value?.Source != _items)
69+
throw new InvalidOperationException("Selection source must be set to Items.");
70+
_selection = value;
71+
_isSelectionSet = true;
72+
RaisePropertyChanged();
73+
}
6674
}
6775
}
6876

77+
public ITreeDataGridCellSelectionModel<TModel>? CellSelection => Selection as ITreeDataGridCellSelectionModel<TModel>;
6978
public ITreeDataGridRowSelectionModel<TModel>? RowSelection => Selection as ITreeDataGridRowSelectionModel<TModel>;
7079
public bool IsHierarchical => false;
7180
public bool IsSorted => _comparer is not null;

‎src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs

+12-5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.ComponentModel;
55
using System.Diagnostics.CodeAnalysis;
66
using System.Linq;
7+
using Avalonia.Controls.Models;
78
using Avalonia.Controls.Models.TreeDataGrid;
89
using Avalonia.Controls.Selection;
910
using Avalonia.Input;
@@ -15,7 +16,8 @@ namespace Avalonia.Controls
1516
/// row may have multiple columns.
1617
/// </summary>
1718
/// <typeparam name="TModel">The model type.</typeparam>
18-
public class HierarchicalTreeDataGridSource<TModel> : ITreeDataGridSource<TModel>,
19+
public class HierarchicalTreeDataGridSource<TModel> : NotifyingBase,
20+
ITreeDataGridSource<TModel>,
1921
IDisposable,
2022
IExpanderRowController<TModel>
2123
where TModel: class
@@ -70,13 +72,18 @@ public ITreeDataGridSelection? Selection
7072
}
7173
set
7274
{
73-
if (_selection is object)
74-
throw new InvalidOperationException("Selection is already initialized.");
75-
_selection = value;
76-
_isSelectionSet = true;
75+
if (_selection != value)
76+
{
77+
if (value?.Source != _items)
78+
throw new InvalidOperationException("Selection source must be set to Items.");
79+
_selection = value;
80+
_isSelectionSet = true;
81+
RaisePropertyChanged();
82+
}
7783
}
7884
}
7985

86+
public ITreeDataGridCellSelectionModel<TModel>? CellSelection => Selection as ITreeDataGridCellSelectionModel<TModel>;
8087
public ITreeDataGridRowSelectionModel<TModel>? RowSelection => Selection as ITreeDataGridRowSelectionModel<TModel>;
8188
public bool IsHierarchical => true;
8289
public bool IsSorted => _comparison is not null;

‎src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Avalonia.Controls
1010
/// <summary>
1111
/// Represents a data source for a <see cref="TreeDataGrid"/> control.
1212
/// </summary>
13-
public interface ITreeDataGridSource
13+
public interface ITreeDataGridSource : INotifyPropertyChanged
1414
{
1515
/// <summary>
1616
/// Gets the columns to be displayed.
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
using Avalonia.Controls.Models.TreeDataGrid;
2+
using Avalonia.Controls.Selection;
23

34
namespace Avalonia.Controls.Primitives
45
{
56
internal interface ITreeDataGridCell
67
{
78
int ColumnIndex { get; }
89

9-
void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex);
10+
void Realize(
11+
TreeDataGridElementFactory factory,
12+
ITreeDataGridSelectionInteraction? selection,
13+
ICell model,
14+
int columnIndex,
15+
int rowIndex);
16+
1017
void Unrealize();
1118
}
1219
}

0 commit comments

Comments
 (0)
Please sign in to comment.