From 9192159eb90b87ed387d97378848a68ec2525b90 Mon Sep 17 00:00:00 2001 From: k-hara Date: Tue, 17 Feb 2026 23:22:51 +0900 Subject: [PATCH 1/7] Remove support for case where the DataRow is descendant of a Grid or DataTable The removed logic would have been useless or more harmful, because of the reasons: 1) DataTable control is designed to represent 'header' of a table, not for containing the list of DataRows. 2) Grid control is often used to lay out other controls rather than constructing a table, so there was possibilities that completely unrelated Grids would have been caught as a parent. --- components/DataTable/src/DataTable/DataRow.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/components/DataTable/src/DataTable/DataRow.cs b/components/DataTable/src/DataTable/DataRow.cs index 6546150ae..a5be9c660 100644 --- a/components/DataTable/src/DataTable/DataRow.cs +++ b/components/DataTable/src/DataTable/DataRow.cs @@ -57,9 +57,6 @@ private void DataRow_Unloaded(object sender, RoutedEventArgs e) _isTreeView = itemsPresenter.FindAscendant() is TreeView; } - // 1b. If we can't find the ItemsPresenter, then we reach up outside to find the next thing we could use as a parent - panel ??= this.FindAscendant(static (element) => element is Grid or DataTable); - // Cache actual datatable reference if (panel is DataTable table) { From 10659b4a22ed230062ae4be08527dfb523561fde Mon Sep 17 00:00:00 2001 From: k-hara Date: Tue, 17 Feb 2026 23:37:23 +0900 Subject: [PATCH 2/7] Prevent to focus DataColumn itself by default From the design concept, DataColumn control simply provides a space for the header content, so it should not interact with keyboard operations by default. If focusable something is actually needed, DataTable users can place any controls like a button there. --- components/DataTable/src/DataTable/DataColumn.xaml | 1 + 1 file changed, 1 insertion(+) diff --git a/components/DataTable/src/DataTable/DataColumn.xaml b/components/DataTable/src/DataTable/DataColumn.xaml index f88fedfe8..a23f020ee 100644 --- a/components/DataTable/src/DataTable/DataColumn.xaml +++ b/components/DataTable/src/DataTable/DataColumn.xaml @@ -18,6 +18,7 @@ + From 20c4c5d8554fa1c2923d585dc899433c13bd4bd1 Mon Sep 17 00:00:00 2001 From: k-hara Date: Wed, 18 Feb 2026 00:06:26 +0900 Subject: [PATCH 3/7] DataRow lay out children like a horizontal StackPanel if DataTable is not present This added behavior just covers a corner case. If a user fails to place corresponding DataTable at correct position, displaying somethings is better than nothing. Each rows layout their columns independent, because there's no main controller to remember the column widths, and then it looks like a horizontal StackPanel. --- components/DataTable/src/DataTable/DataRow.cs | 56 +++++++++++++++---- 1 file changed, 46 insertions(+), 10 deletions(-) diff --git a/components/DataTable/src/DataTable/DataRow.cs b/components/DataTable/src/DataTable/DataRow.cs index a5be9c660..5c5d83259 100644 --- a/components/DataTable/src/DataTable/DataRow.cs +++ b/components/DataTable/src/DataTable/DataRow.cs @@ -74,17 +74,31 @@ protected override Size MeasureOverride(Size availableSize) double maxHeight = 0; - if (Children.Count > 0) + // If we don't have a grid, just layout children like a horizontal StackPanel. + if (_parentPanel is null) { - // If we don't have a grid, just measure first child to get row height and take available space - if (_parentPanel is null) + double totalWidth = 0; + + for (int i = 0; i < Children.Count; i++) { - Children[0].Measure(availableSize); - return new Size(availableSize.Width, Children[0].DesiredSize.Height); + var child = Children[i]; + if (child?.Visibility != Visibility.Visible) + continue; + + child.Measure(availableSize); + + totalWidth += child.DesiredSize.Width; + maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); } + + return new Size(totalWidth, maxHeight); + } + + if (Children.Count > 0) + { // Handle DataTable Parent - else if (_parentTable != null - && _parentTable.Children.Count == Children.Count) + if (_parentTable != null && + _parentTable.Children.Count == Children.Count) { // TODO: Need to check visibility // Measure all children since we need to determine the row's height at minimum @@ -167,19 +181,41 @@ protected override Size MeasureOverride(Size availableSize) protected override Size ArrangeOverride(Size finalSize) { + // If we don't have a grid, just layout children like a horizontal StackPanel. + if (_parentPanel is null) + { + double x = 0; + + for (int i = 0; i < Children.Count; i++) + { + var child = Children[i]; + if (child?.Visibility != Visibility.Visible) + continue; + + double width = child.DesiredSize.Width; + + child.Arrange(new Rect(x, 0, width, finalSize.Height)); + + x += width; + } + + return new Size(x, finalSize.Height); + } + int column = 0; - double x = 0; // Try and grab Column Spacing from DataTable, if not a parent Grid, if not 0. double spacing = _parentTable?.ColumnSpacing ?? (_parentPanel as Grid)?.ColumnSpacing ?? 0; - double width = 0; - if (_parentPanel != null) { + double x = 0; + int i = 0; foreach (UIElement child in Children.Where(static e => e.Visibility == Visibility.Visible)) { + double width; + if (_parentPanel is Grid grid && column < grid.ColumnDefinitions.Count) { From fcc11c52c87ae59a14681df306af10e8471cf97a Mon Sep 17 00:00:00 2001 From: k-hara Date: Thu, 19 Feb 2026 17:01:55 +0900 Subject: [PATCH 4/7] Add internal methods in DataColumn --- components/DataTable/src/DataTable/DataColumn.cs | 14 +++++++++----- components/DataTable/src/DataTable/DataTable.cs | 16 ++++++++-------- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/components/DataTable/src/DataTable/DataColumn.cs b/components/DataTable/src/DataTable/DataColumn.cs index 6f969b15e..3e83f0ecf 100644 --- a/components/DataTable/src/DataTable/DataColumn.cs +++ b/components/DataTable/src/DataTable/DataColumn.cs @@ -13,6 +13,8 @@ public partial class DataColumn : ContentControl private WeakReference? _parent; + internal DataTable? DataTable => _parent?.TryGetTarget(out DataTable? parent) == true ? parent : null; + /// /// Gets or sets the width of the largest child contained within the visible s of the . /// @@ -23,6 +25,12 @@ public partial class DataColumn : ContentControl /// internal GridLength CurrentWidth { get; private set; } + internal bool IsAbsolute => CurrentWidth.IsAbsolute; + + internal bool IsAuto => CurrentWidth.IsAuto; + + internal bool IsStar => CurrentWidth.IsStar; + /// /// Gets or sets whether the column can be resized by the user. /// @@ -111,10 +119,6 @@ private void ColumnResizedByUserSizer() CurrentWidth = new(this.ActualWidth); // Notify the rest of the table to update - if (_parent?.TryGetTarget(out DataTable? parent) == true - && parent != null) - { - parent.ColumnResized(); - } + DataTable?.ColumnResized(); } } diff --git a/components/DataTable/src/DataTable/DataTable.cs b/components/DataTable/src/DataTable/DataTable.cs index 7b78b2beb..3956e3ca8 100644 --- a/components/DataTable/src/DataTable/DataTable.cs +++ b/components/DataTable/src/DataTable/DataTable.cs @@ -57,11 +57,11 @@ protected override Size MeasureOverride(Size availableSize) // We only need to measure elements that are visible foreach (DataColumn column in elements) { - if (column.CurrentWidth.IsStar) + if (column.IsStar) { proportionalUnits += column.DesiredWidth.Value; } - else if (column.CurrentWidth.IsAbsolute) + else if (column.IsAbsolute) { fixedWidth += column.DesiredWidth.Value; } @@ -75,11 +75,11 @@ protected override Size MeasureOverride(Size availableSize) foreach (DataColumn column in elements) { - if (column.CurrentWidth.IsStar) + if (column.IsStar) { column.Measure(new Size(proportionalAmount * column.CurrentWidth.Value, availableSize.Height)); } - else if (column.CurrentWidth.IsAbsolute) + else if (column.IsAbsolute) { column.Measure(new Size(column.CurrentWidth.Value, availableSize.Height)); } @@ -118,11 +118,11 @@ protected override Size ArrangeOverride(Size finalSize) // We only need to measure elements that are visible foreach (DataColumn column in elements) { - if (column.CurrentWidth.IsStar) + if (column.IsStar) { proportionalUnits += column.CurrentWidth.Value; } - else if (column.CurrentWidth.IsAbsolute) + else if (column.IsAbsolute) { fixedWidth += column.CurrentWidth.Value; } @@ -141,12 +141,12 @@ protected override Size ArrangeOverride(Size finalSize) foreach (DataColumn column in elements) { - if (column.CurrentWidth.IsStar) + if (column.IsStar) { width = proportionalAmount * column.CurrentWidth.Value; column.Arrange(new Rect(x, 0, width, finalSize.Height)); } - else if (column.CurrentWidth.IsAbsolute) + else if (column.IsAbsolute) { width = column.CurrentWidth.Value; column.Arrange(new Rect(x, 0, width, finalSize.Height)); From 439dc9478382af721f3f2e25c305b4ef47fbc8cd Mon Sep 17 00:00:00 2001 From: k-hara Date: Tue, 17 Feb 2026 22:24:59 +0900 Subject: [PATCH 5/7] Add XML comments to reduce CS1591 warnings --- components/DataTable/src/DataTable/DataColumn.cs | 7 +++++++ components/DataTable/src/DataTable/DataRow.cs | 10 +++++++++- components/DataTable/src/DataTable/DataTable.cs | 2 ++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/components/DataTable/src/DataTable/DataColumn.cs b/components/DataTable/src/DataTable/DataColumn.cs index 3e83f0ecf..6fbb7e028 100644 --- a/components/DataTable/src/DataTable/DataColumn.cs +++ b/components/DataTable/src/DataTable/DataColumn.cs @@ -4,6 +4,9 @@ namespace CommunityToolkit.WinUI.Controls; +/// +/// Represents a column. +/// [TemplatePart(Name = nameof(PART_ColumnSizer), Type = typeof(ContentSizer))] public partial class DataColumn : ContentControl { @@ -70,11 +73,15 @@ private static void DesiredWidth_PropertyChanged(DependencyObject d, DependencyP } } + /// + /// Initializes a new instance of the class. + /// public DataColumn() { this.DefaultStyleKey = typeof(DataColumn); } + /// protected override void OnApplyTemplate() { if (PART_ColumnSizer != null) diff --git a/components/DataTable/src/DataTable/DataRow.cs b/components/DataTable/src/DataTable/DataRow.cs index 5c5d83259..4177265e0 100644 --- a/components/DataTable/src/DataTable/DataRow.cs +++ b/components/DataTable/src/DataTable/DataRow.cs @@ -6,6 +6,9 @@ namespace CommunityToolkit.WinUI.Controls; +/// +/// Represents a row. +/// public partial class DataRow : Panel { // TODO: Create our own helper class here for the Header as well vs. straight-Grid. @@ -16,6 +19,9 @@ public partial class DataRow : Panel private bool _isTreeView; private double _treePadding; + /// + /// Initializes a new instance of the class. + /// public DataRow() { Unloaded += this.DataRow_Unloaded; @@ -67,6 +73,7 @@ private void DataRow_Unloaded(object sender, RoutedEventArgs e) return panel; } + /// protected override Size MeasureOverride(Size availableSize) { // We should probably only have to do this once ever? @@ -179,6 +186,7 @@ protected override Size MeasureOverride(Size availableSize) return new(_parentPanel?.DesiredSize.Width ?? availableSize.Width, maxHeight); } + /// protected override Size ArrangeOverride(Size finalSize) { // If we don't have a grid, just layout children like a horizontal StackPanel. @@ -219,7 +227,7 @@ protected override Size ArrangeOverride(Size finalSize) if (_parentPanel is Grid grid && column < grid.ColumnDefinitions.Count) { - width = grid.ColumnDefinitions[column++].ActualWidth; + width = grid.ColumnDefinitions[column++].ActualWidth; } // TODO: Need to check Column visibility here as well... else if (_parentPanel is DataTable table && diff --git a/components/DataTable/src/DataTable/DataTable.cs b/components/DataTable/src/DataTable/DataTable.cs index 3956e3ca8..5842c30b9 100644 --- a/components/DataTable/src/DataTable/DataTable.cs +++ b/components/DataTable/src/DataTable/DataTable.cs @@ -44,6 +44,7 @@ public double ColumnSpacing public static readonly DependencyProperty ColumnSpacingProperty = DependencyProperty.Register(nameof(ColumnSpacing), typeof(double), typeof(DataTable), new PropertyMetadata(0d)); + /// protected override Size MeasureOverride(Size availableSize) { double fixedWidth = 0; @@ -107,6 +108,7 @@ protected override Size MeasureOverride(Size availableSize) return new Size(availableSize.Width, maxHeight); } + /// protected override Size ArrangeOverride(Size finalSize) { double fixedWidth = 0; From 06622bc7e3217968c571a8bde41e9783702fafb1 Mon Sep 17 00:00:00 2001 From: k-hara Date: Thu, 19 Feb 2026 16:56:24 +0900 Subject: [PATCH 6/7] Remove support for the 'Hybrid-case' that paring Grid and DataRows That case doesn't work well for the Width="Auto" column. I'd like to make DataTable control simple rather than leave the pitfall. --- components/DataTable/samples/DataTable.md | 8 -- .../samples/DataTableHybridSample.xaml | 50 ----------- .../samples/DataTableHybridSample.xaml.cs | 48 ----------- components/DataTable/src/DataTable/DataRow.cs | 82 +++++-------------- 4 files changed, 20 insertions(+), 168 deletions(-) delete mode 100644 components/DataTable/samples/DataTableHybridSample.xaml delete mode 100644 components/DataTable/samples/DataTableHybridSample.xaml.cs diff --git a/components/DataTable/samples/DataTable.md b/components/DataTable/samples/DataTable.md index c9cdf1a98..38ddc422c 100644 --- a/components/DataTable/samples/DataTable.md +++ b/components/DataTable/samples/DataTable.md @@ -36,14 +36,6 @@ can be made to look like a table of data: There are limitations here with having fixed column sizes that can be difficult to align. Their definitions are also duplicated, and every item is recreating this layout and duplicating it within the Visual Tree. -## DataRow Hybrid Setup - -As a first step, moving to **DataTable** is easy, just replace the `Grid` in your `ItemsTemplate` with the `DataRow` panel -and remove the Column attributes from your controls. `DataRow` automatically will lay each subsequent control in the next column -for you automatically: - -> [!Sample DataTableHybridSample] - ## DataTable Setup The `DataTable` setup provides an easier way to define and manage your columns within your header for this coordinated effort diff --git a/components/DataTable/samples/DataTableHybridSample.xaml b/components/DataTable/samples/DataTableHybridSample.xaml deleted file mode 100644 index b6eaf6023..000000000 --- a/components/DataTable/samples/DataTableHybridSample.xaml +++ /dev/null @@ -1,50 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/components/DataTable/samples/DataTableHybridSample.xaml.cs b/components/DataTable/samples/DataTableHybridSample.xaml.cs deleted file mode 100644 index b47a44fed..000000000 --- a/components/DataTable/samples/DataTableHybridSample.xaml.cs +++ /dev/null @@ -1,48 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using CommunityToolkit.WinUI.Controls; - -namespace DataTableExperiment.Samples; - -[ToolkitSample(id: nameof(DataTableHybridSample), "Hybrid DataTable Example", description: $"A sample for showing how to create and use a {nameof(DataRow)} control alongside an existing traditional setup with Grid.")] -public sealed partial class DataTableHybridSample : Page -{ - public ObservableCollection InventoryItems { get; set; } = new() - { - new() - { - Id = 1002, - Name = "Hydra", - Description = "Multiple Launch Rocket System-2 Hydra", - Quantity = 1, - }, - new() - { - Id = 3456, - Name = "MA40 AR", - Description = "Regular assault rifle - updated version of MA5B or MA37 AR", - Quantity = 4, - }, - new() - { - Id = 5698, - Name = "Needler", - Description = "Alien weapon well-known for its iconic design with pink crystals", - Quantity = 2, - }, - new() - { - Id = 7043, - Name = "Ravager", - Description = "An incendiary plasma launcher", - Quantity = 1, - }, - }; - - public DataTableHybridSample() - { - this.InitializeComponent(); - } -} diff --git a/components/DataTable/src/DataTable/DataRow.cs b/components/DataTable/src/DataTable/DataRow.cs index 4177265e0..f3611060e 100644 --- a/components/DataTable/src/DataTable/DataRow.cs +++ b/components/DataTable/src/DataTable/DataRow.cs @@ -13,7 +13,6 @@ public partial class DataRow : Panel { // TODO: Create our own helper class here for the Header as well vs. straight-Grid. // TODO: WeakReference? - private Panel? _parentPanel; private DataTable? _parentTable; private bool _isTreeView; @@ -32,10 +31,9 @@ private void DataRow_Unloaded(object sender, RoutedEventArgs e) // Remove our references on unloaded _parentTable?.Rows.Remove(this); _parentTable = null; - _parentPanel = null; } - private Panel? InitializeParentHeaderConnection() + private DataTable? InitializeParentHeaderConnection() { // TODO: Think about this expression instead... // Drawback: Can't have Grid between table and header @@ -48,15 +46,9 @@ private void DataRow_Unloaded(object sender, RoutedEventArgs e) // 1a. Get parent ItemsPresenter to find header if (this.FindAscendant() is ItemsPresenter itemsPresenter) { - // 2. Quickly check if the header is just what we're looking for. - if (itemsPresenter.Header is Grid or DataTable) + if (itemsPresenter.Header is DependencyObject header) { - panel = itemsPresenter.Header as Panel; - } - else - { - // 3. Otherwise, try and find the inner thing we want. - panel = itemsPresenter.FindDescendant(static (element) => element is Grid or DataTable); + panel = header.FindDescendantOrSelf(); } // Check if we're in a TreeView @@ -70,19 +62,19 @@ private void DataRow_Unloaded(object sender, RoutedEventArgs e) _parentTable.Rows.Add(this); // Add us to the row list. } - return panel; + return _parentTable; } /// protected override Size MeasureOverride(Size availableSize) { // We should probably only have to do this once ever? - _parentPanel ??= InitializeParentHeaderConnection(); + _parentTable ??= InitializeParentHeaderConnection(); double maxHeight = 0; - // If we don't have a grid, just layout children like a horizontal StackPanel. - if (_parentPanel is null) + // If we don't have a DataTable, just layout children like a horizontal StackPanel. + if (_parentTable is null) { double totalWidth = 0; @@ -100,12 +92,10 @@ protected override Size MeasureOverride(Size availableSize) return new Size(totalWidth, maxHeight); } - - if (Children.Count > 0) + // Handle DataTable Parent + else { - // Handle DataTable Parent - if (_parentTable != null && - _parentTable.Children.Count == Children.Count) + if (_parentTable.Children.Count == Children.Count) { // TODO: Need to check visibility // Measure all children since we need to determine the row's height at minimum @@ -158,39 +148,19 @@ protected override Size MeasureOverride(Size availableSize) maxHeight = Math.Max(maxHeight, Children[i].DesiredSize.Height); } } - // Fallback for Grid Hybrid scenario... - else if (_parentPanel is Grid grid - && _parentPanel.Children.Count == Children.Count - && grid.ColumnDefinitions.Count == Children.Count) - { - // TODO: Need to check visibility - // Measure all children since we need to determine the row's height at minimum - for (int i = 0; i < Children.Count; i++) - { - if (grid.ColumnDefinitions[i].Width.GridUnitType == GridUnitType.Pixel) - { - Children[i].Measure(new(grid.ColumnDefinitions[i].Width.Value, availableSize.Height)); - } - else - { - Children[i].Measure(availableSize); - } - maxHeight = Math.Max(maxHeight, Children[i].DesiredSize.Height); - } - } // TODO: What do we want to do if there's unequal children in the DataTable vs. DataRow? } // Otherwise, return our parent's size as the desired size. - return new(_parentPanel?.DesiredSize.Width ?? availableSize.Width, maxHeight); + return new(_parentTable?.DesiredSize.Width ?? availableSize.Width, maxHeight); } /// protected override Size ArrangeOverride(Size finalSize) { - // If we don't have a grid, just layout children like a horizontal StackPanel. - if (_parentPanel is null) + // If we don't have DataTable, just layout children like a horizontal StackPanel. + if (_parentTable is null) { double x = 0; @@ -209,32 +179,22 @@ protected override Size ArrangeOverride(Size finalSize) return new Size(x, finalSize.Height); } - - int column = 0; - - // Try and grab Column Spacing from DataTable, if not a parent Grid, if not 0. - double spacing = _parentTable?.ColumnSpacing ?? (_parentPanel as Grid)?.ColumnSpacing ?? 0; - - if (_parentPanel != null) + // Handle DataTable Parent + else { + int column = 0; double x = 0; + double spacing = _parentTable.ColumnSpacing; + double width = 0; int i = 0; foreach (UIElement child in Children.Where(static e => e.Visibility == Visibility.Visible)) { - double width; - - if (_parentPanel is Grid grid && - column < grid.ColumnDefinitions.Count) - { - width = grid.ColumnDefinitions[column++].ActualWidth; - } // TODO: Need to check Column visibility here as well... - else if (_parentPanel is DataTable table && - column < table.Children.Count) + if (column < _parentTable.Children.Count) { // TODO: This is messy... - width = (table.Children[column++] as DataColumn)?.ActualWidth ?? 0; + width = (_parentTable.Children[column++] as DataColumn)?.ActualWidth ?? 0; } // Note: For Auto, since we measured our children and bubbled that up to the DataTable layout, then the DataColumn size we grab above should account for the largest of our children. @@ -254,7 +214,5 @@ protected override Size ArrangeOverride(Size finalSize) return new Size(x - spacing, finalSize.Height); } - - return finalSize; } } From 9c9f4614a98ab1690113d978db5b28573fb9d6e0 Mon Sep 17 00:00:00 2001 From: k-hara Date: Wed, 25 Feb 2026 23:15:00 +0900 Subject: [PATCH 7/7] Update the logic for the column widths calculation For `IsAuto` and `IsStar` columns, additional attribute `IsFixed` is considered. - `IsAuto && IsFixed`: have a manually resized width. - `IsAuto && !IsFixed`: have a calculated width that fits to each column content of visualized row. - `IsStar && IsFixed`: have a manually resized width. - `IsStar && !IsFixed`: have a calculated width, from the proportion of remained spaces. If `IsAbsolute == true`, `IsFixed` is also `true` always. If `IsStar` has zero ratio `0*`, `IsFixed` is specially set to `true` and the column has zero width. `DataTable` assigns the width space first to the fixed columns, then unfixed ones. For the IsFixed == true columns, there's nothing to difficult. For the column that is IsAuto && !IsFixed, the column size is determined with the following steps: - [DataTable.MeasureOverride] - The column's `CurerntWidth` is set to best-fit width of its header content. - For each visualized `DataRow`s, `InvalidateMeasure()` is invoked. - [DataRow.MeasureOverride] - Calculates the best-fit width of the column content and increase the column's `CurrentWidth` to get the maximun width. - Then, call `DataTable.InvalidateMeasure()`. - [DataTable.MeasureOverride] - In the last, gets the best-fit width for the column. - The mutual `InvalidateMeasure()` call between `DataTable` and `DataRow`s are stopped by the layout system when all element sizes are stable. For the column that is IsStar && !IsFixed, the column size is determined with the following steps: - [DataTable.MeasureOverride] - The column's `CurerntWidth` is set to best-fit width of its header content. - For each visualized `DataRow`s, InvalidateArrange() is invoked. - [DataRow.ArrangeOverride] - Remaining space of finalSize.Width is supplied to the star proportion column. --- ...ityToolkit.WinUI.Controls.DataTable.csproj | 2 +- .../DataTable/src/DataTable/DataColumn.cs | 56 +++-- components/DataTable/src/DataTable/DataRow.cs | 143 +++++------ .../DataTable/src/DataTable/DataTable.cs | 235 ++++++++++++------ 4 files changed, 273 insertions(+), 163 deletions(-) diff --git a/components/DataTable/src/CommunityToolkit.WinUI.Controls.DataTable.csproj b/components/DataTable/src/CommunityToolkit.WinUI.Controls.DataTable.csproj index a191e716f..4c6645576 100644 --- a/components/DataTable/src/CommunityToolkit.WinUI.Controls.DataTable.csproj +++ b/components/DataTable/src/CommunityToolkit.WinUI.Controls.DataTable.csproj @@ -12,7 +12,7 @@ - + $(PackageIdPrefix).$(PackageIdVariant).Controls.$(ToolkitComponentName) diff --git a/components/DataTable/src/DataTable/DataColumn.cs b/components/DataTable/src/DataTable/DataColumn.cs index 6fbb7e028..9a8cf01af 100644 --- a/components/DataTable/src/DataTable/DataColumn.cs +++ b/components/DataTable/src/DataTable/DataColumn.cs @@ -10,8 +10,6 @@ namespace CommunityToolkit.WinUI.Controls; [TemplatePart(Name = nameof(PART_ColumnSizer), Type = typeof(ContentSizer))] public partial class DataColumn : ContentControl { - private static GridLength StarLength = new GridLength(1, GridUnitType.Star); - private ContentSizer? PART_ColumnSizer; private WeakReference? _parent; @@ -19,20 +17,26 @@ public partial class DataColumn : ContentControl internal DataTable? DataTable => _parent?.TryGetTarget(out DataTable? parent) == true ? parent : null; /// - /// Gets or sets the width of the largest child contained within the visible s of the . + /// Gets or sets the internal calculated or manually set width of this column. + /// NaN means that the column size is no yet calculated. /// - internal double MaxChildDesiredWidth { get; set; } + internal double CurrentWidth { get; set; } = double.NaN; /// - /// Gets or sets the internal copy of the property to be used in calculations, this gets manipulated in Auto-Size mode. + /// Gets the internal calculated or manually set width of this column, as a positive value. /// - internal GridLength CurrentWidth { get; private set; } + internal double ActualCurrentWidth => double.IsNaN(CurrentWidth) ? 0 : CurrentWidth; + + internal bool IsAbsolute => DesiredWidth.IsAbsolute; - internal bool IsAbsolute => CurrentWidth.IsAbsolute; + internal bool IsAuto => DesiredWidth.IsAuto; - internal bool IsAuto => CurrentWidth.IsAuto; + internal bool IsStar => DesiredWidth.IsStar; - internal bool IsStar => CurrentWidth.IsStar; + /// + /// Returns if the column width is fixed with the manual adjustment. + /// + internal bool IsFixed { get; set; } /// /// Gets or sets whether the column can be resized by the user. @@ -66,10 +70,29 @@ public GridLength DesiredWidth private static void DesiredWidth_PropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - // If the developer updates the size of the column, update our internal copy - if (d is DataColumn col) + // If the developer updates the size of the column, update our internal value. + if (d is DataColumn column) { - col.CurrentWidth = col.DesiredWidth; + if (column.DesiredWidth is { GridUnitType: GridUnitType.Pixel, Value: var value }) + { + column.IsFixed = true; + column.CurrentWidth = value; + } + else if (column.DesiredWidth is { GridUnitType: GridUnitType.Star, Value: 0 }) + { + // Handle DesiredWidth="0*" as fixed zero width column. + column.IsFixed = true; + column.CurrentWidth = 0; + } + else + { + // Reset the manual adjusted width. + column.IsFixed = false; + column.CurrentWidth = double.NaN; + } + + // Request to measure for the IsAutoFit or IsStarProportion columns. + column.DataTable?.InvalidateMeasure(); } } @@ -123,9 +146,12 @@ private void PART_ColumnSizer_ManipulationCompleted(object sender, ManipulationC private void ColumnResizedByUserSizer() { // Update our internal representation to be our size now as a fixed value. - CurrentWidth = new(this.ActualWidth); + if (CurrentWidth != this.ActualWidth) + { + CurrentWidth = this.ActualWidth; - // Notify the rest of the table to update - DataTable?.ColumnResized(); + // Notify the rest of the table to update + DataTable?.ColumnResized(); + } } } diff --git a/components/DataTable/src/DataTable/DataRow.cs b/components/DataTable/src/DataTable/DataRow.cs index f3611060e..016f48ea2 100644 --- a/components/DataTable/src/DataTable/DataRow.cs +++ b/components/DataTable/src/DataTable/DataRow.cs @@ -16,7 +16,7 @@ public partial class DataRow : Panel private DataTable? _parentTable; private bool _isTreeView; - private double _treePadding; + internal double TreePadding { get; private set; } /// /// Initializes a new instance of the class. @@ -68,6 +68,8 @@ private void DataRow_Unloaded(object sender, RoutedEventArgs e) /// protected override Size MeasureOverride(Size availableSize) { + //Debug.WriteLine($"DataRow.MeasureOverride"); + // We should probably only have to do this once ever? _parentTable ??= InitializeParentHeaderConnection(); @@ -95,70 +97,72 @@ protected override Size MeasureOverride(Size availableSize) // Handle DataTable Parent else { - if (_parentTable.Children.Count == Children.Count) + int maxChildCount = Math.Min(_parentTable.Children.Count, Children.Count); + + // Measure all children which have corresponding visible DataColumns. + for (int i = 0; i < maxChildCount; i++) { - // TODO: Need to check visibility - // Measure all children since we need to determine the row's height at minimum - for (int i = 0; i < Children.Count; i++) + var child = Children[i]; + var column = _parentTable.Children[i] as DataColumn; + if (column?.Visibility != Visibility.Visible) + continue; + + // For TreeView in the first column, we want the header to expand to encompass + // the maximum indentation of the tree. + //// TODO: We only want/need to do this once? We may want to do if we're not an Auto column too...? + if (i == 0 && _isTreeView) { - if (_parentTable.Children[i] is DataColumn { CurrentWidth.GridUnitType: GridUnitType.Auto } col) + // Get our containing grid from TreeViewItem, start with our indented padding + var parentContainer = this.FindAscendant("MultiSelectGrid") as Grid; + if (parentContainer != null) { - Children[i].Measure(availableSize); - - // For TreeView in the first column, we want the header to expand to encompass - // the maximum indentation of the tree. - double padding = 0; - //// TODO: We only want/need to do this once? We may want to do if we're not an Auto column too...? - if (i == 0 && _isTreeView) + TreePadding = parentContainer.Padding.Left; + // We assume our 'DataRow' is in the last child slot of the Grid, need to know + // how large the other columns are. + for (int j = 0; j < parentContainer.Children.Count - 1; j++) { - // Get our containing grid from TreeViewItem, start with our indented padding - var parentContainer = this.FindAscendant("MultiSelectGrid") as Grid; - if (parentContainer != null) - { - _treePadding = parentContainer.Padding.Left; - // We assume our 'DataRow' is in the last child slot of the Grid, need to know how large the other columns are. - for (int j = 0; j < parentContainer.Children.Count - 1; j++) - { - // TODO: We may need to get the actual size here later in Arrange? - _treePadding += parentContainer.Children[j].DesiredSize.Width; - } - } - padding = _treePadding; + // TODO: We may need to get the actual size here later in Arrange? + TreePadding += parentContainer.Children[j].DesiredSize.Width; } + } + } - // TODO: Do we want this to ever shrink back? - var prev = col.MaxChildDesiredWidth; - col.MaxChildDesiredWidth = Math.Max(col.MaxChildDesiredWidth, Children[i].DesiredSize.Width + padding); - if (col.MaxChildDesiredWidth != prev) - { - // If our measure has changed, then we have to invalidate the arrange of the DataTable - _parentTable.ColumnResized(); - } + double width = column.ActualCurrentWidth; - } - else if (_parentTable.Children[i] is DataColumn { CurrentWidth.GridUnitType: GridUnitType.Pixel } pixel) - { - Children[i].Measure(new(pixel.DesiredWidth.Value, availableSize.Height)); - } - else + if (column.IsAuto && !column.IsFixed) + { + // We should get the *required* width from the child. + child.Measure(new Size(double.PositiveInfinity, availableSize.Height)); + + var childWidth = child.DesiredSize.Width; + if (i == 0) + childWidth += TreePadding; + + // If the adjusted column width is smaller than the current cell width, + // we should call DataTable.MeasureOverride() again to extend it. + if (!(width >= childWidth)) { - Children[i].Measure(availableSize); + _parentTable.InvalidateMeasure(); } - - maxHeight = Math.Max(maxHeight, Children[i].DesiredSize.Height); } + else + { + child.Measure(new Size(width, availableSize.Height)); + } + + maxHeight = Math.Max(maxHeight, child.DesiredSize.Height); } - // TODO: What do we want to do if there's unequal children in the DataTable vs. DataRow? + // Returns the same width as the DataTable requests, regardless of the IsAutoFit column presence. + return new Size(_parentTable.DesiredSize.Width, maxHeight); } - - // Otherwise, return our parent's size as the desired size. - return new(_parentTable?.DesiredSize.Width ?? availableSize.Width, maxHeight); } /// protected override Size ArrangeOverride(Size finalSize) { + //Debug.WriteLine($"DataRow.ArrangeOverride"); + // If we don't have DataTable, just layout children like a horizontal StackPanel. if (_parentTable is null) { @@ -182,37 +186,34 @@ protected override Size ArrangeOverride(Size finalSize) // Handle DataTable Parent else { - int column = 0; - double x = 0; - double spacing = _parentTable.ColumnSpacing; - double width = 0; + int maxChildCount = Math.Min(_parentTable.Children.Count, Children.Count); - int i = 0; - foreach (UIElement child in Children.Where(static e => e.Visibility == Visibility.Visible)) + double columnSpacing = _parentTable.ColumnSpacing; + double x = double.NaN; + + // Arrange all children which have corresponding visible DataColumns. + for (int i = 0; i < maxChildCount; i++) { - // TODO: Need to check Column visibility here as well... - if (column < _parentTable.Children.Count) - { - // TODO: This is messy... - width = (_parentTable.Children[column++] as DataColumn)?.ActualWidth ?? 0; - } + var column = _parentTable.Children[i] as DataColumn; + if (column?.Visibility != Visibility.Visible) + continue; - // Note: For Auto, since we measured our children and bubbled that up to the DataTable layout, then the DataColumn size we grab above should account for the largest of our children. - if (i == 0) - { - child.Arrange(new Rect(x, 0, width, finalSize.Height)); - } + if (double.IsNaN(x)) + x = 0; else - { - // If we're in a tree, remove the indentation from the layout of columns beyond the first. - child.Arrange(new Rect(x - _treePadding, 0, width, finalSize.Height)); - } + x += columnSpacing; + + double width = column.ActualCurrentWidth; + if (i == 0) + width = Math.Max(0, width - TreePadding); + + var child = Children[i]; + child?.Arrange(new Rect(x, 0, width, finalSize.Height)); - x += width + spacing; - i++; + x += width; } - return new Size(x - spacing, finalSize.Height); + return new Size(x, finalSize.Height); } } } diff --git a/components/DataTable/src/DataTable/DataTable.cs b/components/DataTable/src/DataTable/DataTable.cs index 5842c30b9..1fbacdc84 100644 --- a/components/DataTable/src/DataTable/DataTable.cs +++ b/components/DataTable/src/DataTable/DataTable.cs @@ -2,8 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System.Runtime.CompilerServices; - namespace CommunityToolkit.WinUI.Controls; /// @@ -12,15 +10,12 @@ namespace CommunityToolkit.WinUI.Controls; /// public partial class DataTable : Panel { - // TODO: We should cache this result and update if column properties change - internal bool IsAnyColumnAuto => Children.Any(static e => e is DataColumn { CurrentWidth.GridUnitType: GridUnitType.Auto }); - // TODO: Check with Sergio if there's a better structure here, as I don't need a Dictionary like ConditionalWeakTable internal HashSet Rows { get; private set; } = new(); internal void ColumnResized() { - InvalidateArrange(); + InvalidateMeasure(); foreach (var row in Rows) { @@ -47,120 +42,208 @@ public double ColumnSpacing /// protected override Size MeasureOverride(Size availableSize) { - double fixedWidth = 0; - double proportionalUnits = 0; - double autoSized = 0; - + //Debug.WriteLine($"DataTable.MeasureOverride"); + double columnSpacing = ColumnSpacing; + double totalWidth = double.NaN; double maxHeight = 0; - var elements = Children.Where(static e => e.Visibility == Visibility.Visible && e is DataColumn); + int starRemains = 0; + double starAmounts = 0; - // We only need to measure elements that are visible - foreach (DataColumn column in elements) + bool invokeRowsMeasures = false; + bool invokeRowsArranges = false; + + for (int i = 0; i < Children.Count; i++) { - if (column.IsStar) + // We only need to measure children that are visible + var column = Children[i] as DataColumn; + if (column?.Visibility != Visibility.Visible) + continue; + + if (double.IsNaN(totalWidth)) + totalWidth = 0; + else + totalWidth += columnSpacing; + + double width = column.ActualCurrentWidth; + + if (column.IsFixed) { - proportionalUnits += column.DesiredWidth.Value; + //Debug.WriteLine($" Column[{i}] ({column.DesiredWidth}) width is fixed to: {width}"); + + // If availableSize.Width is infinite, the column will also get infinite available width. + column.Measure(new Size(width, availableSize.Height)); } - else if (column.IsAbsolute) + else if (column.IsStar) { - fixedWidth += column.DesiredWidth.Value; + ++starRemains; + starAmounts += column.DesiredWidth.Value; + continue; + } + else // (column.IsAuto) + { + // Get the best-fit width of the header content. + column.Measure(new Size(double.PositiveInfinity, availableSize.Height)); + + width = column.DesiredSize.Width; + foreach (var row in Rows) + { + if (i < row.Children.Count) + { + var child = row.Children[i]; + + var childWidth = child.DesiredSize.Width; + if (i == 0) + childWidth += row.TreePadding; + + width = Math.Max(width, childWidth); + } + } + //Debug.WriteLine($" Column[{i}] ({column.DesiredWidth}) width is adjusted to: {width}"); + column.CurrentWidth = width; + + // The column width of the corresponding cell in each row is taken into account + // in the next layout pass. + invokeRowsMeasures = true; } - } - // Add in spacing between columns to our fixed size allotment - fixedWidth += (elements.Count() - 1) * ColumnSpacing; + totalWidth += width; + maxHeight = Math.Max(maxHeight, column.DesiredSize.Height); + } - // TODO: Handle infinite width? - var proportionalAmount = (availableSize.Width - fixedWidth) / proportionalUnits; + if (double.IsNaN(totalWidth)) + return new Size(0, 0); - foreach (DataColumn column in elements) + if (starRemains > 0) { - if (column.IsStar) - { - column.Measure(new Size(proportionalAmount * column.CurrentWidth.Value, availableSize.Height)); - } - else if (column.IsAbsolute) + Debug.Assert(starAmounts > 0); + double starUnit; + if (double.IsInfinity(availableSize.Width)) { - column.Measure(new Size(column.CurrentWidth.Value, availableSize.Height)); + starUnit = double.NaN; + + // If availableSize.Width is infinite, the size calculation will be deferred + // until the Arrange pass. + invokeRowsArranges = true; } else { - // TODO: Technically this is using 'Auto' on the Header content - // What the developer probably intends is it to be adjusted based on the contents of the rows... - // To enable this scenario, we'll need to actually measure the contents of the rows for that column - // in DataRow and figure out the maximum size to report back and adjust here in some sort of hand-shake - // for the layout process... (i.e. get the data in the measure step, use it in the arrange step here, - // then invalidate the child arranges [don't re-measure and cause loop]...) - - // For now, we'll just use the header content as a guideline to see if things work. - - // Avoid negative values when columns don't fit `availableSize`. Otherwise the `Size` constructor will throw. - column.Measure(new Size(Math.Max(availableSize.Width - fixedWidth - autoSized, 0), availableSize.Height)); + starUnit = Math.Max(0, availableSize.Width - totalWidth) / starAmounts; + } - // Keep track of already 'allotted' space, use either the maximum child size (if we know it) or the header content - autoSized += Math.Max(column.DesiredSize.Width, column.MaxChildDesiredWidth); + for (int i = 0; starRemains != 0; i++) + { + var column = Children[i] as DataColumn; + if (column?.Visibility != Visibility.Visible) + continue; + + if (column.IsFixed || !column.IsStar) + continue; + + --starRemains; + + double width; + if (double.IsNaN(starUnit)) + { + // Just get and store the natural size. + column.Measure(new Size(double.PositiveInfinity, availableSize.Height)); + + width = column.DesiredSize.Width; + } + else + { + // Get the proportion of the remaining space. + width = starUnit * column.DesiredWidth.Value; + + column.Measure(new Size(width, availableSize.Height)); + } + //Debug.WriteLine($" Column[{i}] ({column.DesiredWidth}) width is adjusted to: {width}"); + column.CurrentWidth = width; + + totalWidth += width; + maxHeight = Math.Max(maxHeight, column.DesiredSize.Height); } + } - maxHeight = Math.Max(maxHeight, column.DesiredSize.Height); + if (invokeRowsMeasures) + { + foreach (var row in Rows) + row.InvalidateMeasure(); + } + else if (invokeRowsArranges) + { + foreach (var row in Rows) + row.InvalidateArrange(); } - return new Size(availableSize.Width, maxHeight); + return new Size(totalWidth, maxHeight); } /// protected override Size ArrangeOverride(Size finalSize) { - double fixedWidth = 0; - double proportionalUnits = 0; - double autoSized = 0; + //Debug.WriteLine($"DataTable.ArrangeOverride"); + double columnSpacing = ColumnSpacing; + double totalWidth = double.NaN; - var elements = Children.Where(static e => e.Visibility == Visibility.Visible && e is DataColumn); + int starRemains = 0; + double starAmounts = 0; - // We only need to measure elements that are visible - foreach (DataColumn column in elements) + for (int i = 0; i < Children.Count; i++) { - if (column.IsStar) - { - proportionalUnits += column.CurrentWidth.Value; - } - else if (column.IsAbsolute) + // We only need to measure children that are visible + var column = Children[i] as DataColumn; + if (column?.Visibility != Visibility.Visible) + continue; + + if (double.IsNaN(totalWidth)) + totalWidth = 0; + else + totalWidth += columnSpacing; + + if (column.IsFixed || !column.IsStar) { - fixedWidth += column.CurrentWidth.Value; + totalWidth += column.ActualCurrentWidth; } else { - autoSized += Math.Max(column.DesiredSize.Width, column.MaxChildDesiredWidth); + ++starRemains; + starAmounts += column.DesiredWidth.Value; } } - // TODO: Handle infinite width? - // TODO: This can go out of bounds or something around here when pushing a resized column to the right... - var proportionalAmount = (finalSize.Width - fixedWidth - autoSized) / proportionalUnits; - - double width = 0; - double x = 0; + Debug.Assert(starRemains == 0 || starAmounts > 0); + double starUnit = Math.Max(0, finalSize.Width - totalWidth) / starAmounts; - foreach (DataColumn column in elements) + double x = double.NaN; + for (int i = 0; i < Children.Count; i++) { - if (column.IsStar) - { - width = proportionalAmount * column.CurrentWidth.Value; - column.Arrange(new Rect(x, 0, width, finalSize.Height)); - } - else if (column.IsAbsolute) + // We only need to measure children that are visible + var column = Children[i] as DataColumn; + if (column?.Visibility != Visibility.Visible) + continue; + + if (double.IsNaN(x)) + x = 0; + else + x += columnSpacing; + + double width; + if (column.IsFixed || !column.IsStar) { - width = column.CurrentWidth.Value; - column.Arrange(new Rect(x, 0, width, finalSize.Height)); + width = column.ActualCurrentWidth; } else { - // TODO: We use the comparison of sizes a lot, should we cache in the DataColumn itself? - width = Math.Max(column.DesiredSize.Width, column.MaxChildDesiredWidth); - column.Arrange(new Rect(x, 0, width, finalSize.Height)); + width = starUnit * column.DesiredWidth.Value; + + // Store the actual star column width. + column.CurrentWidth = width; } - x += width + ColumnSpacing; + column.Arrange(new Rect(x, 0, width, finalSize.Height)); + + x += width; } return finalSize;