diff --git a/docfx/articles/columns-and-editing.md b/docfx/articles/columns-and-editing.md
index 1a543ccb..e3f9cee3 100644
--- a/docfx/articles/columns-and-editing.md
+++ b/docfx/articles/columns-and-editing.md
@@ -64,6 +64,60 @@ Double-click the column header resize handle to fit the column to visible conten
CanUserReorderColumns="True" />
```
+### Sharing widths across grids
+
+Use one `DataGridColumnWidthSharingScope` resource for every grid that should participate, then assign the same `WidthSharingGroup` to corresponding column definitions. The scope starts a group at its largest registered width and propagates later programmatic, automatic, and user-initiated width changes in either direction.
+
+```xml
+
+
+
+
+
+
+
+
+
+
+```
+
+```csharp
+public sealed class ComparisonViewModel : ReactiveObject
+{
+ public DataGridColumnDefinitionList CurrentColumns { get; } = CreateColumns();
+ public DataGridColumnDefinitionList BaselineColumns { get; } = CreateColumns();
+
+ private static DataGridColumnDefinitionList CreateColumns() => new()
+ {
+ new DataGridTextColumnDefinition
+ {
+ Header = "Name",
+ Width = DataGridLength.Auto,
+ WidthSharingGroup = "name"
+ },
+ new DataGridNumericColumnDefinition
+ {
+ Header = "Amount",
+ Width = new DataGridLength(120),
+ WidthSharingGroup = "amount"
+ }
+ };
+}
+```
+
+For columns declared directly in XAML, set `sizing:DataGridColumnWidthSharing.Group="name"` instead. Group names are case-sensitive. Pixel columns remain pixel-sized, intrinsic sizing modes such as `Auto` are preserved, and star columns become pixel columns when synchronized because an exact width must be shared across grids with potentially different available space. The scope respects the common `MinWidth`/`MaxWidth` range; if constraints do not overlap, each column is clamped to its own range. Remove the group or scope to stop synchronization. Call `Synchronize()` when you need to recompute a group from the largest current width explicitly.
+
## Headers, Sorting, and Filters
Columns expose header content and sorting metadata:
diff --git a/src/Avalonia.Controls.DataGrid.UnitTests/Sizing/DataGridColumnWidthSharingTests.cs b/src/Avalonia.Controls.DataGrid.UnitTests/Sizing/DataGridColumnWidthSharingTests.cs
new file mode 100644
index 00000000..92e268a1
--- /dev/null
+++ b/src/Avalonia.Controls.DataGrid.UnitTests/Sizing/DataGridColumnWidthSharingTests.cs
@@ -0,0 +1,494 @@
+// Copyright (c) Wieslaw Soltes. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+
+using System.Collections.ObjectModel;
+using Avalonia.Controls;
+using Avalonia.Controls.DataGridSizing;
+using Avalonia.Headless.XUnit;
+using Avalonia.Markup.Xaml;
+using Xunit;
+
+namespace Avalonia.Controls.DataGridTests.Sizing;
+
+public class DataGridColumnWidthSharingTests
+{
+ [Fact]
+ public void Scope_Uses_Largest_Initial_Width_And_Propagates_Later_Changes()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn firstColumn = CreateColumn(100, "name");
+ DataGridTextColumn secondColumn = CreateColumn(180, "name");
+
+ DataGrid firstGrid = CreateGrid(scope, firstColumn);
+ DataGrid secondGrid = CreateGrid(scope, secondColumn);
+
+ Assert.Equal(180, firstColumn.ActualWidth);
+ Assert.Equal(180, secondColumn.ActualWidth);
+
+ firstColumn.Resize(firstColumn.Width, new DataGridLength(240), userInitiated: true);
+
+ Assert.Equal(240, firstColumn.ActualWidth);
+ Assert.Equal(240, secondColumn.ActualWidth);
+
+ secondColumn.Width = new DataGridLength(80);
+
+ Assert.Equal(80, firstColumn.ActualWidth);
+ Assert.Equal(80, secondColumn.ActualWidth);
+ Assert.Same(scope, firstGrid.ColumnWidthSharingScope);
+ Assert.Same(scope, secondGrid.ColumnWidthSharingScope);
+ }
+
+ [Fact]
+ public void Scope_Keeps_Different_Groups_Independent()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn nameColumn = CreateColumn(100, "name");
+ DataGridTextColumn amountColumn = CreateColumn(160, "amount");
+ CreateGrid(scope, nameColumn);
+ CreateGrid(scope, amountColumn);
+
+ nameColumn.Width = new DataGridLength(225);
+
+ Assert.Equal(225, nameColumn.ActualWidth);
+ Assert.Equal(160, amountColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Removed_Column_Is_No_Longer_Synchronized()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn firstColumn = CreateColumn(100, "name");
+ DataGridTextColumn removedColumn = CreateColumn(140, "name");
+ CreateGrid(scope, firstColumn);
+ DataGrid secondGrid = CreateGrid(scope, removedColumn);
+
+ secondGrid.Columns.Remove(removedColumn);
+ var independentGrid = new DataGrid();
+ independentGrid.Columns.Add(removedColumn);
+ firstColumn.Width = new DataGridLength(260);
+
+ Assert.Equal(260, firstColumn.ActualWidth);
+ Assert.Equal(140, removedColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Changing_Group_Updates_Registration()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn firstColumn = CreateColumn(100, "name");
+ DataGridTextColumn secondColumn = CreateColumn(160, "name");
+ CreateGrid(scope, firstColumn);
+ CreateGrid(scope, secondColumn);
+
+ DataGridColumnWidthSharing.SetGroup(secondColumn, "other");
+ firstColumn.Width = new DataGridLength(210);
+
+ Assert.Equal(210, firstColumn.ActualWidth);
+ Assert.Equal(160, secondColumn.ActualWidth);
+
+ DataGridColumnWidthSharing.SetGroup(secondColumn, "name");
+
+ Assert.Equal(210, secondColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Changing_Grid_Scope_Unregisters_Columns_From_The_Old_Scope()
+ {
+ var oldScope = new DataGridColumnWidthSharingScope();
+ var newScope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn firstColumn = CreateColumn(100, "name");
+ DataGridTextColumn secondColumn = CreateColumn(160, "name");
+ CreateGrid(oldScope, firstColumn);
+ DataGrid secondGrid = CreateGrid(oldScope, secondColumn);
+
+ secondGrid.ColumnWidthSharingScope = newScope;
+ firstColumn.Width = new DataGridLength(220);
+
+ Assert.Equal(220, firstColumn.ActualWidth);
+ Assert.Equal(160, secondColumn.ActualWidth);
+ }
+
+ [AvaloniaFact]
+ public void Reattached_Grid_Reregisters_Columns_Pruned_While_Detached()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn detachedColumn = CreateColumn(100, "name");
+ DataGridTextColumn activeColumn = CreateColumn(150, "name");
+ DataGrid detachedGrid = CreateGrid(scope, detachedColumn);
+ CreateGrid(scope, activeColumn);
+ var root = new Window { Content = detachedGrid };
+
+ try
+ {
+ root.Show();
+ root.Content = null;
+
+ activeColumn.Width = new DataGridLength(210);
+ root.Content = detachedGrid;
+ root.UpdateLayout();
+
+ Assert.Equal(210, detachedColumn.ActualWidth);
+
+ activeColumn.Width = new DataGridLength(240);
+
+ Assert.Equal(240, detachedColumn.ActualWidth);
+ }
+ finally
+ {
+ root.Close();
+ }
+ }
+
+ [AvaloniaFact]
+ public void Reattached_Grid_Adopts_Active_Group_Width_Instead_Of_Stale_Width()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn detachedColumn = CreateColumn(200, "name");
+ DataGridTextColumn activeColumn = CreateColumn(100, "name");
+ DataGrid detachedGrid = CreateGrid(scope, detachedColumn);
+ CreateGrid(scope, activeColumn);
+ var root = new Window { Content = detachedGrid };
+
+ try
+ {
+ root.Show();
+ root.Content = null;
+
+ activeColumn.Width = new DataGridLength(100);
+ Assert.Equal(100, activeColumn.ActualWidth);
+ detachedColumn.Width = new DataGridLength(200);
+
+ root.Content = detachedGrid;
+ root.UpdateLayout();
+
+ Assert.Equal(100, detachedColumn.ActualWidth);
+ Assert.Equal(100, activeColumn.ActualWidth);
+ }
+ finally
+ {
+ root.Close();
+ }
+ }
+
+ [AvaloniaFact]
+ public void Width_Adjustment_Side_Effects_Are_Reported_To_Shared_Participants()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn leadingColumn = CreateColumn(100, group: null);
+ DataGridTextColumn adjustedColumn = CreateColumn(160, "name");
+ DataGridTextColumn peerColumn = CreateColumn(160, "name");
+ DataGrid firstGrid = CreateGrid(scope, leadingColumn);
+ firstGrid.Columns.Add(adjustedColumn);
+ DataGrid secondGrid = CreateGrid(scope, peerColumn);
+ firstGrid.CanUserResizeColumns = true;
+ firstGrid.Height = 120;
+ secondGrid.Height = 120;
+ var root = new Window
+ {
+ Width = 500,
+ Height = 300,
+ Content = new StackPanel
+ {
+ Children =
+ {
+ firstGrid,
+ secondGrid
+ }
+ }
+ };
+
+ try
+ {
+ root.Show();
+ root.UpdateLayout();
+
+ double startingWidth = adjustedColumn.ActualWidth;
+ double remaining = firstGrid.DecreaseColumnWidths(
+ adjustedColumn.DisplayIndex,
+ amount: -40,
+ userInitiated: false);
+
+ Assert.Equal(0, remaining);
+ Assert.Equal(startingWidth - 40, adjustedColumn.ActualWidth);
+ Assert.Equal(adjustedColumn.ActualWidth, peerColumn.ActualWidth);
+ }
+ finally
+ {
+ root.Close();
+ }
+ }
+
+ [AvaloniaFact]
+ public void Detached_Grid_Scope_Change_Removes_Old_Scope_Registration()
+ {
+ var oldScope = new DataGridColumnWidthSharingScope();
+ var newScope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn oldScopeColumn = CreateColumn(100, "name");
+ DataGridTextColumn movedColumn = CreateColumn(150, "name");
+ DataGridTextColumn newScopeColumn = CreateColumn(175, "name");
+ CreateGrid(oldScope, oldScopeColumn);
+ DataGrid movedGrid = CreateGrid(oldScope, movedColumn);
+ CreateGrid(newScope, newScopeColumn);
+ var root = new Window { Content = movedGrid };
+
+ try
+ {
+ root.Show();
+ root.Content = null;
+ movedGrid.ColumnWidthSharingScope = newScope;
+ root.Content = movedGrid;
+ root.UpdateLayout();
+
+ Assert.Equal(175, movedColumn.ActualWidth);
+
+ oldScopeColumn.Width = new DataGridLength(220);
+
+ Assert.Equal(175, movedColumn.ActualWidth);
+
+ newScopeColumn.Width = new DataGridLength(240);
+
+ Assert.Equal(240, movedColumn.ActualWidth);
+ }
+ finally
+ {
+ root.Close();
+ }
+ }
+
+ [Fact]
+ public void Scope_Uses_Common_Constraints_And_Preserves_Auto_Sizing_Mode()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn constrainedColumn = CreateColumn(100, "name");
+ constrainedColumn.MaxWidth = 150;
+ DataGridTextColumn autoColumn = CreateColumn(DataGridLength.Auto, "name");
+ CreateGrid(scope, constrainedColumn);
+ CreateGrid(scope, autoColumn);
+
+ autoColumn.Width = new DataGridLength(220);
+
+ Assert.Equal(150, constrainedColumn.ActualWidth);
+ Assert.Equal(150, autoColumn.ActualWidth);
+
+ autoColumn.Width = DataGridLength.Auto;
+ scope.Synchronize();
+
+ Assert.True(autoColumn.Width.IsAuto);
+ Assert.Equal(constrainedColumn.ActualWidth, autoColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Scope_Converts_Equal_Width_Star_Column_To_Synchronized_Pixels()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn pixelColumn = CreateColumn(100, "name");
+ DataGridTextColumn starColumn = CreateColumn(
+ new DataGridLength(1, DataGridLengthUnitType.Star, 100, 100),
+ "name");
+
+ CreateGrid(scope, pixelColumn);
+ CreateGrid(scope, starColumn);
+
+ Assert.True(starColumn.Width.IsAbsolute);
+ Assert.Equal(100, starColumn.ActualWidth);
+ Assert.Equal(pixelColumn.ActualWidth, starColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Scope_Does_Not_Convert_Unmeasured_Star_Columns_During_Registration()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn firstColumn = CreateColumn(
+ new DataGridLength(1, DataGridLengthUnitType.Star),
+ "name");
+ DataGridTextColumn secondColumn = CreateColumn(
+ new DataGridLength(1, DataGridLengthUnitType.Star),
+ "name");
+
+ CreateGrid(scope, firstColumn);
+ Assert.True(firstColumn.Width.IsStar);
+
+ CreateGrid(scope, secondColumn);
+
+ Assert.True(firstColumn.Width.IsStar);
+ Assert.True(secondColumn.Width.IsStar);
+ }
+
+ [Fact]
+ public void Unmeasured_Star_Column_Does_Not_Clamp_Measured_Participant()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn measuredColumn = CreateColumn(10, "name");
+ measuredColumn.MinWidth = 0;
+ DataGridTextColumn unmeasuredStarColumn = CreateColumn(
+ new DataGridLength(1, DataGridLengthUnitType.Star),
+ "name");
+
+ CreateGrid(scope, measuredColumn);
+ CreateGrid(scope, unmeasuredStarColumn);
+
+ Assert.Equal(10, measuredColumn.ActualWidth);
+ Assert.True(unmeasuredStarColumn.Width.IsStar);
+
+ unmeasuredStarColumn.SetWidthInternalNoCallback(
+ new DataGridLength(1, DataGridLengthUnitType.Star, 30, 30));
+ unmeasuredStarColumn.IsInitialDesiredWidthDetermined = true;
+
+ Assert.Equal(30, measuredColumn.ActualWidth);
+ Assert.Equal(30, unmeasuredStarColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void First_Measure_Completion_Reports_Previously_Unmeasured_Star_Columns()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ DataGridTextColumn firstColumn = CreateColumn(
+ new DataGridLength(1, DataGridLengthUnitType.Star),
+ "name");
+ DataGridTextColumn secondColumn = CreateColumn(
+ new DataGridLength(1, DataGridLengthUnitType.Star),
+ "name");
+ CreateGrid(scope, firstColumn);
+ CreateGrid(scope, secondColumn);
+
+ firstColumn.SetWidthInternalNoCallback(
+ new DataGridLength(1, DataGridLengthUnitType.Star, 100, 100));
+ firstColumn.IsInitialDesiredWidthDetermined = true;
+ secondColumn.SetWidthInternalNoCallback(
+ new DataGridLength(1, DataGridLengthUnitType.Star, 180, 180));
+ secondColumn.IsInitialDesiredWidthDetermined = true;
+
+ Assert.True(firstColumn.Width.IsAbsolute);
+ Assert.True(secondColumn.Width.IsAbsolute);
+ Assert.Equal(180, firstColumn.ActualWidth);
+ Assert.Equal(firstColumn.ActualWidth, secondColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Inherited_Grid_ColumnWidth_Changes_Are_Shared()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ var firstColumn = new DataGridTextColumn();
+ var secondColumn = new DataGridTextColumn();
+ DataGridColumnWidthSharing.SetGroup(firstColumn, "name");
+ DataGridColumnWidthSharing.SetGroup(secondColumn, "name");
+ DataGrid firstGrid = CreateGrid(scope, firstColumn);
+ DataGrid secondGrid = CreateGrid(scope, secondColumn);
+
+ Assert.True(firstColumn.InheritsWidth);
+ Assert.True(secondColumn.InheritsWidth);
+
+ firstGrid.ColumnWidth = new DataGridLength(165);
+
+ Assert.Equal(165, firstColumn.ActualWidth);
+ Assert.Equal(165, secondColumn.ActualWidth);
+
+ secondGrid.ColumnWidth = new DataGridLength(210);
+
+ Assert.Equal(210, firstColumn.ActualWidth);
+ Assert.Equal(210, secondColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Column_Definitions_Apply_And_Update_Width_Sharing_Group()
+ {
+ var scope = new DataGridColumnWidthSharingScope();
+ var firstDefinition = new DataGridTextColumnDefinition
+ {
+ Width = new DataGridLength(100),
+ WidthSharingGroup = "name"
+ };
+ var secondDefinition = new DataGridTextColumnDefinition
+ {
+ Width = new DataGridLength(175),
+ WidthSharingGroup = "name"
+ };
+ DataGrid firstGrid = CreateGrid(scope, firstDefinition);
+ DataGrid secondGrid = CreateGrid(scope, secondDefinition);
+ DataGridColumn firstColumn = firstGrid.Columns[0];
+ DataGridColumn secondColumn = secondGrid.Columns[0];
+
+ Assert.Equal("name", DataGridColumnWidthSharing.GetGroup(firstColumn));
+ Assert.Equal(175, firstColumn.ActualWidth);
+ Assert.Equal(175, secondColumn.ActualWidth);
+
+ secondDefinition.WidthSharingGroup = "other";
+ firstColumn.Width = new DataGridLength(230);
+
+ Assert.Equal("other", DataGridColumnWidthSharing.GetGroup(secondColumn));
+ Assert.Equal(230, firstColumn.ActualWidth);
+ Assert.Equal(175, secondColumn.ActualWidth);
+ }
+
+ [Fact]
+ public void Scope_And_Attached_Group_Can_Be_Configured_In_Xaml()
+ {
+ const string xaml = """
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ """;
+
+ StackPanel panel = AvaloniaRuntimeXamlLoader.Parse(xaml, typeof(DataGrid).Assembly);
+ DataGrid firstGrid = Assert.IsType(panel.Children[0]);
+ DataGrid secondGrid = Assert.IsType(panel.Children[1]);
+
+ Assert.Same(firstGrid.ColumnWidthSharingScope, secondGrid.ColumnWidthSharingScope);
+ Assert.Equal(180, firstGrid.Columns[0].ActualWidth);
+ Assert.Equal(180, secondGrid.Columns[0].ActualWidth);
+ }
+
+ private static DataGridTextColumn CreateColumn(double width, string group)
+ {
+ return CreateColumn(new DataGridLength(width), group);
+ }
+
+ private static DataGridTextColumn CreateColumn(DataGridLength width, string group)
+ {
+ var column = new DataGridTextColumn
+ {
+ Width = width
+ };
+ DataGridColumnWidthSharing.SetGroup(column, group);
+ return column;
+ }
+
+ private static DataGrid CreateGrid(
+ DataGridColumnWidthSharingScope scope,
+ DataGridColumn column)
+ {
+ var grid = new DataGrid
+ {
+ ColumnWidthSharingScope = scope
+ };
+ grid.Columns.Add(column);
+ return grid;
+ }
+
+ private static DataGrid CreateGrid(
+ DataGridColumnWidthSharingScope scope,
+ DataGridColumnDefinition definition)
+ {
+ return new DataGrid
+ {
+ ColumnWidthSharingScope = scope,
+ ColumnDefinitionsSource = new ObservableCollection { definition }
+ };
+ }
+}
diff --git a/src/Avalonia.Controls.DataGrid/ColumnDefinitions/DataGridColumnDefinition.cs b/src/Avalonia.Controls.DataGrid/ColumnDefinitions/DataGridColumnDefinition.cs
index ea4a613b..92ae3156 100644
--- a/src/Avalonia.Controls.DataGrid/ColumnDefinitions/DataGridColumnDefinition.cs
+++ b/src/Avalonia.Controls.DataGrid/ColumnDefinitions/DataGridColumnDefinition.cs
@@ -13,6 +13,7 @@
using Avalonia.Utilities;
using Avalonia.Controls.DataGridSearching;
using Avalonia.Controls.DataGridSorting;
+using Avalonia.Controls.DataGridSizing;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.Styling;
@@ -90,6 +91,7 @@ abstract class DataGridColumnDefinition : INotifyPropertyChanged
private string _sortMemberPath;
private object _tag;
private object _columnKey;
+ private string _widthSharingGroup;
private System.Collections.IComparer _customSortComparer;
private IDataGridColumnValueAccessor _valueAccessor;
private Type _valueType;
@@ -273,6 +275,15 @@ public object ColumnKey
set => SetProperty(ref _columnKey, value);
}
+ ///
+ /// Gets or sets the name of the column width-sharing group.
+ ///
+ public string WidthSharingGroup
+ {
+ get => _widthSharingGroup;
+ set => SetProperty(ref _widthSharingGroup, value);
+ }
+
public System.Collections.IComparer CustomSortComparer
{
get => _customSortComparer;
@@ -397,6 +408,7 @@ protected virtual void ApplyBaseProperties(DataGridColumn column, DataGridColumn
{
column.Header = Header;
column.ColumnKey = ColumnKey;
+ DataGridColumnWidthSharing.SetGroup(column, WidthSharingGroup);
column.SortMemberPath = SortMemberPath;
column.Tag = Tag;
column.CustomSortComparer = CustomSortComparer;
@@ -609,6 +621,9 @@ private bool ApplyBasePropertyChange(DataGridColumn column, DataGridColumnDefini
case nameof(Tag):
column.Tag = Tag;
return true;
+ case nameof(WidthSharingGroup):
+ DataGridColumnWidthSharing.SetGroup(column, WidthSharingGroup);
+ return true;
case nameof(CustomSortComparer):
column.CustomSortComparer = CustomSortComparer;
return true;
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.ColumnWidthSharing.cs b/src/Avalonia.Controls.DataGrid/DataGrid.ColumnWidthSharing.cs
new file mode 100644
index 00000000..8a3a6de3
--- /dev/null
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.ColumnWidthSharing.cs
@@ -0,0 +1,66 @@
+// Copyright (c) Wieslaw Soltes. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+
+#nullable disable
+
+using Avalonia.Controls.DataGridSizing;
+
+namespace Avalonia.Controls
+{
+#if !DATAGRID_INTERNAL
+ public
+#else
+ internal
+#endif
+ partial class DataGrid
+ {
+ private void OnColumnWidthSharingScopeChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ DataGridColumnWidthSharingScope oldScope = e.OldValue as DataGridColumnWidthSharingScope;
+ DataGridColumnWidthSharingScope newScope = e.NewValue as DataGridColumnWidthSharingScope;
+
+ oldScope?.UnregisterGrid(this);
+ newScope?.RegisterGrid(this);
+ }
+
+ internal void OnColumnWidthSharingGroupChanged(
+ DataGridColumn column,
+ string oldGroup,
+ string newGroup)
+ {
+ ColumnWidthSharingScope?.ChangeColumnGroup(column, oldGroup, newGroup);
+ }
+
+ internal void OnColumnWidthSharingColumnAttached(DataGridColumn column)
+ {
+ ColumnWidthSharingScope?.RegisterColumn(column);
+ }
+
+ internal void OnColumnWidthSharingColumnDetached(DataGridColumn column)
+ {
+ ColumnWidthSharingScope?.UnregisterColumn(column);
+ }
+
+ internal void OnColumnWidthSharingColumnMeasured(DataGridColumn column)
+ {
+ ColumnWidthSharingScope?.ReportWidth(column);
+ }
+
+ internal void OnColumnWidthSharingSideEffect(DataGridColumn column)
+ {
+ ColumnWidthSharingScope?.ReportWidth(column);
+ }
+
+ internal void RefreshColumnWidthSharingRegistration()
+ {
+ DataGridColumnWidthSharingScope scope = ColumnWidthSharingScope;
+ if (scope == null)
+ {
+ return;
+ }
+
+ scope.UnregisterGrid(this);
+ scope.RegisterGrid(this, adoptExistingWidths: true);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Columns.Width.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Columns.Width.cs
index d3aa6d6f..b1442ab6 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.Columns.Width.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.Columns.Width.cs
@@ -42,6 +42,7 @@ private static double DecreaseNonStarColumnWidth(DataGridColumn column, double t
Math.Max(targetWidth - column.Width.DisplayValue, amount));
column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment);
+ column.OwningGrid?.OnColumnWidthSharingSideEffect(column);
return amount - adjustment;
}
@@ -109,6 +110,7 @@ private static double IncreaseNonStarColumnWidth(DataGridColumn column, double t
Math.Min(targetWidth - column.Width.DisplayValue, amount));
column.SetWidthDisplayValue(column.Width.DisplayValue + adjustment);
+ column.OwningGrid?.OnColumnWidthSharingSideEffect(column);
return amount - adjustment;
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Columns.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Columns.cs
index d4bfd4b2..08407425 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.Columns.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.Columns.cs
@@ -604,6 +604,8 @@ internal void OnColumnWidthChanged(DataGridColumn updatedColumn)
EnsureHorizontalLayout();
UpdateSummaryRowLayout();
}
+
+ ColumnWidthSharingScope?.ReportWidth(updatedColumn);
}
internal void OnFillerColumnWidthNeeded(double finalWidth)
@@ -1017,6 +1019,7 @@ private void NormalizeStarColumnWidths(int displayIndex, bool userInitiated)
var expected = totalStarColumnsWidth * column.Width.Value / totalStarWeights;
column.SetWidthDesiredValue(expected);
column.SetWidthDisplayValue(expected);
+ OnColumnWidthSharingSideEffect(column);
}
}
@@ -1078,6 +1081,7 @@ private double AdjustStarColumnWidths(int displayIndex, double remainingAdjustme
remainingAdjustment -= adjustment;
totalStarWeights -= starColumnPair.Key.Width.Value;
starColumnPair.Key.SetWidthDisplayValue(Math.Max(DataGrid.DATAGRID_minimumStarColumnWidth, starColumnPair.Key.Width.DisplayValue + adjustment));
+ OnColumnWidthSharingSideEffect(starColumnPair.Key);
}
return remainingAdjustment;
@@ -1397,7 +1401,8 @@ private void OnColumnWidthChanged(AvaloniaPropertyChangedEventArgs e)
{
if (column.InheritsWidth)
{
- column.SetWidthInternalNoCallback(value);
+ column.SetWidthInternalNoCallback(value, preserveInheritance: true);
+ ColumnWidthSharingScope?.ReportWidth(column);
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Properties.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Properties.cs
index 5c1ecfbe..3dbfe303 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.Properties.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.Properties.cs
@@ -11,6 +11,7 @@
using Avalonia.Controls.DataGridSorting;
using Avalonia.Controls.DataGridSearching;
using Avalonia.Controls.DataGridFilling;
+using Avalonia.Controls.DataGridSizing;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Media;
@@ -465,6 +466,21 @@ DataGridLength ColumnWidth
set { SetValue(ColumnWidthProperty, value); }
}
+ ///
+ /// Identifies the dependency property.
+ ///
+ public static readonly StyledProperty ColumnWidthSharingScopeProperty =
+ AvaloniaProperty.Register(nameof(ColumnWidthSharingScope));
+
+ ///
+ /// Gets or sets the scope that synchronizes columns with matching width-sharing group names.
+ ///
+ public DataGridColumnWidthSharingScope ColumnWidthSharingScope
+ {
+ get { return GetValue(ColumnWidthSharingScopeProperty); }
+ set { SetValue(ColumnWidthSharingScopeProperty, value); }
+ }
+
///
/// Identifies the dependency property.
///
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.Template.cs b/src/Avalonia.Controls.DataGrid/DataGrid.Template.cs
index 6a57d976..1397ca01 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.Template.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.Template.cs
@@ -164,6 +164,7 @@ protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
}
}
NormalizeColumnDisplayIndexesAfterDetachedMutations();
+ RefreshColumnWidthSharingRegistration();
if (_columnHeadersPresenter != null && _columnHeadersPresenter.OwningGrid == null)
{
_columnHeadersPresenter.OwningGrid = this;
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index a9b7c705..04eee92f 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -517,6 +517,7 @@ static DataGrid()
ItemsSourceProperty.Changed.AddClassHandler((x, e) => x.OnItemsSourcePropertyChanged(e));
CanUserResizeColumnsProperty.Changed.AddClassHandler((x, e) => x.OnCanUserResizeColumnsChanged(e));
ColumnWidthProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthChanged(e));
+ ColumnWidthSharingScopeProperty.Changed.AddClassHandler((x, e) => x.OnColumnWidthSharingScopeChanged(e));
FrozenColumnCountProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountChanged(e));
FrozenColumnCountRightProperty.Changed.AddClassHandler((x, e) => x.OnFrozenColumnCountRightChanged(e));
GridLinesVisibilityProperty.Changed.AddClassHandler((x, e) => x.OnGridLinesVisibilityChanged(e));
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.Width.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.Width.cs
index e60f9de0..5a6700a6 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumn.Width.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.Width.cs
@@ -126,7 +126,7 @@ internal void ComputeLayoutRoundedWidth(double leftEdge)
///
internal void EnsureWidth()
{
- SetWidthInternalNoCallback(CoerceWidth(this, Width));
+ SetWidthInternalNoCallback(CoerceWidth(this, Width), preserveInheritance: InheritsWidth);
}
///
@@ -255,17 +255,21 @@ internal void SetWidthInternal(DataGridLength width)
/// Sets the column's Width directly, without any callback effects.
///
/// The new Width.
- internal void SetWidthInternalNoCallback(DataGridLength width)
+ /// Whether the internal update should retain grid-level width inheritance.
+ internal void SetWidthInternalNoCallback(DataGridLength width, bool preserveInheritance = false)
{
- var originalValue = _setWidthInternalNoCallback;
+ bool originalNoCallback = _setWidthInternalNoCallback;
+ bool originalInternal = _settingWidthInternally;
_setWidthInternalNoCallback = true;
+ _settingWidthInternally |= preserveInheritance;
try
{
SetCurrentValue(WidthProperty, width);
}
finally
{
- _setWidthInternalNoCallback = originalValue;
+ _setWidthInternalNoCallback = originalNoCallback;
+ _settingWidthInternally = originalInternal;
}
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
index 5e3b023e..d1cc9ecd 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumn.cs
@@ -235,10 +235,24 @@ internal bool InheritsWidth
///
internal bool IsInitialDesiredWidthDetermined
{
- get;
- set;
+ get => _isInitialDesiredWidthDetermined;
+ set
+ {
+ if (_isInitialDesiredWidthDetermined == value)
+ {
+ return;
+ }
+
+ _isInitialDesiredWidthDetermined = value;
+ if (value)
+ {
+ OwningGrid?.OnColumnWidthSharingColumnMeasured(this);
+ }
+ }
}
+ private bool _isInitialDesiredWidthDetermined;
+
internal double LayoutRoundedWidth
{
get;
diff --git a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs
index ec68c492..ba259394 100644
--- a/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGridColumnCollection.cs
@@ -163,6 +163,7 @@ protected override void ClearItems()
for (int columnIndex = 0; columnIndex < ItemsInternal.Count; columnIndex++)
{
// Detach the column...
+ _owningGrid.OnColumnWidthSharingColumnDetached(ItemsInternal[columnIndex]);
ItemsInternal[columnIndex].OwningGrid = null;
ItemsInternal[columnIndex].ClearElementCache();
}
@@ -209,6 +210,7 @@ protected override void InsertItem(int columnIndex, DataGridColumn dataGridColum
ItemsInternal.Insert(columnIndexWithFiller, dataGridColumn);
dataGridColumn.Index = columnIndexWithFiller;
dataGridColumn.OwningGrid = _owningGrid;
+ _owningGrid.OnColumnWidthSharingColumnAttached(dataGridColumn);
dataGridColumn.RemoveEditingElement();
if (dataGridColumn != RowGroupSpacerColumn && dataGridColumn != FillerColumn)
{
@@ -612,6 +614,7 @@ private void RemoveItemPrivate(int columnIndex, bool isSpacer)
VisibleEdgedColumnsWidth -= dataGridColumn.ActualWidth;
}
// continue with the base remove
+ _owningGrid.OnColumnWidthSharingColumnDetached(dataGridColumn);
dataGridColumn.OwningGrid = null;
_owningGrid.OnRemovedColumn_PreNotification(dataGridColumn, headerCell);
_owningGrid.OnColumnCollectionChanged_PreNotification(false /*columnsGrew*/);
diff --git a/src/Avalonia.Controls.DataGrid/Sizing/DataGridColumnWidthSharing.cs b/src/Avalonia.Controls.DataGrid/Sizing/DataGridColumnWidthSharing.cs
new file mode 100644
index 00000000..3a0ede30
--- /dev/null
+++ b/src/Avalonia.Controls.DataGrid/Sizing/DataGridColumnWidthSharing.cs
@@ -0,0 +1,58 @@
+// Copyright (c) Wieslaw Soltes. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+
+#nullable disable
+
+using Avalonia;
+using Avalonia.Controls;
+
+namespace Avalonia.Controls.DataGridSizing
+{
+ ///
+ /// Provides the attached column group used by .
+ ///
+#if !DATAGRID_INTERNAL
+ public
+#else
+ internal
+#endif
+ static class DataGridColumnWidthSharing
+ {
+ ///
+ /// Identifies the Group attached property.
+ ///
+ public static readonly AttachedProperty GroupProperty =
+ AvaloniaProperty.RegisterAttached(
+ "Group",
+ typeof(DataGridColumnWidthSharing));
+
+ static DataGridColumnWidthSharing()
+ {
+ GroupProperty.Changed.AddClassHandler(
+ (column, args) => column.OwningGrid?.OnColumnWidthSharingGroupChanged(
+ column,
+ args.OldValue as string,
+ args.NewValue as string));
+ }
+
+ ///
+ /// Gets the width-sharing group assigned to a column.
+ ///
+ /// The target column.
+ /// The group name, or when sharing is disabled.
+ public static string GetGroup(AvaloniaObject target)
+ {
+ return target.GetValue(GroupProperty);
+ }
+
+ ///
+ /// Sets the width-sharing group assigned to a column.
+ ///
+ /// The target column.
+ /// The group name, or to disable sharing.
+ public static void SetGroup(AvaloniaObject target, string value)
+ {
+ target.SetValue(GroupProperty, value);
+ }
+ }
+}
diff --git a/src/Avalonia.Controls.DataGrid/Sizing/DataGridColumnWidthSharingScope.cs b/src/Avalonia.Controls.DataGrid/Sizing/DataGridColumnWidthSharingScope.cs
new file mode 100644
index 00000000..0713b312
--- /dev/null
+++ b/src/Avalonia.Controls.DataGrid/Sizing/DataGridColumnWidthSharingScope.cs
@@ -0,0 +1,323 @@
+// Copyright (c) Wieslaw Soltes. All rights reserved.
+// Licensed under the MIT license. See LICENSE file in the project root for details.
+
+#nullable disable
+
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Controls.DataGridSizing
+{
+ ///
+ /// Synchronizes the display widths of columns that share the same group name across DataGrid controls.
+ ///
+#if !DATAGRID_INTERNAL
+ public
+#else
+ internal
+#endif
+ sealed class DataGridColumnWidthSharingScope
+ {
+ private readonly Dictionary>> _groups =
+ new(StringComparer.Ordinal);
+ private bool _isSynchronizing;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public DataGridColumnWidthSharingScope()
+ {
+ }
+
+ ///
+ /// Recomputes every registered group using its largest current column width.
+ ///
+ public void Synchronize()
+ {
+ if (_isSynchronizing)
+ {
+ return;
+ }
+
+ foreach (string group in new List(_groups.Keys))
+ {
+ SynchronizeLargestWidth(group);
+ }
+ }
+
+ internal void RegisterGrid(DataGrid grid, bool adoptExistingWidths = false)
+ {
+ if (grid == null)
+ {
+ return;
+ }
+
+ foreach (DataGridColumn column in grid.ColumnsInternal.ItemsInternal)
+ {
+ RegisterColumn(column, adoptExistingWidths);
+ }
+ }
+
+ internal void UnregisterGrid(DataGrid grid)
+ {
+ if (grid == null)
+ {
+ return;
+ }
+
+ foreach (string group in new List(_groups.Keys))
+ {
+ RemoveMatching(
+ group,
+ column => column.OwningGrid == grid ||
+ grid.ColumnsInternal.ItemsInternal.Contains(column));
+ }
+ }
+
+ internal void RegisterColumn(DataGridColumn column, bool adoptExistingWidth = false)
+ {
+ if (column?.OwningGrid == null)
+ {
+ return;
+ }
+
+ string group = DataGridColumnWidthSharing.GetGroup(column);
+ if (string.IsNullOrWhiteSpace(group))
+ {
+ return;
+ }
+
+ List> participants = GetOrCreateGroup(group);
+ Prune(participants);
+ double existingWidth = adoptExistingWidth
+ ? GetLargestMeasuredWidth(participants)
+ : 0;
+ foreach (WeakReference reference in participants)
+ {
+ if (reference.TryGetTarget(out DataGridColumn existing) && ReferenceEquals(existing, column))
+ {
+ return;
+ }
+ }
+
+ participants.Add(new WeakReference(column));
+ if (existingWidth > 0)
+ {
+ ApplyWidth(participants, existingWidth);
+ }
+ else
+ {
+ SynchronizeLargestWidth(group);
+ }
+ }
+
+ internal void UnregisterColumn(DataGridColumn column, string group = null)
+ {
+ if (column == null)
+ {
+ return;
+ }
+
+ group ??= DataGridColumnWidthSharing.GetGroup(column);
+ if (!string.IsNullOrWhiteSpace(group))
+ {
+ RemoveMatching(group, candidate => ReferenceEquals(candidate, column));
+ }
+ }
+
+ internal void ChangeColumnGroup(DataGridColumn column, string oldGroup, string newGroup)
+ {
+ UnregisterColumn(column, oldGroup);
+ if (!string.IsNullOrWhiteSpace(newGroup))
+ {
+ RegisterColumn(column);
+ }
+ }
+
+ internal void ReportWidth(DataGridColumn source)
+ {
+ if (_isSynchronizing || source?.OwningGrid == null || IsUnmeasuredStar(source))
+ {
+ return;
+ }
+
+ string group = DataGridColumnWidthSharing.GetGroup(source);
+ if (string.IsNullOrWhiteSpace(group) ||
+ !_groups.TryGetValue(group, out List> participants))
+ {
+ return;
+ }
+
+ Prune(participants);
+ bool isRegistered = false;
+ foreach (WeakReference reference in participants)
+ {
+ if (reference.TryGetTarget(out DataGridColumn column) &&
+ ReferenceEquals(column, source))
+ {
+ isRegistered = true;
+ break;
+ }
+ }
+
+ if (!isRegistered)
+ {
+ RemoveGroupIfEmpty(group, participants);
+ return;
+ }
+
+ ApplyWidth(participants, source.ActualWidth);
+ RemoveGroupIfEmpty(group, participants);
+ }
+
+ private void SynchronizeLargestWidth(string group)
+ {
+ if (!_groups.TryGetValue(group, out List> participants))
+ {
+ return;
+ }
+
+ Prune(participants);
+ double width = GetLargestMeasuredWidth(participants);
+
+ if (width > 0)
+ {
+ ApplyWidth(participants, width);
+ }
+
+ RemoveGroupIfEmpty(group, participants);
+ }
+
+ private static double GetLargestMeasuredWidth(
+ List> participants)
+ {
+ double width = 0;
+ foreach (WeakReference reference in participants)
+ {
+ if (reference.TryGetTarget(out DataGridColumn column) && !IsUnmeasuredStar(column))
+ {
+ width = Math.Max(width, column.ActualWidth);
+ }
+ }
+
+ return width;
+ }
+
+ private void ApplyWidth(List> participants, double requestedWidth)
+ {
+ if (_isSynchronizing || double.IsNaN(requestedWidth) || double.IsInfinity(requestedWidth))
+ {
+ return;
+ }
+
+ Prune(participants);
+ double minimum = 0;
+ double maximum = double.PositiveInfinity;
+ foreach (WeakReference reference in participants)
+ {
+ if (reference.TryGetTarget(out DataGridColumn column) &&
+ !IsUnmeasuredStar(column))
+ {
+ minimum = Math.Max(minimum, column.ActualMinWidth);
+ maximum = Math.Min(maximum, column.ActualMaxWidth);
+ }
+ }
+
+ double sharedWidth = minimum <= maximum
+ ? Math.Clamp(requestedWidth, minimum, maximum)
+ : requestedWidth;
+
+ _isSynchronizing = true;
+ try
+ {
+ foreach (WeakReference reference in participants)
+ {
+ if (!reference.TryGetTarget(out DataGridColumn column) ||
+ column.OwningGrid == null ||
+ IsUnmeasuredStar(column))
+ {
+ continue;
+ }
+
+ double width = minimum <= maximum
+ ? sharedWidth
+ : Math.Clamp(sharedWidth, column.ActualMinWidth, column.ActualMaxWidth);
+ DataGridLength current = column.Width;
+ if (Math.Abs(column.ActualWidth - width) <= 0.001 && !current.IsStar)
+ {
+ continue;
+ }
+
+ DataGridLength synchronized = current.IsAbsolute || current.IsStar
+ ? new DataGridLength(width)
+ : new DataGridLength(current.Value, current.UnitType, width, width);
+ column.SetWidthInternalNoCallback(synchronized, preserveInheritance: column.InheritsWidth);
+ column.OwningGrid.OnColumnWidthChanged(column);
+ }
+ }
+ finally
+ {
+ _isSynchronizing = false;
+ }
+ }
+
+ private static bool IsUnmeasuredStar(DataGridColumn column)
+ {
+ if (!column.Width.IsStar || column.IsInitialDesiredWidthDetermined)
+ {
+ return false;
+ }
+
+ return double.IsNaN(column.Width.DisplayValue) ||
+ column.Width.DisplayValue <= column.ActualMinWidth + 0.001;
+ }
+
+ private List> GetOrCreateGroup(string group)
+ {
+ if (!_groups.TryGetValue(group, out List> participants))
+ {
+ participants = new List>();
+ _groups.Add(group, participants);
+ }
+
+ return participants;
+ }
+
+ private void RemoveMatching(string group, Predicate predicate)
+ {
+ if (!_groups.TryGetValue(group, out List> participants))
+ {
+ return;
+ }
+
+ for (int index = participants.Count - 1; index >= 0; index--)
+ {
+ if (!participants[index].TryGetTarget(out DataGridColumn column) || predicate(column))
+ {
+ participants.RemoveAt(index);
+ }
+ }
+
+ RemoveGroupIfEmpty(group, participants);
+ }
+
+ private static void Prune(List> participants)
+ {
+ for (int index = participants.Count - 1; index >= 0; index--)
+ {
+ if (!participants[index].TryGetTarget(out DataGridColumn column) || column.OwningGrid == null)
+ {
+ participants.RemoveAt(index);
+ }
+ }
+ }
+
+ private void RemoveGroupIfEmpty(string group, List> participants)
+ {
+ if (participants.Count == 0)
+ {
+ _groups.Remove(group);
+ }
+ }
+ }
+}