diff --git a/samples/ControlCatalog/Pages/TableViewPage.axaml b/samples/ControlCatalog/Pages/TableViewPage.axaml
index bd730eb650e..231c059b093 100644
--- a/samples/ControlCatalog/Pages/TableViewPage.axaml
+++ b/samples/ControlCatalog/Pages/TableViewPage.axaml
@@ -38,6 +38,11 @@
IsChecked="True"
Margin="8" />
+
+
columns)
- => columns.Count > 0 && double.IsNaN(columns[0].ActualWidth);
+ {
+ foreach (var column in columns)
+ {
+ if (column.IsVisible)
+ return double.IsNaN(column.ActualWidth);
+ }
+
+ return false;
+ }
public static void ResetActualWidths(AvaloniaList columns)
{
foreach (var column in columns)
- column.ActualWidth = double.NaN;
+ {
+ if (column.IsVisible)
+ column.ActualWidth = double.NaN;
+ }
}
public static Size MeasureRow(AvaloniaList columns, Controls cells, Size availableSize)
@@ -115,6 +132,9 @@ public static Size MeasureRow(AvaloniaList columns, Controls ce
for (var i = 0; i < cells.Count; i++)
{
+ if (!columns[i].IsVisible)
+ continue;
+
var child = cells[i];
var columnWidth = columns[i].ActualWidth;
child.Measure(new Size(columnWidth, availableSize.Height));
@@ -133,6 +153,9 @@ public static Size ArrangeRow(AvaloniaList columns, Controls ce
var x = offset;
for (var i = 0; i < cells.Count; i++)
{
+ if (!columns[i].IsVisible)
+ continue;
+
var width = columns[i].ActualWidth;
cells[i].Arrange(new Rect(x, 0, width, finalSize.Height));
x += width;
diff --git a/src/Avalonia.Controls/TableView.cs b/src/Avalonia.Controls/TableView.cs
index 6fbc6344788..e79aa81bf33 100644
--- a/src/Avalonia.Controls/TableView.cs
+++ b/src/Avalonia.Controls/TableView.cs
@@ -139,8 +139,10 @@ private void AttachColumn(TableViewColumn column)
$"The column {column.DebugDisplay} is already attached to a {nameof(TableView)}.");
}
- column.TableView = this;
+ // Resolve styles and bindings before enabling refresh notifications. The headers
+ // and cells are rebuilt after attachment to apply the column's current values.
((ISetLogicalParent)column).SetParent(this);
+ column.TableView = this;
}
private void DetachColumn(TableViewColumn column)
diff --git a/src/Avalonia.Controls/TableViewCell.cs b/src/Avalonia.Controls/TableViewCell.cs
index 2eacde12b41..e8a792edc4f 100644
--- a/src/Avalonia.Controls/TableViewCell.cs
+++ b/src/Avalonia.Controls/TableViewCell.cs
@@ -35,6 +35,7 @@ internal set
private void ClearProperties()
{
+ ClearValue(IsVisibleProperty);
ClearValue(ThemeProperty);
ClearValue(HorizontalContentAlignmentProperty);
ClearValue(ContentTemplateProperty);
@@ -48,6 +49,7 @@ private void SetProperties(TableViewColumn column)
// Second, we have additional logic depending on whether a cell template is specified.
// Instead, values are updated manually via Refresh().
+ SetValue(IsVisibleProperty, column.IsVisible);
SetValue(ThemeProperty, column.CellTheme);
SetValue(HorizontalContentAlignmentProperty, column.HorizontalContentAlignment);
diff --git a/src/Avalonia.Controls/TableViewColumn.cs b/src/Avalonia.Controls/TableViewColumn.cs
index d10278faf9f..d2ef2fb2d50 100644
--- a/src/Avalonia.Controls/TableViewColumn.cs
+++ b/src/Avalonia.Controls/TableViewColumn.cs
@@ -14,6 +14,12 @@ namespace Avalonia.Controls;
///
public class TableViewColumn : StyledElement, IHeadered
{
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty IsVisibleProperty =
+ Visual.IsVisibleProperty.AddOwner();
+
///
/// Defines the property.
///
@@ -87,6 +93,15 @@ public class TableViewColumn : StyledElement, IHeadered
public static readonly DirectProperty CanUserEffectivelyResizeProperty =
AvaloniaProperty.RegisterDirect(nameof(CanUserEffectivelyResize), o => o.CanUserEffectivelyResize);
+ ///
+ /// Gets or sets whether the column is visible. The default is true.
+ ///
+ public bool IsVisible
+ {
+ get => GetValue(IsVisibleProperty);
+ set => SetValue(IsVisibleProperty, value);
+ }
+
///
/// Gets or sets the theme to apply to the header.
/// It must target .
@@ -217,22 +232,25 @@ private static bool IsCellProperty(AvaloniaProperty property)
=> property == CellThemeProperty ||
property == CellTemplateProperty ||
property == BindingProperty ||
- property == HorizontalContentAlignmentProperty;
+ property == HorizontalContentAlignmentProperty ||
+ property == IsVisibleProperty;
private static bool IsHeaderProperty(AvaloniaProperty property)
=> property == HeaderThemeProperty ||
property == HeaderTemplateProperty ||
property == HeaderProperty ||
- property == HorizontalContentAlignmentProperty;
+ property == HorizontalContentAlignmentProperty ||
+ property == IsVisibleProperty;
///
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
- if (change.Property == WidthProperty)
+ if (change.Property == WidthProperty || change.Property == IsVisibleProperty)
TableView?.OnColumnsSizeChanged();
- else if (change.Property == CanUserResizeProperty)
+
+ if (change.Property == CanUserResizeProperty)
UpdateCanUserEffectivelyResize();
else
{
diff --git a/src/Avalonia.Controls/TableViewColumnHeader.cs b/src/Avalonia.Controls/TableViewColumnHeader.cs
index 4afe51e475a..6cc3bf7ab20 100644
--- a/src/Avalonia.Controls/TableViewColumnHeader.cs
+++ b/src/Avalonia.Controls/TableViewColumnHeader.cs
@@ -78,6 +78,7 @@ internal override void BuildDebugDisplay(StringBuilder builder, bool includeCont
private void ClearProperties()
{
+ ClearValue(IsVisibleProperty);
ClearValue(ThemeProperty);
ClearValue(HorizontalContentAlignmentProperty);
ClearValue(ContentTemplateProperty);
@@ -86,6 +87,7 @@ private void ClearProperties()
private void SetProperties(TableViewColumn column)
{
+ SetValue(IsVisibleProperty, column.IsVisible);
SetOrClearValue(ThemeProperty, column.HeaderTheme);
SetValue(HorizontalContentAlignmentProperty, column.HorizontalContentAlignment);
SetOrClearValue(ContentTemplateProperty, column.HeaderTemplate);
diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/TableViewLayoutHelperTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/TableViewLayoutHelperTests.cs
index 66146ff80e3..ad25bb741b0 100644
--- a/tests/Avalonia.Controls.UnitTests/Presenters/TableViewLayoutHelperTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Presenters/TableViewLayoutHelperTests.cs
@@ -100,6 +100,34 @@ public void UpdateActualWidths_Treats_Auto_As_One_Star()
Assert.Equal(100, columns[1].ActualWidth);
}
+ [Theory]
+ [InlineData(GridUnitType.Pixel)]
+ [InlineData(GridUnitType.Star)]
+ [InlineData(GridUnitType.Auto)]
+ public void UpdateActualWidths_Excludes_Hidden_Columns_And_Preserves_Their_Widths(GridUnitType unit)
+ {
+ var hidden = new TableViewColumn { Width = new GridLength(2, unit) };
+ var columns = new AvaloniaList
+ {
+ hidden,
+ new() { Width = new GridLength(1, GridUnitType.Star) },
+ };
+ TableViewLayoutHelper.UpdateActualWidths(columns, 200, false, 1);
+ var actualWidth = hidden.ActualWidth;
+
+ hidden.IsVisible = false;
+ TableViewLayoutHelper.UpdateActualWidths(columns, 200, false, 1);
+
+ Assert.Equal(new GridLength(2, unit), hidden.Width);
+ Assert.Equal(actualWidth, hidden.ActualWidth);
+ Assert.Equal(200, columns[1].ActualWidth);
+
+ hidden.IsVisible = true;
+ TableViewLayoutHelper.UpdateActualWidths(columns, 200, false, 1);
+
+ Assert.Equal(actualWidth, hidden.ActualWidth);
+ }
+
[Fact]
public void UpdateActualWidths_Falls_Back_To_1000_For_Infinite_Width()
{
@@ -210,6 +238,29 @@ public void UpdateActualWidths_Spreads_Rounding_At_Fractional_Layout_Scale()
Assert.Equal(100, columns[0].ActualWidth + columns[1].ActualWidth + columns[2].ActualWidth);
}
+ [Fact]
+ public void UpdateActualWidths_Ignores_Hidden_Columns()
+ {
+ var columns = new AvaloniaList
+ {
+ new() { Width = new GridLength(3, GridUnitType.Star) },
+ new() { Width = new GridLength(100), IsVisible = false },
+ new() { Width = new GridLength(1, GridUnitType.Star) },
+ new() { Width = new GridLength(5, GridUnitType.Star), IsVisible = false },
+ new() { Width = new GridLength(3, GridUnitType.Star) },
+ };
+
+ TableViewLayoutHelper.UpdateActualWidths(columns, 100, useLayoutRounding: true, layoutScale: 2);
+
+ // Hidden columns are skipped, so their uncalculated widths remain NaN.
+ Assert.Equal(43, columns[0].ActualWidth);
+ Assert.Equal(double.NaN, columns[1].ActualWidth);
+ Assert.Equal(14, columns[2].ActualWidth);
+ Assert.Equal(double.NaN, columns[3].ActualWidth);
+ Assert.Equal(43, columns[4].ActualWidth);
+ Assert.Equal(100, columns[0].ActualWidth + columns[2].ActualWidth + columns[4].ActualWidth);
+ }
+
[Fact]
public void UpdateActualWidths_Rounds_Fixed_Column_Width_And_Star_Absorbs_Remainder()
{
@@ -259,6 +310,27 @@ public void NeedsActualWidths_Returns_False_After_Widths_Are_Computed()
Assert.False(TableViewLayoutHelper.NeedsActualWidths(columns));
}
+ [Fact]
+ public void NeedsActualWidths_Ignores_Hidden_Columns_After_Reset()
+ {
+ var columns = new AvaloniaList { new(), new() };
+ TableViewLayoutHelper.UpdateActualWidths(columns, 200, false, 1);
+ columns[0].IsVisible = false;
+
+ TableViewLayoutHelper.ResetActualWidths(columns);
+
+ Assert.Equal(100, columns[0].ActualWidth);
+ Assert.True(TableViewLayoutHelper.NeedsActualWidths(columns));
+
+ TableViewLayoutHelper.UpdateActualWidths(columns, 200, false, 1);
+
+ Assert.Equal(200, columns[1].ActualWidth);
+ Assert.False(TableViewLayoutHelper.NeedsActualWidths(columns));
+
+ columns[1].IsVisible = false;
+ Assert.False(TableViewLayoutHelper.NeedsActualWidths(columns));
+ }
+
[Fact]
public void ResetActualWidths_Sets_All_Widths_To_NaN()
{
diff --git a/tests/Avalonia.Controls.UnitTests/TableViewTests.cs b/tests/Avalonia.Controls.UnitTests/TableViewTests.cs
index e347d436dab..c49d3ce4e16 100644
--- a/tests/Avalonia.Controls.UnitTests/TableViewTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TableViewTests.cs
@@ -158,6 +158,31 @@ public void Adding_Column_Adds_Cell_To_Realized_Rows_And_Headers()
Assert.Equal(cellsPresenter.Children, logicalChildren);
}
+ [Theory]
+ [InlineData(0)]
+ [InlineData(1)]
+ public void Adding_Hidden_Column_Updates_Cells_And_Headers(int columnIndex)
+ {
+ using var app = Start();
+ var target = CreateTarget(new[] { "Foo" });
+ target.Styles.Add(new Style(x => x.OfType().Class("hidden"))
+ {
+ Setters = { new Setter(TableViewColumn.IsVisibleProperty, false) },
+ });
+ target.Columns.Add(new TableViewColumn());
+ Prepare(target);
+ var column = new TableViewColumn { Classes = { "hidden" } };
+
+ target.Columns.Insert(columnIndex, column);
+ Layout(target);
+
+ var row = (TableViewRow)target.GetRealizedContainers().Single();
+ Assert.False(GetCellsPresenter(row).Children[columnIndex].IsVisible);
+ Assert.False(GetColumnHeadersPresenter(target).Children[columnIndex].IsVisible);
+ Assert.True(GetCellsPresenter(row).Children[1 - columnIndex].IsVisible);
+ Assert.True(GetColumnHeadersPresenter(target).Children[1 - columnIndex].IsVisible);
+ }
+
[Fact]
public void Removing_Column_Removes_Cell_From_Realized_Rows_And_Headers()
{
@@ -254,6 +279,79 @@ public void Changing_Column_Width_Updates_ActualWidth_After_Layout()
Assert.Equal(50, target.Columns[1].ActualWidth);
}
+ [Fact]
+ public void Hiding_Column_Closes_Gap_Without_Recreating_Controls()
+ {
+ using var app = Start();
+ var column = new TableViewColumn { Width = new GridLength(80) };
+ var target = CreateTarget(new[] { "Foo" });
+ target.Columns.Add(column);
+ target.Columns.Add(new TableViewColumn());
+ Prepare(target, width: 200);
+ var row = (TableViewRow)target.GetRealizedContainers().Single();
+ var cells = GetCellsPresenter(row);
+ var headers = GetColumnHeadersPresenter(target);
+ var originalCells = cells.Children.ToArray();
+ var originalHeaders = headers.Children.ToArray();
+
+ column.IsVisible = false;
+ Layout(target);
+
+ Assert.False(cells.Children[0].IsVisible);
+ Assert.False(headers.Children[0].IsVisible);
+ Assert.Equal(0, cells.Children[1].Bounds.X);
+ Assert.Equal(0, headers.Children[1].Bounds.X);
+ Assert.Equal(200, target.Columns[1].ActualWidth);
+ Assert.Equal(80, column.ActualWidth);
+ Assert.Equal(new GridLength(80), column.Width);
+
+ column.IsVisible = true;
+ Layout(target);
+
+ Assert.True(cells.Children[0].IsVisible);
+ Assert.True(headers.Children[0].IsVisible);
+ Assert.Equal(80, cells.Children[1].Bounds.X);
+ Assert.Equal(80, headers.Children[1].Bounds.X);
+ Assert.Equal(originalCells, cells.Children);
+ Assert.Equal(originalHeaders, headers.Children);
+ }
+
+ [Fact]
+ public void Hidden_Columns_Do_Not_Contribute_To_Row_Size()
+ {
+ using var app = Start();
+ var target = CreateTarget(new[] { "Foo" });
+ var column = new TableViewColumn { IsVisible = false };
+ target.Columns.Add(column);
+ target.Columns.Add(new TableViewColumn());
+ Prepare(target);
+ var row = (TableViewRow)target.GetRealizedContainers().Single();
+ var cells = GetCellsPresenter(row);
+ cells.Children[0].Height = 80;
+ cells.Children[1].Height = 20;
+ Layout(target);
+
+ Assert.False(cells.Children[0].IsVisible);
+ Assert.False(GetColumnHeadersPresenter(target).Children[0].IsVisible);
+ Assert.Equal(20, cells.DesiredSize.Height);
+
+ column.IsVisible = true;
+ Layout(target);
+
+ Assert.Equal(80, cells.DesiredSize.Height);
+
+ column.IsVisible = false;
+ target.Columns[1].IsVisible = false;
+ Layout(target);
+
+ Assert.Equal(default, cells.DesiredSize);
+
+ column.IsVisible = true;
+ Layout(target);
+
+ Assert.Equal(80, cells.DesiredSize.Height);
+ }
+
[Fact]
public void Replacing_Columns_Collection_Updates_Realized_Rows_And_Headers()
{
@@ -334,6 +432,37 @@ public void Cell_Uses_Column_Binding()
Assert.Equal("Bob", secondCell.Content);
}
+ [Fact]
+ public void Hidden_Column_Preserves_Width_And_Visibility_Bindings()
+ {
+ using var app = Start();
+ var model = new Border { IsVisible = false, Tag = new GridLength(80) };
+ var target = CreateTarget(new[] { "Foo" });
+ target.DataContext = model;
+ var column = new TableViewColumn();
+ column.Bind(TableViewColumn.WidthProperty, new ReflectionBinding(nameof(Border.Tag)));
+ column.Bind(TableViewColumn.IsVisibleProperty, new ReflectionBinding(nameof(Border.IsVisible)));
+ target.Columns.Add(column);
+ target.Columns.Add(new TableViewColumn());
+ Prepare(target, width: 200);
+ Assert.False(column.IsVisible);
+ Assert.Equal(200, target.Columns[1].ActualWidth);
+
+ model.Tag = new GridLength(120);
+ model.IsVisible = true;
+ Layout(target);
+
+ Assert.True(column.IsVisible);
+ Assert.Equal(120, column.ActualWidth);
+ Assert.Equal(80, target.Columns[1].ActualWidth);
+
+ model.IsVisible = false;
+ Layout(target);
+
+ Assert.False(column.IsVisible);
+ Assert.Equal(200, target.Columns[1].ActualWidth);
+ }
+
[Fact]
public void Cell_Uses_Column_CellTemplate()
{
@@ -495,13 +624,15 @@ public void Changing_Column_Properties_Updates_Existing_Headers_And_Cells()
Assert.Same(item, cell.Content);
}
- [Fact]
- public void Re_Templating_Row_Detaches_Old_Cells_And_Rebuilds_New_Cells()
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Re_Templating_Row_Detaches_Old_Cells_And_Rebuilds_New_Cells(bool isVisible)
{
using var app = Start();
var target = CreateTarget(new[] { "Foo" });
- target.Columns.Add(new TableViewColumn());
+ target.Columns.Add(new TableViewColumn { IsVisible = isVisible });
target.Columns.Add(new TableViewColumn());
Prepare(target);
@@ -516,6 +647,8 @@ public void Re_Templating_Row_Detaches_Old_Cells_And_Rebuilds_New_Cells()
var newCells = GetCellsPresenter(row).Children.ToArray();
Assert.Equal(2, newCells.Length);
+ Assert.Equal(isVisible, newCells[0].IsVisible);
+ Assert.True(newCells[1].IsVisible);
Assert.All(oldCells, cell => Assert.Null(cell.Parent));
Assert.Equal(newCells, row.GetLogicalChildren());