Skip to content

Commit

Permalink
Merge pull request #178 from AvaloniaUI/feature/cell-selection
Browse files Browse the repository at this point in the history
MVP of cell selection
  • Loading branch information
maxkatz6 authored May 14, 2023
2 parents 51336cf + 9f9ed62 commit 78ebc02
Show file tree
Hide file tree
Showing 35 changed files with 912 additions and 137 deletions.
18 changes: 18 additions & 0 deletions docs/column-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ This is the signature of the `TextColumn` constructor. There are two most import
**Note**:
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.

## CheckBoxColumn

As its name suggests, `CheckBoxColumn` displays a `CheckBox` in its cells. For a readonly checkbox:

```csharp
new CheckColumn<Person>("Firstborn", x => x.IsFirstborn)
```

The first parameter defines the column header. The second parameter is an expression which gets the value of the property from the model.

For a read/write checkbox:

```csharp
new CheckColumn<Person>("Firstborn", x => x.IsFirstborn, (o, v) => o.IsFirstborn = v)
```

This overload adds a second paramter which is the expression used to set the property in the model.

## HierarchicalExpanderColumn
`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.

Expand Down
92 changes: 92 additions & 0 deletions docs/selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Selection

Two selection modes are supported:

- Row selection allows the user to select whole rows
- Cell selection allows the user to select individial cells

Both selection types support either single or multiple selection. The default selection type is single row selection.

## Index Paths

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.

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.

Consider the following data source:

```
|- A
| |- B
| |- C
| |- D
|- E
```

- `A` has an index path of `0` as it's the first item at the root of the hierarchy
- `B` has an index path of `0,0` as it's the first child of the first item
- `C` has an index path of `0,1` as it's the second child of the first item
- `D` has an index path of `0,1,0` as it's the first child of `C`
- `E` has an index path of `1` as it's the second item in the root

`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.

## Row Selection

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.

By default is single selection. To enable multiple selection set the the `SingleSelect` property to `false`, e.g.:

```csharp
Source = new FlatTreeDataGridSource<Person>(_people)
{
Columns =
{
new TextColumn<Person, string>("First Name", x => x.FirstName),
new TextColumn<Person, string>("Last Name", x => x.LastName),
new TextColumn<Person, int>("Age", x => x.Age),
},
};

Source.RowSelection!.SingleSelect = false;
```

The properties on `ITreeDataGridRowSelectionModel<TModel>` can be used to manipulate the selection, e.g.:

```csharp
Source.RowSelection!.SelectedIndex = 1;
```

Or

```csharp
Source.RowSelection!.SelectedIndex = new IndexPath(0, 1);
```

## Cell Selection

To enable cell selection for a `TreeDataGridSource`, assign an instance of `TreeDataGridCellSelectionModel<TModel>` to the source's `Selection` property:

```csharp
Source = new FlatTreeDataGridSource<Person>(_people)
{
Columns =
{
new TextColumn<Person, string>("First Name", x => x.FirstName),
new TextColumn<Person, string>("Last Name", x => x.LastName),
new TextColumn<Person, int>("Age", x => x.Age),
},
};

Source.Selection = new TreeDataGridCellSelectionModel<Person>(Source);
```

Or for multiple cell selection:

```csharp
Source.Selection = new TreeDataGridCellSelectionModel<Person>(Source) { SingleSelect = false };
```

Cell selection is is exposed via the `CellSelection` property on the `FlatTreeDataGridSource<TModel>` and `HierarchicalTreeDataGridSource<TModel>` classes when enabled.

The `CellIndex` struct indentifies an individual cell with by combination of an integer column index and an `IndexPath` row index.
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ We accept issues and pull requests but we answer and review only pull requests a
- [Creating a flat `TreeDataGrid`](docs/get-started-flat.md)
- [Creating a hierarchical `TreeDataGrid`](docs/get-started-hierarchical.md)
- [Supported column types](docs/column-types.md)
- [Selection](docs/selection.md)
8 changes: 7 additions & 1 deletion samples/TreeDataGridDemo/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
<TabItem Header="Countries">
<DockPanel>
<TextBlock Classes="realized-count" DockPanel.Dock="Bottom"/>
<StackPanel DockPanel.Dock="Right" Spacing="4">
<StackPanel DockPanel.Dock="Right" Spacing="4" Margin="4 0 0 0">
<CheckBox IsChecked="{Binding Countries.CellSelection}">Cell Selection</CheckBox>
<Label Target="countryTextBox">_Country</Label>
<TextBox Name="countryTextBox">Sealand</TextBox>
<Label Target="regionTextBox">_Region</Label>
Expand Down Expand Up @@ -48,6 +49,11 @@
<ComboBox ItemsSource="{Binding Files.Drives}"
SelectedItem="{Binding Files.SelectedDrive}"
DockPanel.Dock="Left"/>
<CheckBox IsChecked="{Binding Files.CellSelection}"
Margin="4 0 0 0"
DockPanel.Dock="Right">
Cell Selection
</CheckBox>
<TextBox Text="{Binding Files.SelectedPath, Mode=OneWay}"
Margin="4 0 0 0"
VerticalContentAlignment="Center"
Expand Down
25 changes: 22 additions & 3 deletions samples/TreeDataGridDemo/ViewModels/CountriesPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Selection;
using ReactiveUI;
using TreeDataGridDemo.Models;

namespace TreeDataGridDemo.ViewModels
{
internal class CountriesPageViewModel
internal class CountriesPageViewModel : ReactiveObject
{
private readonly ObservableCollection<Country> _data;
private bool _cellSelection;

public CountriesPageViewModel()
{
Expand All @@ -20,8 +22,8 @@ public CountriesPageViewModel()
Columns =
{
new TextColumn<Country, string>("Country", x => x.Name, (r, v) => r.Name = v, new GridLength(6, GridUnitType.Star), new()
{
IsTextSearchEnabled = true
{
IsTextSearchEnabled = true
}),
new TextColumn<Country, string>("Region", x => x.Region, new GridLength(4, GridUnitType.Star)),
new TextColumn<Country, int>("Population", x => x.Population, new GridLength(3, GridUnitType.Star)),
Expand All @@ -35,6 +37,23 @@ public CountriesPageViewModel()
Source.RowSelection!.SingleSelect = false;
}

public bool CellSelection
{
get => _cellSelection;
set
{
if (_cellSelection != value)
{
_cellSelection = value;
if (_cellSelection)
Source.Selection = new TreeDataGridCellSelectionModel<Country>(Source) { SingleSelect = false };
else
Source.Selection = new TreeDataGridRowSelectionModel<Country>(Source) { SingleSelect = false };
this.RaisePropertyChanged();
}
}
}

public FlatTreeDataGridSource<Country> Source { get; }

public void AddCountry(Country country) => _data.Add(country);
Expand Down
18 changes: 18 additions & 0 deletions samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ namespace TreeDataGridDemo.ViewModels
public class FilesPageViewModel : ReactiveObject
{
private static IconConverter? s_iconConverter;
private bool _cellSelection;
private FileTreeNodeModel? _root;
private string _selectedDrive;
private string? _selectedPath;
Expand Down Expand Up @@ -94,6 +95,23 @@ public FilesPageViewModel()
});
}

public bool CellSelection
{
get => _cellSelection;
set
{
if (_cellSelection != value)
{
_cellSelection = value;
if (_cellSelection)
Source.Selection = new TreeDataGridCellSelectionModel<FileTreeNodeModel>(Source) { SingleSelect = false };
else
Source.Selection = new TreeDataGridRowSelectionModel<FileTreeNodeModel>(Source) { SingleSelect = false };
this.RaisePropertyChanged();
}
}
}

public IList<string> Drives { get; }

public string SelectedDrive
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
<PropertyGroup>
<TargetFramework>net5.0</TargetFramework>
<IsPackable>True</IsPackable>
<LangVersion>10</LangVersion>
<RootNamespace>Avalonia.Controls</RootNamespace>
</PropertyGroup>
<PropertyGroup>
Expand Down
13 changes: 13 additions & 0 deletions src/Avalonia.Controls.TreeDataGrid/CellIndex.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Avalonia.Controls
{
/// <summary>
/// Represents a cell in a <see cref="TreeDataGrid"/>.
/// </summary>
/// <param name="ColumnIndex">
/// The index of the cell in the <see cref="TreeDataGrid.Columns"/> collection.
/// </param>
/// <param name="RowIndex">
/// The hierarchical index of the row model in the data source.
/// </param>
public readonly record struct CellIndex(int ColumnIndex, IndexPath RowIndex);
}
21 changes: 15 additions & 6 deletions src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Models;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Selection;
using Avalonia.Input;
Expand All @@ -12,8 +13,10 @@ namespace Avalonia.Controls
/// A data source for a <see cref="TreeDataGrid"/> which displays a flat grid.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public class FlatTreeDataGridSource<TModel> : ITreeDataGridSource<TModel>, IDisposable
where TModel: class
public class FlatTreeDataGridSource<TModel> : NotifyingBase,
ITreeDataGridSource<TModel>,
IDisposable
where TModel: class
{
private IEnumerable<TModel> _items;
private TreeDataGridItemsSourceView<TModel> _itemsView;
Expand Down Expand Up @@ -45,6 +48,7 @@ public IEnumerable<TModel> Items
_rows?.SetItems(_itemsView);
if (_selection is object)
_selection.Source = value;
RaisePropertyChanged();
}
}
}
Expand All @@ -59,13 +63,18 @@ public ITreeDataGridSelection? Selection
}
set
{
if (_selection is object)
throw new InvalidOperationException("Selection is already initialized.");
_selection = value;
_isSelectionSet = true;
if (_selection != value)
{
if (value?.Source != _items)
throw new InvalidOperationException("Selection source must be set to Items.");
_selection = value;
_isSelectionSet = true;
RaisePropertyChanged();
}
}
}

public ITreeDataGridCellSelectionModel<TModel>? CellSelection => Selection as ITreeDataGridCellSelectionModel<TModel>;
public ITreeDataGridRowSelectionModel<TModel>? RowSelection => Selection as ITreeDataGridRowSelectionModel<TModel>;
public bool IsHierarchical => false;
public bool IsSorted => _comparer is not null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Models;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Selection;
using Avalonia.Input;
Expand All @@ -15,7 +16,8 @@ namespace Avalonia.Controls
/// row may have multiple columns.
/// </summary>
/// <typeparam name="TModel">The model type.</typeparam>
public class HierarchicalTreeDataGridSource<TModel> : ITreeDataGridSource<TModel>,
public class HierarchicalTreeDataGridSource<TModel> : NotifyingBase,
ITreeDataGridSource<TModel>,
IDisposable,
IExpanderRowController<TModel>
where TModel: class
Expand Down Expand Up @@ -70,13 +72,18 @@ public ITreeDataGridSelection? Selection
}
set
{
if (_selection is object)
throw new InvalidOperationException("Selection is already initialized.");
_selection = value;
_isSelectionSet = true;
if (_selection != value)
{
if (value?.Source != _items)
throw new InvalidOperationException("Selection source must be set to Items.");
_selection = value;
_isSelectionSet = true;
RaisePropertyChanged();
}
}
}

public ITreeDataGridCellSelectionModel<TModel>? CellSelection => Selection as ITreeDataGridCellSelectionModel<TModel>;
public ITreeDataGridRowSelectionModel<TModel>? RowSelection => Selection as ITreeDataGridRowSelectionModel<TModel>;
public bool IsHierarchical => true;
public bool IsSorted => _comparison is not null;
Expand Down
2 changes: 1 addition & 1 deletion src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ namespace Avalonia.Controls
/// <summary>
/// Represents a data source for a <see cref="TreeDataGrid"/> control.
/// </summary>
public interface ITreeDataGridSource
public interface ITreeDataGridSource : INotifyPropertyChanged
{
/// <summary>
/// Gets the columns to be displayed.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Selection;

namespace Avalonia.Controls.Primitives
{
internal interface ITreeDataGridCell
{
int ColumnIndex { get; }

void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex);
void Realize(
TreeDataGridElementFactory factory,
ITreeDataGridSelectionInteraction? selection,
ICell model,
int columnIndex,
int rowIndex);

void Unrealize();
}
}
Loading

0 comments on commit 78ebc02

Please sign in to comment.