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/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 6f969b15e..9a8cf01af 100644 --- a/components/DataTable/src/DataTable/DataColumn.cs +++ b/components/DataTable/src/DataTable/DataColumn.cs @@ -4,24 +4,39 @@ namespace CommunityToolkit.WinUI.Controls; +/// +/// Represents a column. +/// [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; + internal DataTable? DataTable => _parent?.TryGetTarget(out DataTable? parent) == true ? parent : null; + + /// + /// 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 CurrentWidth { get; set; } = double.NaN; + /// - /// Gets or sets the width of the largest child contained within the visible s of the . + /// Gets the internal calculated or manually set width of this column, as a positive value. /// - internal double MaxChildDesiredWidth { get; set; } + internal double ActualCurrentWidth => double.IsNaN(CurrentWidth) ? 0 : CurrentWidth; + + internal bool IsAbsolute => DesiredWidth.IsAbsolute; + + internal bool IsAuto => DesiredWidth.IsAuto; + + internal bool IsStar => DesiredWidth.IsStar; /// - /// Gets or sets the internal copy of the property to be used in calculations, this gets manipulated in Auto-Size mode. + /// Returns if the column width is fixed with the manual adjustment. /// - internal GridLength CurrentWidth { get; private set; } + internal bool IsFixed { get; set; } /// /// Gets or sets whether the column can be resized by the user. @@ -55,18 +70,41 @@ 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(); } } + /// + /// Initializes a new instance of the class. + /// public DataColumn() { this.DefaultStyleKey = typeof(DataColumn); } + /// protected override void OnApplyTemplate() { if (PART_ColumnSizer != null) @@ -108,13 +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); - - // Notify the rest of the table to update - if (_parent?.TryGetTarget(out DataTable? parent) == true - && parent != null) + if (CurrentWidth != this.ActualWidth) { - parent.ColumnResized(); + CurrentWidth = this.ActualWidth; + + // Notify the rest of the table to update + DataTable?.ColumnResized(); } } } 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 @@ + diff --git a/components/DataTable/src/DataTable/DataRow.cs b/components/DataTable/src/DataTable/DataRow.cs index 6546150ae..d1be32325 100644 --- a/components/DataTable/src/DataTable/DataRow.cs +++ b/components/DataTable/src/DataTable/DataRow.cs @@ -6,16 +6,21 @@ 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. // TODO: WeakReference? - private Panel? _parentPanel; private DataTable? _parentTable; private bool _isTreeView; - private double _treePadding; + internal double TreePadding { get; private set; } + /// + /// Initializes a new instance of the class. + /// public DataRow() { Unloaded += this.DataRow_Unloaded; @@ -26,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 @@ -42,24 +46,15 @@ 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 _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) { @@ -67,153 +62,158 @@ 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) { + //Debug.WriteLine($"DataRow.MeasureOverride"); + // We should probably only have to do this once ever? - _parentPanel ??= InitializeParentHeaderConnection(); + _parentTable ??= InitializeParentHeaderConnection(); double maxHeight = 0; - if (Children.Count > 0) + // If we don't have a DataTable, just layout children like a horizontal StackPanel. + if (_parentTable 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); } - // Handle DataTable Parent - else if (_parentTable != null - && _parentTable.Children.Count == Children.Count) + + return new Size(totalWidth, maxHeight); + } + // Handle DataTable Parent + else + { + 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) - { - // 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: 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) + 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++) { - // If our measure has changed, then we have to invalidate the arrange of the DataTable - _parentTable.ColumnResized(); + // TODO: We may need to get the actual size here later in Arrange? + TreePadding += parentContainer.Children[j].DesiredSize.Width; } - - } - else if (_parentTable.Children[i] is DataColumn { CurrentWidth.GridUnitType: GridUnitType.Pixel } pixel) - { - Children[i].Measure(new(pixel.DesiredWidth.Value, availableSize.Height)); - } - else - { - Children[i].Measure(availableSize); } + } - maxHeight = Math.Max(maxHeight, Children[i].DesiredSize.Height); + double width = column.ActualCurrentWidth; + + if (column.IsFixed) + { + child.Measure(new Size(width, availableSize.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++) + else { - if (grid.ColumnDefinitions[i].Width.GridUnitType == GridUnitType.Pixel) - { - Children[i].Measure(new(grid.ColumnDefinitions[i].Width.Value, availableSize.Height)); - } - else + // 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 (column.IsAuto && !(width >= childWidth)) { - Children[i].Measure(availableSize); + _parentTable.InvalidateMeasure(); } - - maxHeight = Math.Max(maxHeight, Children[i].DesiredSize.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? - } - // Otherwise, return our parent's size as the desired size. - return new(_parentPanel?.DesiredSize.Width ?? availableSize.Width, maxHeight); + // Returns the same width as the DataTable requests, regardless of the IsAutoFit column presence. + return new Size(_parentTable.DesiredSize.Width, maxHeight); + } } + /// protected override Size ArrangeOverride(Size finalSize) { - int column = 0; - double x = 0; + //Debug.WriteLine($"DataRow.ArrangeOverride"); + + // If we don't have DataTable, just layout children like a horizontal StackPanel. + if (_parentTable is null) + { + double x = 0; + + for (int i = 0; i < Children.Count; i++) + { + var child = Children[i]; + if (child?.Visibility != Visibility.Visible) + continue; - // 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 = child.DesiredSize.Width; - double width = 0; + child.Arrange(new Rect(x, 0, width, finalSize.Height)); - if (_parentPanel != null) + x += width; + } + + return new Size(x, finalSize.Height); + } + // Handle DataTable Parent + else { - int i = 0; - foreach (UIElement child in Children.Where(static e => e.Visibility == Visibility.Visible)) + int maxChildCount = Math.Min(_parentTable.Children.Count, Children.Count); + + double columnSpacing = _parentTable.ColumnSpacing; + double x = double.NaN; + + // Arrange all children which have corresponding visible DataColumns. + for (int i = 0; i < maxChildCount; i++) { - 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) - { - // TODO: This is messy... - width = (table.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; - x += width + spacing; - i++; + 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; } - return new Size(x - spacing, finalSize.Height); + return new Size(x, finalSize.Height); } - - return finalSize; } } diff --git a/components/DataTable/src/DataTable/DataTable.cs b/components/DataTable/src/DataTable/DataTable.cs index 7b78b2beb..bb4c7fe92 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) { @@ -44,121 +39,212 @@ 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; - 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.CurrentWidth.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.CurrentWidth.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.CurrentWidth.IsStar) - { - column.Measure(new Size(proportionalAmount * column.CurrentWidth.Value, availableSize.Height)); - } - else if (column.CurrentWidth.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.CurrentWidth.IsStar) - { - proportionalUnits += column.CurrentWidth.Value; - } - else if (column.CurrentWidth.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.CurrentWidth.IsStar) - { - width = proportionalAmount * column.CurrentWidth.Value; - column.Arrange(new Rect(x, 0, width, finalSize.Height)); - } - else if (column.CurrentWidth.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. + //Debug.WriteLine($" Column[{i}] ({column.DesiredWidth}) width is re-adjusted to: {width}"); + column.CurrentWidth = width; } - x += width + ColumnSpacing; + column.Arrange(new Rect(x, 0, width, finalSize.Height)); + + x += width; } return finalSize;