Skip to content

Commit fb96820

Browse files
authored
Merge pull request #277 from AvaloniaUI/feature/switch-layout
Make switching between flat and tree layouts easier.
2 parents d5d6496 + a7e4796 commit fb96820

File tree

6 files changed

+209
-61
lines changed

6 files changed

+209
-61
lines changed

samples/TreeDataGridDemo/MainWindow.axaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,11 @@
6565
DockPanel.Dock="Right">
6666
Cell Selection
6767
</CheckBox>
68+
<CheckBox IsChecked="{Binding Files.FlatList}"
69+
Margin="4 0 0 0"
70+
DockPanel.Dock="Right">
71+
Flat
72+
</CheckBox>
6873
<TextBox Text="{Binding Files.SelectedPath, Mode=OneWay}"
6974
Margin="4 0 0 0"
7075
VerticalContentAlignment="Center"

samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs

Lines changed: 137 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
using System.Linq;
66
using System.Reactive.Linq;
77
using System.Runtime.InteropServices;
8-
using Avalonia;
98
using Avalonia.Controls;
109
using Avalonia.Controls.Models.TreeDataGrid;
1110
using Avalonia.Controls.Selection;
@@ -20,6 +19,9 @@ namespace TreeDataGridDemo.ViewModels
2019
public class FilesPageViewModel : ReactiveObject
2120
{
2221
private static IconConverter? s_iconConverter;
22+
private readonly HierarchicalTreeDataGridSource<FileTreeNodeModel>? _treeSource;
23+
private FlatTreeDataGridSource<FileTreeNodeModel>? _flatSource;
24+
private ITreeDataGridSource<FileTreeNodeModel> _source;
2325
private bool _cellSelection;
2426
private FileTreeNodeModel? _root;
2527
private string _selectedDrive;
@@ -38,61 +40,17 @@ public FilesPageViewModel()
3840
_selectedDrive = Drives.FirstOrDefault() ?? "/";
3941
}
4042

41-
Source = new HierarchicalTreeDataGridSource<FileTreeNodeModel>(Array.Empty<FileTreeNodeModel>())
42-
{
43-
Columns =
44-
{
45-
new CheckBoxColumn<FileTreeNodeModel>(
46-
null,
47-
x => x.IsChecked,
48-
(o, v) => o.IsChecked = v,
49-
options: new()
50-
{
51-
CanUserResizeColumn = false,
52-
}),
53-
new HierarchicalExpanderColumn<FileTreeNodeModel>(
54-
new TemplateColumn<FileTreeNodeModel>(
55-
"Name",
56-
"FileNameCell",
57-
"FileNameEditCell",
58-
new GridLength(1, GridUnitType.Star),
59-
new()
60-
{
61-
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Name),
62-
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Name),
63-
IsTextSearchEnabled = true,
64-
TextSearchValueSelector = x => x.Name
65-
}),
66-
x => x.Children,
67-
x => x.HasChildren,
68-
x => x.IsExpanded),
69-
new TextColumn<FileTreeNodeModel, long?>(
70-
"Size",
71-
x => x.Size,
72-
options: new()
73-
{
74-
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Size),
75-
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Size),
76-
}),
77-
new TextColumn<FileTreeNodeModel, DateTimeOffset?>(
78-
"Modified",
79-
x => x.Modified,
80-
options: new()
81-
{
82-
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Modified),
83-
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Modified),
84-
}),
85-
}
86-
};
87-
88-
Source.RowSelection!.SingleSelect = false;
89-
Source.RowSelection.SelectionChanged += SelectionChanged;
43+
_source = _treeSource = CreateTreeSource();
9044

9145
this.WhenAnyValue(x => x.SelectedDrive)
9246
.Subscribe(x =>
9347
{
9448
_root = new FileTreeNodeModel(_selectedDrive, isDirectory: true, isRoot: true);
95-
Source.Items = new[] { _root };
49+
50+
if (_treeSource is not null)
51+
_treeSource.Items = new[] { _root };
52+
else if (_flatSource is not null)
53+
_flatSource.Items = _root.Children;
9654
});
9755
}
9856

@@ -115,6 +73,16 @@ public bool CellSelection
11573

11674
public IList<string> Drives { get; }
11775

76+
public bool FlatList
77+
{
78+
get => Source != _treeSource;
79+
set
80+
{
81+
if (value != FlatList)
82+
Source = value ? _flatSource ??= CreateFlatSource() : _treeSource!;
83+
}
84+
}
85+
11886
public string SelectedDrive
11987
{
12088
get => _selectedDrive;
@@ -127,7 +95,11 @@ public string? SelectedPath
12795
set => SetSelectedPath(value);
12896
}
12997

130-
public HierarchicalTreeDataGridSource<FileTreeNodeModel> Source { get; }
98+
public ITreeDataGridSource<FileTreeNodeModel> Source
99+
{
100+
get => _source;
101+
private set => this.RaiseAndSetIfChanged(ref _source, value);
102+
}
131103

132104
public static IMultiValueConverter FileIconConverter
133105
{
@@ -151,11 +123,115 @@ public static IMultiValueConverter FileIconConverter
151123
}
152124
}
153125

126+
private FlatTreeDataGridSource<FileTreeNodeModel> CreateFlatSource()
127+
{
128+
var result = new FlatTreeDataGridSource<FileTreeNodeModel>(_root!.Children)
129+
{
130+
Columns =
131+
{
132+
new CheckBoxColumn<FileTreeNodeModel>(
133+
null,
134+
x => x.IsChecked,
135+
(o, v) => o.IsChecked = v,
136+
options: new()
137+
{
138+
CanUserResizeColumn = false,
139+
}),
140+
new TemplateColumn<FileTreeNodeModel>(
141+
"Name",
142+
"FileNameCell",
143+
"FileNameEditCell",
144+
new GridLength(1, GridUnitType.Star),
145+
new()
146+
{
147+
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Name),
148+
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Name),
149+
IsTextSearchEnabled = true,
150+
TextSearchValueSelector = x => x.Name
151+
}),
152+
new TextColumn<FileTreeNodeModel, long?>(
153+
"Size",
154+
x => x.Size,
155+
options: new()
156+
{
157+
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Size),
158+
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Size),
159+
}),
160+
new TextColumn<FileTreeNodeModel, DateTimeOffset?>(
161+
"Modified",
162+
x => x.Modified,
163+
options: new()
164+
{
165+
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Modified),
166+
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Modified),
167+
}),
168+
}
169+
};
170+
171+
result.RowSelection!.SingleSelect = false;
172+
result.RowSelection.SelectionChanged += SelectionChanged;
173+
return result;
174+
}
175+
176+
private HierarchicalTreeDataGridSource<FileTreeNodeModel> CreateTreeSource()
177+
{
178+
var result = new HierarchicalTreeDataGridSource<FileTreeNodeModel>(Array.Empty<FileTreeNodeModel>())
179+
{
180+
Columns =
181+
{
182+
new CheckBoxColumn<FileTreeNodeModel>(
183+
null,
184+
x => x.IsChecked,
185+
(o, v) => o.IsChecked = v,
186+
options: new()
187+
{
188+
CanUserResizeColumn = false,
189+
}),
190+
new HierarchicalExpanderColumn<FileTreeNodeModel>(
191+
new TemplateColumn<FileTreeNodeModel>(
192+
"Name",
193+
"FileNameCell",
194+
"FileNameEditCell",
195+
new GridLength(1, GridUnitType.Star),
196+
new()
197+
{
198+
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Name),
199+
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Name),
200+
IsTextSearchEnabled = true,
201+
TextSearchValueSelector = x => x.Name
202+
}),
203+
x => x.Children,
204+
x => x.HasChildren,
205+
x => x.IsExpanded),
206+
new TextColumn<FileTreeNodeModel, long?>(
207+
"Size",
208+
x => x.Size,
209+
options: new()
210+
{
211+
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Size),
212+
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Size),
213+
}),
214+
new TextColumn<FileTreeNodeModel, DateTimeOffset?>(
215+
"Modified",
216+
x => x.Modified,
217+
options: new()
218+
{
219+
CompareAscending = FileTreeNodeModel.SortAscending(x => x.Modified),
220+
CompareDescending = FileTreeNodeModel.SortDescending(x => x.Modified),
221+
}),
222+
}
223+
};
224+
225+
result.RowSelection!.SingleSelect = false;
226+
result.RowSelection.SelectionChanged += SelectionChanged;
227+
return result;
228+
}
229+
154230
private void SetSelectedPath(string? value)
155231
{
156232
if (string.IsNullOrEmpty(value))
157233
{
158-
Source.RowSelection!.Clear();
234+
GetRowSelection(Source).Clear();
159235
return;
160236
}
161237

@@ -204,12 +280,18 @@ private void SetSelectedPath(string? value)
204280
}
205281
}
206282

207-
Source.RowSelection!.SelectedIndex = index;
283+
GetRowSelection(Source).SelectedIndex = index;
284+
}
285+
286+
private ITreeDataGridRowSelectionModel<FileTreeNodeModel> GetRowSelection(ITreeDataGridSource source)
287+
{
288+
return source.Selection as ITreeDataGridRowSelectionModel<FileTreeNodeModel> ??
289+
throw new InvalidOperationException("Expected a row selection model.");
208290
}
209291

210292
private void SelectionChanged(object? sender, TreeSelectionModelSelectionChangedEventArgs<FileTreeNodeModel> e)
211293
{
212-
var selectedPath = Source.RowSelection?.SelectedItem?.Path;
294+
var selectedPath = GetRowSelection(Source).SelectedItem?.Path;
213295
this.RaiseAndSetIfChanged(ref _selectedPath, selectedPath, nameof(SelectedPath));
214296

215297
foreach (var i in e.DeselectedItems)

src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,9 @@ public interface ITreeDataGridSource : INotifyPropertyChanged
2323
IRows Rows { get; }
2424

2525
/// <summary>
26-
/// Gets the selection model.
26+
/// Gets or sets the selection model.
2727
/// </summary>
28-
ITreeDataGridSelection? Selection { get; }
28+
ITreeDataGridSelection? Selection { get; set; }
2929

3030
/// <summary>
3131
/// Gets a value indicating whether the data source is hierarchical.
@@ -84,8 +84,8 @@ void DragDropRows(
8484
public interface ITreeDataGridSource<TModel> : ITreeDataGridSource
8585
{
8686
/// <summary>
87-
/// Gets the items in the data source.
87+
/// Gets or sets the items in the data source.
8888
/// </summary>
89-
new IEnumerable<TModel> Items { get; }
89+
new IEnumerable<TModel> Items { get; set; }
9090
}
9191
}

src/Avalonia.Controls.TreeDataGrid/Models/TreeDataGrid/ColumnList.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace Avalonia.Controls.Models.TreeDataGrid
99
/// </summary>
1010
public class ColumnList<TModel> : NotifyingListBase<IColumn<TModel>>, IColumns
1111
{
12+
private bool _initialized;
1213
private double _viewportWidth;
1314

1415
public event EventHandler? LayoutInvalidated;
@@ -22,6 +23,7 @@ public void AddRange(IEnumerable<IColumn<TModel>> items)
2223
public Size CellMeasured(int columnIndex, int rowIndex, Size size)
2324
{
2425
var column = (IUpdateColumnLayout)this[columnIndex];
26+
_initialized = true;
2527
return new Size(column.CellMeasured(size.Width, rowIndex), size.Height);
2628
}
2729

@@ -103,7 +105,8 @@ public void ViewportChanged(Rect viewport)
103105
if (_viewportWidth != viewport.Width)
104106
{
105107
_viewportWidth = viewport.Width;
106-
UpdateColumnSizes();
108+
if (_initialized)
109+
UpdateColumnSizes();
107110
}
108111
}
109112

src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -427,7 +427,7 @@ protected virtual void OnEffectiveViewportChanged(object? sender, EffectiveViewp
427427
// viewport.
428428
Viewport = e.EffectiveViewport.Size == default ?
429429
s_invalidViewport :
430-
e.EffectiveViewport.Intersect(new(Bounds.Size));
430+
Intersect(e.EffectiveViewport, new(Bounds.Size));
431431

432432
_isWaitingForViewportUpdate = false;
433433

@@ -730,6 +730,24 @@ private void OnUnrealizedFocusedElementLostFocus(object? sender, RoutedEventArgs
730730

731731
private static bool HasInfinity(Size s) => double.IsInfinity(s.Width) || double.IsInfinity(s.Height);
732732

733+
private static Rect Intersect(Rect a, Rect b)
734+
{
735+
// Hack fix for https://github.com/AvaloniaUI/Avalonia/issues/15075
736+
var newLeft = (a.X > b.X) ? a.X : b.X;
737+
var newTop = (a.Y > b.Y) ? a.Y : b.Y;
738+
var newRight = (a.Right < b.Right) ? a.Right : b.Right;
739+
var newBottom = (a.Bottom < b.Bottom) ? a.Bottom : b.Bottom;
740+
741+
if ((newRight >= newLeft) && (newBottom >= newTop))
742+
{
743+
return new Rect(newLeft, newTop, newRight - newLeft, newBottom - newTop);
744+
}
745+
else
746+
{
747+
return default;
748+
}
749+
}
750+
733751
private struct MeasureViewport
734752
{
735753
public int anchorIndex;

tests/Avalonia.Controls.TreeDataGrid.Tests/TreeDataGridTests_Flat.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,6 +312,46 @@ public void Does_Not_Realize_Columns_Outside_Viewport()
312312
Assert.True(double.IsNaN(columns[3].ActualWidth));
313313
}
314314

315+
[AvaloniaFact(Timeout = 10000)]
316+
public void Columns_Are_Correctly_Sized_After_Changing_Source()
317+
{
318+
// Create the initial target with 2 columns and make sure our preconditions are correct.
319+
var (target, items) = CreateTarget(columns: new IColumn<Model>[]
320+
{
321+
new TextColumn<Model, int>("ID", x => x.Id, width: new GridLength(1, GridUnitType.Star)),
322+
new TextColumn<Model, string?>("Title1", x => x.Title, options: MinWidth(50)),
323+
});
324+
325+
AssertColumnIndexes(target, 0, 2);
326+
327+
// Create a new source and assign it to the TreeDataGrid.
328+
var newSource = new FlatTreeDataGridSource<Model>(items)
329+
{
330+
Columns =
331+
{
332+
new TextColumn<Model, int>("ID", x => x.Id, width: new GridLength(1, GridUnitType.Star)),
333+
new TextColumn<Model, string?>("Title1", x => x.Title, options: MinWidth(20)),
334+
new TextColumn<Model, string?>("Title2", x => x.Title, options: MinWidth(20)),
335+
}
336+
};
337+
338+
target.Source = newSource;
339+
340+
// The columns should not have an ActualWidth yet.
341+
Assert.True(double.IsNaN(newSource.Columns[0].ActualWidth));
342+
Assert.True(double.IsNaN(newSource.Columns[1].ActualWidth));
343+
Assert.True(double.IsNaN(newSource.Columns[2].ActualWidth));
344+
345+
// Do a layout pass and check that the columns have been correctly sized.
346+
target.UpdateLayout();
347+
AssertColumnIndexes(target, 0, 3);
348+
349+
var columns = (ColumnList<Model>)target.Columns!;
350+
Assert.Equal(60, columns[0].ActualWidth);
351+
Assert.Equal(20, columns[1].ActualWidth);
352+
Assert.Equal(20, columns[2].ActualWidth);
353+
}
354+
315355
public class RemoveItems
316356
{
317357
[AvaloniaFact(Timeout = 10000)]

0 commit comments

Comments
 (0)