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());